Compare commits
561
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37db94649f | ||
|
|
896f4cccc0 | ||
|
|
de3c554e63 | ||
|
|
8417427059 | ||
|
|
ba5a32fb34 | ||
|
|
8b1c94151a | ||
|
|
3623024cce | ||
|
|
2dcbe0302e | ||
|
|
502bddd720 | ||
|
|
0c22fee8c0 | ||
|
|
f69f6eb883 | ||
|
|
cebd9aec96 | ||
|
|
2554a84950 | ||
|
|
49bbdff418 | ||
|
|
8995de9067 | ||
|
|
18b74212ca | ||
|
|
06190d0ef9 | ||
|
|
254a37d757 | ||
|
|
2b5bcf4098 | ||
|
|
4e00934353 | ||
|
|
c524451704 | ||
|
|
e26ce6d076 | ||
|
|
da3dfaee66 | ||
|
|
463335856f | ||
|
|
3315926f7c | ||
|
|
68d3fb6062 | ||
|
|
b108cc1f4a | ||
|
|
167010380e | ||
|
|
dd91f3844a | ||
|
|
b7cf3092da | ||
|
|
60afc12893 | ||
|
|
8677837a8f | ||
|
|
02f6cf7d71 | ||
|
|
85a46c0bba | ||
|
|
30f65bd914 | ||
|
|
bedb45c8aa | ||
|
|
1602520624 | ||
|
|
f69c379ae1 | ||
|
|
fbd0e23fb3 | ||
|
|
8dbb1d84c1 | ||
|
|
ab70c67a4f | ||
|
|
0a6aa90e23 | ||
|
|
22a7152f0d | ||
|
|
b5ca9dbc8f | ||
|
|
48118f6a84 | ||
|
|
771ed93992 | ||
|
|
a8e6f14022 | ||
|
|
84270c023a | ||
|
|
25a19d7951 | ||
|
|
c5665548a0 | ||
|
|
22142be7b5 | ||
|
|
a949c18ec1 | ||
|
|
6f122b5237 | ||
|
|
85e5ef3f59 | ||
|
|
0bf4fc6808 | ||
|
|
a44b4f4e90 | ||
|
|
f2d30e6611 | ||
|
|
242653054d | ||
|
|
95e4dff448 | ||
|
|
a30cf4d3ab | ||
|
|
eebfe8fa4d | ||
|
|
019e5cfb43 | ||
|
|
6ed2ac1b60 | ||
|
|
1853b09443 | ||
|
|
9126faa11c | ||
|
|
090e5132b2 | ||
|
|
bbe6d02568 | ||
|
|
04e7064790 | ||
|
|
ff0bce4d24 | ||
|
|
2ecb4f80ea | ||
|
|
41f9be8595 | ||
|
|
f73f7e733c | ||
|
|
91b53daac9 | ||
|
|
5bcdc72b83 | ||
|
|
3f0670ea9d | ||
|
|
c54b5ae6d7 | ||
|
|
14c16d8fe0 | ||
|
|
e3d1e4a442 | ||
|
|
04605f6855 | ||
|
|
451ac986c0 | ||
|
|
80d478d2c9 | ||
|
|
79719262a2 | ||
|
|
806c4ab643 | ||
|
|
6ee8b64a38 | ||
|
|
a97e5703c9 | ||
|
|
39f140dae1 | ||
|
|
632591f8d8 | ||
|
|
66e994b5ab | ||
|
|
1d56094388 | ||
|
|
69f572288f | ||
|
|
1a5c7f2a2c | ||
|
|
68734f6ac5 | ||
|
|
333c408d1b | ||
|
|
3c57328b3c | ||
|
|
08178b60fe | ||
|
|
4342233876 | ||
|
|
89cb467016 | ||
|
|
9488a8647e | ||
|
|
fa1d1f312a | ||
|
|
b359224ecd | ||
|
|
a909aa880a | ||
|
|
8f8fec0816 | ||
|
|
c2d20492c3 | ||
|
|
4a7eade70d | ||
|
|
6466d8d8fd | ||
|
|
7c72da978d | ||
|
|
76b842e6a0 | ||
|
|
00cefe14f7 | ||
|
|
2a3034be2c | ||
|
|
910633819a | ||
|
|
daddd0a005 | ||
|
|
cdd6ae8ec9 | ||
|
|
65c9454370 | ||
|
|
8a1e9b49bc | ||
|
|
f288f9148c | ||
|
|
e165f6b65c | ||
|
|
019730c756 | ||
|
|
7fa951b7fa | ||
|
|
b39b19f107 | ||
|
|
88db7a8383 | ||
|
|
d782bccb5f | ||
|
|
84947d6b33 | ||
|
|
f79849d9fb | ||
|
|
cb38639e11 | ||
|
|
8f567c83c6 | ||
|
|
8876abde1c | ||
|
|
2d95bf9043 | ||
|
|
0475d8c43d | ||
|
|
491199cfea | ||
|
|
5aa40d2e61 | ||
|
|
c5b6a858e1 | ||
|
|
e28051a109 | ||
|
|
1471e53a91 | ||
|
|
6ae4f980e4 | ||
|
|
6fbacbcae6 | ||
|
|
e934f24d3e | ||
|
|
191bc1318e | ||
|
|
249219fe57 | ||
|
|
5a86d4cb4a | ||
|
|
75a5d7f52a | ||
|
|
c078637d3d | ||
|
|
6c41e774ff | ||
|
|
c630a4e41a | ||
|
|
33956ea550 | ||
|
|
2e45cc39f7 | ||
|
|
c8860ab837 | ||
|
|
64ab091807 | ||
|
|
243275b414 | ||
|
|
70d7953b45 | ||
|
|
812e7bf593 | ||
|
|
e5c2d8bd36 | ||
|
|
670961acce | ||
|
|
b46410c43b | ||
|
|
2cd7db9ade | ||
|
|
3ba9079101 | ||
|
|
db1d31b1e3 | ||
|
|
d0e712a3c5 | ||
|
|
b86a2775a1 | ||
|
|
2d16da9dd5 | ||
|
|
9c0f9a1434 | ||
|
|
d58b0c90fd | ||
|
|
bf4cec4ea5 | ||
|
|
2d75ee4ee8 | ||
|
|
37569d069d | ||
|
|
f44647ba52 | ||
|
|
518c1d9d57 | ||
|
|
7caff20255 | ||
|
|
69698c16f7 | ||
|
|
3705afb593 | ||
|
|
ebe7200d65 | ||
|
|
b457f070b9 | ||
|
|
4f44274111 | ||
|
|
51c6108e35 | ||
|
|
3a71cd5b02 | ||
|
|
9424e2ec71 | ||
|
|
d50d19b609 | ||
|
|
bf280d945e | ||
|
|
0b469e6a5a | ||
|
|
57da2f1181 | ||
|
|
c771764251 | ||
|
|
a004585cba | ||
|
|
92a1f70eb1 | ||
|
|
1d0c582729 | ||
|
|
0f947b5115 | ||
|
|
8852856d45 | ||
|
|
f7a882b0ab | ||
|
|
35b127def2 | ||
|
|
13a5347412 | ||
|
|
ca235486bb | ||
|
|
89f29d5d0e | ||
|
|
99929c7d64 | ||
|
|
8836cc851b | ||
|
|
7849e2e04c | ||
|
|
0bf5bf030f | ||
|
|
21cc6936f7 | ||
|
|
5f225953c2 | ||
|
|
9fbeac628c | ||
|
|
df102fff84 | ||
|
|
d3cd883782 | ||
|
|
7ce54c8561 | ||
|
|
faa18c99ab | ||
|
|
a73b139256 | ||
|
|
02066bf380 | ||
|
|
925cd6d062 | ||
|
|
f45aa843e5 | ||
|
|
9f4b2f015d | ||
|
|
16b3936649 | ||
|
|
8b8104412c | ||
|
|
2bbabff979 | ||
|
|
39dc0963ce | ||
|
|
c37efa53b3 | ||
|
|
fe629b8bf5 | ||
|
|
a8e15b98dd | ||
|
|
1a71f055ff | ||
|
|
fa68fcad50 | ||
|
|
66e58b31c2 | ||
|
|
938429dca7 | ||
|
|
b9eb77f9e1 | ||
|
|
a9b331044f | ||
|
|
db22e99b0f | ||
|
|
290da45cde | ||
|
|
56e8cde0f9 | ||
|
|
6df7aacb56 | ||
|
|
c2f19ef3cf | ||
|
|
6ac91f10ab | ||
|
|
52930df0a9 | ||
|
|
ac9f765cb6 | ||
|
|
4962e0e4c3 | ||
|
|
020636d293 | ||
|
|
0f172ccbab | ||
|
|
635219a556 | ||
|
|
5e53a2e188 | ||
|
|
39b4b616b0 | ||
|
|
0471a007ce | ||
|
|
b60f218e29 | ||
|
|
67fb65dcf5 | ||
|
|
a6f3fd79b3 | ||
|
|
ab60119266 | ||
|
|
6a40d6b6f1 | ||
|
|
10a4868d8f | ||
|
|
01da8b8198 | ||
|
|
787ffd7bcb | ||
|
|
462b6fc462 | ||
|
|
97603f84ec | ||
|
|
fba84ebb9a | ||
|
|
edfe1d59c1 | ||
|
|
7b95f083e7 | ||
|
|
33c8945d42 | ||
|
|
4e029570d0 | ||
|
|
e5a418d7b2 | ||
|
|
b38e247bff | ||
|
|
59248ee520 | ||
|
|
1ec0b90f9b | ||
|
|
4dca80b6b1 | ||
|
|
2317d981b1 | ||
|
|
dbd096dfbf | ||
|
|
edef4f4893 | ||
|
|
9712bc0f47 | ||
|
|
e0d0571078 | ||
|
|
c467eb3b95 | ||
|
|
57f6ffa3d4 | ||
|
|
2df38b2962 | ||
|
|
f947fa5f17 | ||
|
|
9a8c9670c6 | ||
|
|
301b75c003 | ||
|
|
ceeb64282f | ||
|
|
d68e47d1cc | ||
|
|
b04014a87f | ||
|
|
8854726916 | ||
|
|
f182ac7e91 | ||
|
|
68a366a30b | ||
|
|
dfb3c6e614 | ||
|
|
6af6ecefad | ||
|
|
8039862142 | ||
|
|
b6f164b43b | ||
|
|
962d2c0319 | ||
|
|
12b4a4fc5f | ||
|
|
b99840a937 | ||
|
|
f9938a51e9 | ||
|
|
2fa04b079d | ||
|
|
c22a9ff5ae | ||
|
|
66024ebfa2 | ||
|
|
66e4a12819 | ||
|
|
216b1707ae | ||
|
|
bdc21920eb | ||
|
|
5d2a21dd69 | ||
|
|
7f5acb9daf | ||
|
|
7728c6e818 | ||
|
|
ea6d2958e7 | ||
|
|
04a4a1b226 | ||
|
|
b59c2ba705 | ||
|
|
4ddd650102 | ||
|
|
5f63f8b37a | ||
|
|
9110cbe722 | ||
|
|
c2144dcfc8 | ||
|
|
39d03c4ef6 | ||
|
|
d4107df833 | ||
|
|
196dd08943 | ||
|
|
199635be0b | ||
|
|
d82921976b | ||
|
|
8bfc3bc699 | ||
|
|
e348b94720 | ||
|
|
35df2b2b27 | ||
|
|
a577ee7796 | ||
|
|
8d528387db | ||
|
|
90db3d3915 | ||
|
|
4d7a79f517 | ||
|
|
5b8ce8f048 | ||
|
|
7fd5493995 | ||
|
|
7af945486c | ||
|
|
5bd0d249f9 | ||
|
|
0cd192107d | ||
|
|
3aa98d458d | ||
|
|
eca1be7d81 | ||
|
|
bdc22e87ae | ||
|
|
9b73e4d1c9 | ||
|
|
439dc2653e | ||
|
|
9fee71154e | ||
|
|
afedec45ae | ||
|
|
5544946769 | ||
|
|
debf9ce96d | ||
|
|
c93b0e049d | ||
|
|
6eb7fa4c8b | ||
|
|
a02653c5ab | ||
|
|
12bb846e50 | ||
|
|
58a38ad809 | ||
|
|
b9657d6f45 | ||
|
|
da27d820f7 | ||
|
|
e07ec5a54a | ||
|
|
27c5bafb6b | ||
|
|
3291dc1e6e | ||
|
|
7ada7ea3c8 | ||
|
|
fc87506965 | ||
|
|
0084457008 | ||
|
|
d874d5a388 | ||
|
|
53332157e3 | ||
|
|
e70c1423a4 | ||
|
|
9dbf431a55 | ||
|
|
0f41b2786c | ||
|
|
86d588ffc4 | ||
|
|
c19b92b242 | ||
|
|
8027b627d8 | ||
|
|
9c34198138 | ||
|
|
6226604fa5 | ||
|
|
98d5bf5ce5 | ||
|
|
9bf04639b3 | ||
|
|
e8671640e3 | ||
|
|
daf71b0e8c | ||
|
|
d2b016ddc7 | ||
|
|
4bb6cf8489 | ||
|
|
cd8a41524f | ||
|
|
68a43d55b9 | ||
|
|
8d3f283f4c | ||
|
|
2cd1e39ec5 | ||
|
|
45ceaf8b39 | ||
|
|
dc17e9aa44 | ||
|
|
bd9dc51578 | ||
|
|
1b3c23587d | ||
|
|
da24cc584b | ||
|
|
55b1814c1e | ||
|
|
98976ea745 | ||
|
|
e9f0a49389 | ||
|
|
e414bc2097 | ||
|
|
6711753bec | ||
|
|
076c3d67f8 | ||
|
|
6af269da11 | ||
|
|
b98fe211f0 | ||
|
|
c70ac16978 | ||
|
|
6ad563fd4a | ||
|
|
8be1bab1b6 | ||
|
|
a553360868 | ||
|
|
944bada2a2 | ||
|
|
8ed855723b | ||
|
|
5b9807c5d3 | ||
|
|
d9e5153452 | ||
|
|
92b310e5e2 | ||
|
|
02d973e4e4 | ||
|
|
2f41fc080c | ||
|
|
80684f7e07 | ||
|
|
e3ad918755 | ||
|
|
178fb1a484 | ||
|
|
2b0f1dcb48 | ||
|
|
154fbd774f | ||
|
|
a497d2a14f | ||
|
|
00af8c4cd7 | ||
|
|
3a922418ca | ||
|
|
96d7f5065c | ||
|
|
fb85702ee9 | ||
|
|
69cad4caab | ||
|
|
23e508138b | ||
|
|
593b9f4760 | ||
|
|
5b44caa358 | ||
|
|
28d71e63a9 | ||
|
|
84addc7996 | ||
|
|
3f8d68619a | ||
|
|
d941334fa3 | ||
|
|
9c36d33cb2 | ||
|
|
6b03a1441a | ||
|
|
f40d3026e6 | ||
|
|
2ea122c4a9 | ||
|
|
dfce271c37 | ||
|
|
d40724205c | ||
|
|
55afd1e436 | ||
|
|
eb3e3d61db | ||
|
|
5da0e3c428 | ||
|
|
f464d10971 | ||
|
|
6e0bc22c17 | ||
|
|
bb95999112 | ||
|
|
9c0f92d034 | ||
|
|
9d04958ff2 | ||
|
|
cfe0d4a97f | ||
|
|
0995484c09 | ||
|
|
ec2254cf74 | ||
|
|
71ae403188 | ||
|
|
6689919b3b | ||
|
|
246e49e521 | ||
|
|
eb065b20d7 | ||
|
|
038c859133 | ||
|
|
434afa7f62 | ||
|
|
fd492b91a0 | ||
|
|
48a41af3b0 | ||
|
|
6da9eb55ba | ||
|
|
22efd8b6e0 | ||
|
|
b71d779900 | ||
|
|
6c3dbd90a0 | ||
|
|
1b16098121 | ||
|
|
092fcfb009 | ||
|
|
008a0927b6 | ||
|
|
3c2f75e438 | ||
|
|
09c354337d | ||
|
|
dc6387cc27 | ||
|
|
bff18a05d1 | ||
|
|
6717d7e901 | ||
|
|
bbf4b17494 | ||
|
|
68b4bfc439 | ||
|
|
77f5350496 | ||
|
|
3c1914c30a | ||
|
|
ae8ec4b4ef | ||
|
|
bdd421e1da | ||
|
|
7b480d1b8b | ||
|
|
2a425335f0 | ||
|
|
02f8b486f3 | ||
|
|
f0966c86f3 | ||
|
|
65c6c2a284 | ||
|
|
9fe05722f9 | ||
|
|
61be7c317f | ||
|
|
df35497af2 | ||
|
|
8614e1bdd1 | ||
|
|
c5c38247cc | ||
|
|
f9c0d3fe58 | ||
|
|
44c0b69c4d | ||
|
|
0cfa7b81ef | ||
|
|
9caf47d81b | ||
|
|
3c27d3b377 | ||
|
|
66b96102f8 | ||
|
|
7fc05df240 | ||
|
|
d32f124974 | ||
|
|
fe4d999643 | ||
|
|
cd84866ee8 | ||
|
|
cd5cac7f69 | ||
|
|
377031c0a9 | ||
|
|
85d4a1de38 | ||
|
|
44d7aa3641 | ||
|
|
53732c5df3 | ||
|
|
265b44187b | ||
|
|
f46f9c474e | ||
|
|
049c92785d | ||
|
|
37ec729d1d | ||
|
|
2747ad366e | ||
|
|
7de3d5c9bc | ||
|
|
f952fa7095 | ||
|
|
e62da29a49 | ||
|
|
d3258edbc7 | ||
|
|
2096925a02 | ||
|
|
3db44bab85 | ||
|
|
bba12146ea | ||
|
|
4403a31c93 | ||
|
|
6ee2fc3b0e | ||
|
|
3b0ab53d86 | ||
|
|
7683e7b91e | ||
|
|
3ba6202b43 | ||
|
|
a49212ca64 | ||
|
|
88fe53b907 | ||
|
|
52f80281bd | ||
|
|
2f93822996 | ||
|
|
8d8cbc8b28 | ||
|
|
a7c5f0d3df | ||
|
|
60d83530ae | ||
|
|
569aecde4a | ||
|
|
9fd79aadd5 | ||
|
|
e031672d72 | ||
|
|
289a2cd78a | ||
|
|
aab4f02dc0 | ||
|
|
5b37131bfd | ||
|
|
79ceaf67a1 | ||
|
|
b0dbdeafa3 | ||
|
|
3bc21a5b30 | ||
|
|
953182e56a | ||
|
|
39cea14183 | ||
|
|
8baf28fcfe | ||
|
|
619c4bce21 | ||
|
|
cc80fc5239 | ||
|
|
f9039fdf67 | ||
|
|
bb93d72f66 | ||
|
|
86be2eb956 | ||
|
|
eb40903372 | ||
|
|
039d2cd032 | ||
|
|
365063e8d5 | ||
|
|
b43b076ca0 | ||
|
|
71a7721c52 | ||
|
|
2bcad83409 | ||
|
|
6750abcc11 | ||
|
|
46d3408be9 | ||
|
|
8015a23342 | ||
|
|
4e6bdce019 | ||
|
|
68519ac00c | ||
|
|
50da953dc4 | ||
|
|
f5776450f0 | ||
|
|
3b3283c6b0 | ||
|
|
3f06efe8f4 | ||
|
|
2d021e886f | ||
|
|
96f201cdb4 | ||
|
|
57c1d21407 | ||
|
|
5d8068c964 | ||
|
|
418c47f483 | ||
|
|
08c001afbb | ||
|
|
052a2ac06c | ||
|
|
38041b01cf | ||
|
|
fdda25464b | ||
|
|
6bfdf66cdd | ||
|
|
82f8d0a550 | ||
|
|
81bfe41e25 | ||
|
|
db0e627f5c | ||
|
|
da970354e8 | ||
|
|
045e5cd5da | ||
|
|
e517377bdd | ||
|
|
520b5221b5 | ||
|
|
c567b282a5 | ||
|
|
5f19abebbf | ||
|
|
58eda56f37 | ||
|
|
50cab402d4 | ||
|
|
fa7161d371 | ||
|
|
252808a467 | ||
|
|
31e9a8d56e | ||
|
|
ba1b68d8fa | ||
|
|
dcd82fa58e | ||
|
|
4132a05c8f | ||
|
|
47f8dd5e22 | ||
|
|
9b30ec7016 | ||
|
|
d10f229ffa | ||
|
|
061ed674e3 | ||
|
|
5b5f781095 | ||
|
|
48f7aefbf6 | ||
|
|
8f111badf6 | ||
|
|
687bda68a1 | ||
|
|
8e2c986ae4 | ||
|
|
faa95e664a | ||
|
|
c9230da168 | ||
|
|
7002fbec0e | ||
|
|
496d27b861 | ||
|
|
3c37b80bc2 |
@@ -1,12 +0,0 @@
|
||||
<factorypath>
|
||||
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/io.github.llamalad7/mixinextras-fabric/0.4.1/8d1a9e96afb990367fa1f904d17580d164da72e3/mixinextras-fabric-0.4.1.jar" enabled="true" runInBatchMode="false"/>
|
||||
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm-analysis/9.7.1/f97a3b319f0ed6a8cd944dc79060d3912a28985f/asm-analysis-9.7.1.jar" enabled="true" runInBatchMode="false"/>
|
||||
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/net.fabricmc/fabric-mixin-compile-extensions/0.6.0/6fda0c85c419f0a259da017a1424f27507eb30f0/fabric-mixin-compile-extensions-0.6.0.jar" enabled="true" runInBatchMode="false"/>
|
||||
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/net.fabricmc/mapping-io/0.2.1/2a1aeedce104191bbb9e39e26abfbfeaf9b0590b/mapping-io-0.2.1.jar" enabled="true" runInBatchMode="false"/>
|
||||
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm-tree/9.7.1/3a53139787663b139de76b627fca0084ab60d32c/asm-tree-9.7.1.jar" enabled="true" runInBatchMode="false"/>
|
||||
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/fabric-loom/1.21.4/net.fabricmc.yarn.1_21_4.1.21.4+build.7-v2/mappings.jar" enabled="true" runInBatchMode="false"/>
|
||||
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/net.fabricmc/sponge-mixin/0.15.4+mixin.0.8.7/6a12aacc794f1078458433116e9ed42c1cc98096/sponge-mixin-0.15.4+mixin.0.8.7.jar" enabled="true" runInBatchMode="false"/>
|
||||
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm/9.7.1/f0ed132a49244b042cd0e15702ab9f2ce3cc8436/asm-9.7.1.jar" enabled="true" runInBatchMode="false"/>
|
||||
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm-util/9.7.1/9e23359b598ec6b74b23e53110dd5c577adf2243/asm-util-9.7.1.jar" enabled="true" runInBatchMode="false"/>
|
||||
<factorypathentry kind="EXTJAR" id="/Users/klestiselimaj/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm-commons/9.7.1/406c6a2225cfe1819f102a161e54cc16a5c24f75/asm-commons-9.7.1.jar" enabled="true" runInBatchMode="false"/>
|
||||
</factorypath>
|
||||
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env node
|
||||
// Publish the jars attached to a Gitea release to Modrinth.
|
||||
//
|
||||
// Creates one Modrinth version per jar (saturn-client-<version>+<mc>.jar), for
|
||||
// that Minecraft version, with Fabric API as a required dependency and the
|
||||
// Gitea release notes as the changelog. Versions that already exist on
|
||||
// Modrinth are skipped, so the script is safe to re-run.
|
||||
//
|
||||
// Runs on plain Node (>= 18: fetch, FormData, Blob) with no dependencies, since
|
||||
// every Gitea Actions runner has Node to run JavaScript actions.
|
||||
//
|
||||
// Environment:
|
||||
// TAG release tag, e.g. 0.1.1-beta
|
||||
// SERVER_URL public Gitea URL, e.g. https://git.selimaj.dev
|
||||
// REPO owner/name, e.g. saturnclientmc/saturnclient
|
||||
// MODRINTH_TOKEN Modrinth PAT with the "Create versions" scope
|
||||
// DRY_RUN "true" to check everything and print what would be
|
||||
// published without uploading (no token needed)
|
||||
|
||||
const MODRINTH_API = "https://api.modrinth.com/v2";
|
||||
const MODRINTH_PROJECT_ID = "i6JDSY9x"; // saturnclient
|
||||
const FABRIC_API_PROJECT_ID = "P7dR8mSH";
|
||||
const USER_AGENT =
|
||||
"saturnclientmc/saturnclient release workflow (https://git.selimaj.dev/saturnclientmc/saturnclient)";
|
||||
|
||||
const JAR_NAME = /^saturn-client-(?<version>.+)\+(?<mc>\d+\.\d+(?:\.\d+)?)\.jar$/;
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function http(url, options = {}) {
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
headers: { "User-Agent": USER_AGENT, ...options.headers },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const error = new Error(`${options.method ?? "GET"} ${url} returned ${res.status}: ${await res.text()}`);
|
||||
error.status = res.status;
|
||||
throw error;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async function getJson(url) {
|
||||
return (await http(url)).json();
|
||||
}
|
||||
|
||||
function versionType(tag) {
|
||||
const t = tag.toLowerCase();
|
||||
if (t.includes("alpha")) return "alpha";
|
||||
if (t.includes("beta") || t.includes("rc") || t.includes("pre")) return "beta";
|
||||
return "release";
|
||||
}
|
||||
|
||||
function compareMc(a, b) {
|
||||
const pa = a.split(".").map(Number);
|
||||
const pb = b.split(".").map(Number);
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
||||
if (diff !== 0) return diff;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { TAG: tag, SERVER_URL, REPO: repo } = process.env;
|
||||
const serverUrl = (SERVER_URL ?? "").replace(/\/+$/, "");
|
||||
const dryRun = (process.env.DRY_RUN ?? "").toLowerCase() === "true";
|
||||
const token = process.env.MODRINTH_TOKEN ?? "";
|
||||
|
||||
if (!tag || !serverUrl || !repo) fail("TAG, SERVER_URL and REPO must be set.");
|
||||
if (!token && !dryRun) {
|
||||
fail("MODRINTH_TOKEN is not set. Add it as a repository secret (Settings → Actions → Secrets).");
|
||||
}
|
||||
|
||||
let release;
|
||||
try {
|
||||
release = await getJson(`${serverUrl}/api/v1/repos/${repo}/releases/tags/${encodeURIComponent(tag)}`);
|
||||
} catch (e) {
|
||||
if (e.status === 404) fail(`No Gitea release with tag ${tag}.`);
|
||||
throw e;
|
||||
}
|
||||
const changelog = release.body ?? "";
|
||||
|
||||
const jars = (release.assets ?? [])
|
||||
.map((asset) => ({ name: asset.name, match: JAR_NAME.exec(asset.name) }))
|
||||
.filter(({ match }) => match)
|
||||
.map(({ name, match }) => ({ name, version: match.groups.version, mc: match.groups.mc }))
|
||||
.sort((a, b) => compareMc(a.mc, b.mc));
|
||||
|
||||
if (jars.length === 0) fail(`Release ${tag} has no saturn-client-*.jar assets. Run the Release workflow first.`);
|
||||
|
||||
for (const jar of jars) {
|
||||
if (jar.version !== tag.replace(/^v/, "")) fail(`${jar.name} is version ${jar.version}, but the release tag is ${tag}.`);
|
||||
}
|
||||
|
||||
const knownGameVersions = new Set((await getJson(`${MODRINTH_API}/tag/game_version`)).map((g) => g.version));
|
||||
const unknown = [...new Set(jars.map((j) => j.mc))].filter((mc) => !knownGameVersions.has(mc));
|
||||
if (unknown.length > 0) fail(`Modrinth doesn't know these Minecraft versions: ${unknown.join(", ")}`);
|
||||
|
||||
const existing = new Set(
|
||||
(await getJson(`${MODRINTH_API}/project/${MODRINTH_PROJECT_ID}/version`)).map((v) => v.version_number),
|
||||
);
|
||||
const kind = versionType(tag);
|
||||
const failed = [];
|
||||
|
||||
for (const { name, version, mc } of jars) {
|
||||
const versionNumber = `${version}+${mc}`;
|
||||
|
||||
if (existing.has(versionNumber)) {
|
||||
console.log(`skip ${versionNumber}: already on Modrinth`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Gitea's own download URLs use its internal ROOT_URL, so build a public one.
|
||||
const downloadUrl = `${serverUrl}/${repo}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(name)}`;
|
||||
|
||||
const data = {
|
||||
project_id: MODRINTH_PROJECT_ID,
|
||||
name: `Saturn Client ${versionNumber}`,
|
||||
version_number: versionNumber,
|
||||
changelog,
|
||||
dependencies: [{ project_id: FABRIC_API_PROJECT_ID, dependency_type: "required" }],
|
||||
game_versions: [mc],
|
||||
version_type: kind,
|
||||
loaders: ["fabric"],
|
||||
featured: false,
|
||||
status: "listed",
|
||||
file_parts: ["file"],
|
||||
primary_file: "file",
|
||||
};
|
||||
|
||||
if (dryRun) {
|
||||
const probe = await http(downloadUrl, { headers: { Range: "bytes=0-0" } });
|
||||
await probe.arrayBuffer();
|
||||
console.log(`would publish ${versionNumber} (${kind}, Minecraft ${mc}) from ${downloadUrl} [reachable]`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const jarBytes = await (await http(downloadUrl)).arrayBuffer();
|
||||
|
||||
const form = new FormData();
|
||||
form.append("data", JSON.stringify(data));
|
||||
form.append("file", new Blob([jarBytes], { type: "application/java-archive" }), name);
|
||||
|
||||
await http(`${MODRINTH_API}/version`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: token },
|
||||
body: form,
|
||||
});
|
||||
console.log(`publish ${versionNumber}: done (${(jarBytes.byteLength / 1_000_000).toFixed(1)} MB)`);
|
||||
} catch (e) {
|
||||
console.error(`publish ${versionNumber}: FAILED ${e.message}`);
|
||||
failed.push(versionNumber);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed.length > 0) fail(`Failed to publish: ${failed.join(", ")}`);
|
||||
}
|
||||
|
||||
main().catch((e) => fail(e.stack ?? String(e)));
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Check
|
||||
|
||||
# Compiles every Minecraft version on pull requests, so a change that breaks
|
||||
# one version (e.g. a missed or bad port) fails before it's merged instead of
|
||||
# in the release build. Merges to master aren't re-checked: the PR already
|
||||
# checked that code.
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
compile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
# The JDK comes from the runner's tool cache. `cache: gradle` stores Gradle's
|
||||
# dependencies, Loom's Minecraft cache and the build cache in the runner's
|
||||
# actions cache.
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
cache: gradle
|
||||
|
||||
- name: Compile all versions
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
# --continue reports every version that fails, not just the first.
|
||||
./gradlew compileJava --no-daemon --stacktrace --continue
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Publish to Modrinth
|
||||
|
||||
# Publishes the jars already attached to a Gitea release (built by the Release
|
||||
# workflow) to Modrinth, one version per Minecraft version. Run it manually
|
||||
# after smoke-testing the release. It doesn't rebuild anything, and versions
|
||||
# already on Modrinth are skipped, so re-running is safe.
|
||||
#
|
||||
# Needs a MODRINTH_TOKEN repository secret: a Modrinth PAT with the
|
||||
# "Create versions" scope.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Tag of the Gitea release to publish
|
||||
required: true
|
||||
dry_run:
|
||||
description: Only check and print what would be published
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Publish to Modrinth
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
SERVER_URL: ${{ github.server_url }}
|
||||
REPO: ${{ github.repository }}
|
||||
MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
|
||||
# Node is always available on Gitea runners (it runs JavaScript actions).
|
||||
run: node .gitea/scripts/publish_modrinth.mjs
|
||||
@@ -0,0 +1,101 @@
|
||||
name: Release
|
||||
|
||||
# Publishing a release builds every Minecraft version and attaches the mod jars
|
||||
# to it. mod_version in gradle.properties is the source of truth: the release
|
||||
# tag must match it (a leading "v" is allowed), so bump mod_version in a PR
|
||||
# before tagging the release.
|
||||
#
|
||||
# To (re)attach jars to an existing release, run this workflow manually with
|
||||
# the release's tag.
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Tag of the existing release to build and attach jars to
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
TAG: ${{ github.event.release.tag_name || inputs.tag }}
|
||||
# The public address. Uploads go through the Cloudflare tunnel, which caps
|
||||
# request bodies at 100 MB, so each mod jar must stay under that.
|
||||
GITEA_API: ${{ github.server_url }}/api/v1
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ env.TAG }}
|
||||
submodules: true
|
||||
|
||||
- name: Check tag matches mod_version
|
||||
run: |
|
||||
MOD_VERSION="$(sed -n 's/^mod_version=//p' gradle.properties | tr -d '[:space:]')"
|
||||
if [ "${TAG#v}" != "$MOD_VERSION" ]; then
|
||||
echo "::error::Release tag '$TAG' doesn't match mod_version '$MOD_VERSION' in gradle.properties. Bump mod_version to '${TAG#v}' on the release branch, or tag the release '$MOD_VERSION'." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Building Saturn Client $MOD_VERSION"
|
||||
|
||||
# The JDK comes from the runner's tool cache. `cache: gradle` stores Gradle's
|
||||
# dependencies, Loom's Minecraft cache and the build cache in the runner's
|
||||
# actions cache.
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
cache: gradle
|
||||
|
||||
- name: Build all versions
|
||||
run: |
|
||||
chmod +x gradlew
|
||||
# buildAll remaps every Minecraft version (two at a time, to stay within the
|
||||
# Gradle heap) into build/allJars.
|
||||
./gradlew buildAll --no-daemon --stacktrace
|
||||
|
||||
- name: Upload jars to the release
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
jars=(build/allJars/*.jar)
|
||||
if [ ${#jars[@]} -eq 0 ]; then
|
||||
echo "No jars found in build/allJars" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Cloudflare in front of the server rejects uploads of 100 MB or more.
|
||||
for jar in "${jars[@]}"; do
|
||||
size=$(stat -c%s "$jar")
|
||||
if [ "$size" -ge 100000000 ]; then
|
||||
echo "::error::$(basename "$jar") is $((size / 1000000)) MB; uploads must be under 100 MB. Check what's taking space with: unzip -l \"$jar\" | sort -n" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Look the release up by tag so manual runs work too.
|
||||
RELEASE_ID="$(curl --fail-with-body -sS -H "Authorization: token $TOKEN" \
|
||||
"$GITEA_API/repos/$REPO/releases/tags/$TAG" \
|
||||
| grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)"
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "No release found for tag $TAG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for jar in "${jars[@]}"; do
|
||||
echo "Uploading $(basename "$jar") to release $TAG ($RELEASE_ID)"
|
||||
curl --fail-with-body -sS -X POST \
|
||||
-H "Authorization: token $TOKEN" \
|
||||
-F "attachment=@$jar" \
|
||||
"$GITEA_API/repos/$REPO/releases/$RELEASE_ID/assets"
|
||||
echo
|
||||
done
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "common"]
|
||||
path = common
|
||||
url = https://git.selimaj.dev/saturnclientmc/common.git
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Licenses
|
||||
|
||||
This project is under [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0), but a lot of code taken from other open source projects that are under other licenses that need to be mentioned due to legal reasons.
|
||||
This project is under [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0), but a lot of code taken from other open source projects that are under other licenses that need to be mentioned due to legal or ethical reasons.
|
||||
|
||||
| File | License |
|
||||
| --------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
||||
|
||||
@@ -1,19 +1,86 @@
|
||||
<p align="center">
|
||||
<img src="src/main/resources/assets/saturnclient/icon.png" alt="OSUI" width="150px"/>
|
||||
<img src="logo.png" alt="Saturn Client Logo" width="150px"/>
|
||||
</p>
|
||||
|
||||
<h1 align="center">Saturn Client</h1>
|
||||
|
||||
<p align="center">
|
||||
<b>Saturn Client is a free and open source Minecraft client</b>
|
||||
<b>A free and open-source Minecraft client.</b><br/>
|
||||
Built with performance, customization, and community in mind.
|
||||
</p>
|
||||
|
||||

|
||||
<p align="center">
|
||||
<img src="screenshot.png" alt="Saturn Client Screenshot"/>
|
||||
</p>
|
||||
|
||||
# Credits
|
||||
## About
|
||||
|
||||
- Cloak rendering: `IIpho3nix` & `Dhyan Blum`
|
||||
- Who Am I: `FokshaWasTaken`
|
||||
- Panorama: https://www.planetminecraft.com/texture-pack/minecraft-cavesand-cliffs-part-ii-main-menu-panorama/
|
||||
Saturn Client is a free, open-source Minecraft client designed to enhance the gameplay experience while staying lightweight and customizable.
|
||||
Our goal is to build a clean, optimized client that the community can contribute to and improve together.
|
||||
|
||||
If any of the code or assets are in your name and you want to remove them, please submit an issue, we are still in alpha to have any artists so we would also appreciate if you could help us make some cosmetics
|
||||
Saturn runs on Fabric and supports **Minecraft 1.21.4 to 1.21.11**, with one build per version. If something doesn't work on your version, please open an issue.
|
||||
|
||||
## Features
|
||||
|
||||
- Free cosmetics that don't require real money: cloaks, including animated ones, and hats
|
||||
- 18 built-in mods: Armor Display, Auto Sprint, Clock, Coordinates, Crosshair, Day Counter, FPS Display, Freelook, Fullbright, Health Display, Keystrokes, Nametags, No Fog, Ping Display, Speedometer, Status Effects, TPS Display and Zoom
|
||||
- Clean and modern UI with animations
|
||||
- Ongoing performance improvements
|
||||
- Open-source and community-driven
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install [Fabric Loader](https://fabricmc.net/use/) and [Fabric API](https://modrinth.com/mod/fabric-api) for your Minecraft version.
|
||||
2. Download the Saturn Client jar for the same version from [Modrinth](https://modrinth.com/mod/saturnclient) or the [releases page](https://git.selimaj.dev/saturnclientmc/saturnclient/releases).
|
||||
3. Put it in your `mods` folder.
|
||||
|
||||
### Network use
|
||||
|
||||
For cosmetics, Saturn Client connects to the Saturn server (`saturn-server.selimaj.dev`). You're signed in the same way as on an online-mode Minecraft server: your client proves who you are to Mojang's session server, and the Saturn server asks Mojang to confirm it. **Your Minecraft access token is only ever sent to Mojang, never to the Saturn server.**
|
||||
|
||||
## Building from source
|
||||
|
||||
The version-independent code lives in the `common` submodule, so clone with submodules:
|
||||
|
||||
```sh
|
||||
git clone --recurse-submodules https://git.selimaj.dev/saturnclientmc/saturnclient.git
|
||||
cd saturnclient
|
||||
|
||||
./gradlew buildAll # every version → build/allJars/
|
||||
./gradlew :1.21.11:build # a single version
|
||||
./gradlew :1.21.11:runClient # launch a development client
|
||||
```
|
||||
|
||||
Building needs Java 21.
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions of all kinds:
|
||||
|
||||
- Code improvements
|
||||
- Bug fixes
|
||||
- UI enhancements
|
||||
- Cosmetic designs
|
||||
- Suggestions and feedback
|
||||
|
||||
If you're an artist or designer interested in helping with cosmetics or visual assets, we'd love to collaborate.
|
||||
|
||||
Most features belong in `common`, which every Minecraft version shares. For code that differs between versions, see the [version guide](VERSION_GUIDE.md): it covers how the versions share one copy of the code with Stonecutter, writing version-specific code, and adding a new Minecraft version. Every pull request is compiled for all versions automatically.
|
||||
|
||||
## Credits
|
||||
|
||||
- **Cloak Rendering:** `IIpho3nix` & Saturn
|
||||
- **Who Am I:** `FokshaWasTaken`
|
||||
- **Panorama:**
|
||||
https://www.planetminecraft.com/texture-pack/minecraft-cavesand-cliffs-part-ii-main-menu-panorama/
|
||||
|
||||
If any code or assets belong to you and you would like them removed, please open an issue.
|
||||
We aim to properly credit all contributors and respect original creators of their respective art.
|
||||
|
||||
## License
|
||||
|
||||
Saturn Client is free and open-source under the **Apache-2.0** license.
|
||||
|
||||
<p align="center">
|
||||
Made with ❤️ by the Saturn Client team
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# Version guide
|
||||
|
||||
How Saturn Client supports several Minecraft versions, and how to change or add one.
|
||||
|
||||
## Layout
|
||||
|
||||
Every supported Minecraft version (1.21.4 to 1.21.11) builds from one copy of the version-specific code with [Stonecutter](https://stonecutter.kikugie.dev/):
|
||||
|
||||
- `common/`: all version-independent code (mods, UI, cosmetics, the server client). It has no Minecraft dependency and reaches the game only through the interfaces in `org.saturnclient.common`.
|
||||
- `src/`: the version-specific code (providers, refs, mixins and the three mixin configs), one copy for every version. Where Minecraft's API differs between versions, the code goes behind version comments (see below).
|
||||
- `versions/<mc>/`: each version's `gradle.properties` (Minecraft, Yarn, Fabric Loader and Fabric API versions), plus its `build/` and `run/` folders. Each version is the Gradle project `:<mc>`.
|
||||
|
||||
`settings.gradle.kts` lists the versions in `stonecutterVersions`. The root `build.gradle.kts` builds each version, and `stonecutter.gradle.kts` configures the root project (shared repositories, the `common` wiring and `buildAll`). Loom's version is `loom_version` in the root `gradle.properties`.
|
||||
|
||||
## Version-specific code with Stonecutter
|
||||
|
||||
The code in `src/` is always plain Java for the **active** version (1.21.11, set in `stonecutter.gradle.kts`). Code for other versions sits in comments that Stonecutter swaps before building each version:
|
||||
|
||||
```java
|
||||
//? if >=1.21.9 {
|
||||
public void render(MatrixStack matrices, OrderedRenderCommandQueue queue, int light, PlayerEntityRenderState state, float limbAngle, float limbDistance) {
|
||||
//?} else
|
||||
//public void render(MatrixStack matrices, VertexConsumerProvider consumers, int light, PlayerEntityRenderState state, float limbAngle, float limbDistance) {
|
||||
```
|
||||
|
||||
A condition without braces covers the next line only. For several lines, use braces and wrap the inactive block in `/* … */`:
|
||||
|
||||
```java
|
||||
//? if >=1.21.11 {
|
||||
import net.minecraft.client.render.RenderLayers;
|
||||
import net.minecraft.client.render.RenderSetup;
|
||||
//?} else {
|
||||
/*import net.minecraft.client.render.RenderPhase.Textures;
|
||||
*///?}
|
||||
```
|
||||
|
||||
Stonecutter rewrites inactive code into its own layout when switching versions (a single inactive line becomes `//code`), so write it that way to begin with, or run a switch and a reset before committing.
|
||||
|
||||
When a newer version leaves something out that an older one implements, keep the older version's code behind a condition and mark the newer branch with a `TODO`, rather than dropping it for every version. The panorama in `SaturnScreenFabric.render` is an example. Search for `TODO` to find what newer versions still lack (tracked in #30), and remove the condition once a version gap is fixed.
|
||||
|
||||
A whole class that only exists for some versions (such as `MatrixStackMixin`, which only makes sense before 1.21.6's GUI rewrite) can keep an empty class body on the other versions. Before splitting a class like that, look for a hook that works on every version: nametags used to hook label rendering, which 1.21.9 changed, and now replace the name while the render state is built (`LivingEntityRendererMixin`), with no version condition at all. An empty mixin changes nothing, and it keeps the mixin configs the same for every version, since JSON can't hold version comments.
|
||||
|
||||
Inactive code sits inside a `/* … */` comment, so it can't contain `/* … */` comments itself: use `//` comments in code that's only active for some versions. For the same reason, don't nest multi-line version blocks; use `//?} else if <condition> {` for more than two version ranges, as in `PlayerEntityRendererMixin` and `SaturnRenderState`.
|
||||
|
||||
- `./gradlew "Set active project to <mc>"` (or the Stonecutter IntelliJ plugin) rewrites `src/` so that version's code is the uncommented one, for editing it in the IDE.
|
||||
- `./gradlew "Reset active project"` switches back to 1.21.11. **Run it before committing**, so `src/` is always committed with 1.21.11 active.
|
||||
- Each version's processed copy is compiled from `versions/<mc>/build/generated/stonecutter/`, which is also where to look when a non-active version fails to compile.
|
||||
|
||||
The `emotes` repository uses the same setup, and its `BentCubeRenderer` and `PlayerModelMixin` are small examples of version comments.
|
||||
|
||||
## Checking a change on every version
|
||||
|
||||
Since every version shares `src/`, a change applies to all of them at once. Code that uses the Minecraft API may still only compile on some, so build them all:
|
||||
|
||||
```sh
|
||||
./gradlew compileJava
|
||||
```
|
||||
|
||||
The **Check** workflow runs the same compile on every pull request, so a version a change doesn't compile on fails before merging. Mixins whose target changed fail at startup rather than at compile time, so launch the versions a change touches with `./gradlew :<mc>:runClient`.
|
||||
|
||||
For bigger changes, `./gradlew buildAll` builds every version's jar into `build/allJars/`.
|
||||
|
||||
## Adding a Minecraft version
|
||||
|
||||
1. Create `versions/<new>/gradle.properties` with `minecraft_version`, `yarn_mappings`, `loader_version` and `fabric_api_version` (copy 1.21.11's and change the values; they're listed at <https://fabricmc.net/develop>). Loom's version is shared, in the root `gradle.properties`.
|
||||
2. Add `<new>` to `stonecutterVersions` in `settings.gradle.kts`.
|
||||
3. Build it with `./gradlew :<new>:compileJava` and put what Minecraft changed behind version comments. Check the mixin configs in `src/main/resources/*.mixins.json` as well: a mixin whose target changed fails at startup, not at compile time, so launch it with `./gradlew :<new>:runClient`.
|
||||
4. The release workflow builds everything through `buildAll`, and "Publish to Modrinth" takes the game version from each jar's name, so neither needs changing.
|
||||
@@ -1,89 +0,0 @@
|
||||
plugins {
|
||||
id 'fabric-loom' version '1.9-SNAPSHOT'
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
version = project.mod_version
|
||||
group = project.maven_group
|
||||
|
||||
repositories {}
|
||||
|
||||
dependencies {
|
||||
minecraft "com.mojang:minecraft:${project.minecraft_version}"
|
||||
|
||||
// Switch back to Yarn
|
||||
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
|
||||
|
||||
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
|
||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
||||
|
||||
modApi "dev.felnull:special-model-loader:1.4.0+pre.1"
|
||||
include "dev.felnull:special-model-loader:1.4.0+pre.1"
|
||||
|
||||
include modImplementation("dev.kosmx.player-anim:player-animation-lib-fabric:${project.player_anim}")
|
||||
//You might want bendy-lib. playerAnimator will wrap it.
|
||||
// include modRuntimeOnly("io.github.kosmx.bendy-lib:bendy-lib-fabric:${project.bendylib_version}")
|
||||
}
|
||||
|
||||
|
||||
repositories {
|
||||
maven { url "https://maven.parchmentmc.org" }
|
||||
maven {
|
||||
name = "FelNull Maven"
|
||||
url = "https://maven.felnull.dev"
|
||||
}
|
||||
|
||||
maven {
|
||||
name "KosmX's maven"
|
||||
url 'https://maven.kosmx.dev/'
|
||||
}
|
||||
}
|
||||
|
||||
processResources {
|
||||
inputs.property "version", project.version
|
||||
inputs.property "minecraft_version", project.minecraft_version
|
||||
inputs.property "loader_version", project.loader_version
|
||||
filteringCharset "UTF-8"
|
||||
|
||||
filesMatching("fabric.mod.json") {
|
||||
expand "version": project.version,
|
||||
"minecraft_version": project.minecraft_version,
|
||||
"loader_version": project.loader_version
|
||||
}
|
||||
}
|
||||
|
||||
def targetJavaVersion = 21
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
it.options.encoding = "UTF-8"
|
||||
if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) {
|
||||
it.options.release.set(targetJavaVersion)
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
def javaVersion = JavaVersion.toVersion(targetJavaVersion)
|
||||
if (JavaVersion.current() < javaVersion) {
|
||||
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
|
||||
}
|
||||
archivesBaseName = project.archives_base_name
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
jar {
|
||||
from("LICENSE") {
|
||||
rename { "${it}_${project.archivesBaseName}"}
|
||||
}
|
||||
from("NOTICE") {
|
||||
rename { "${it}_${project.archivesBaseName}"}
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
from components.java
|
||||
}
|
||||
}
|
||||
|
||||
repositories {}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Builds one Minecraft version from the shared src/. Stonecutter runs this for each version in
|
||||
// stonecutterVersions (settings.gradle.kts), with that version's versions/<v>/gradle.properties.
|
||||
plugins {
|
||||
id("net.fabricmc.fabric-loom-remap")
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
val minecraftVersion = property("minecraft_version") as String
|
||||
val loaderVersion = property("loader_version") as String
|
||||
|
||||
version = "${property("mod_version")}+$minecraftVersion"
|
||||
group = property("maven_group") as String
|
||||
|
||||
base {
|
||||
archivesName = property("archives_base_name") as String
|
||||
}
|
||||
|
||||
dependencies {
|
||||
minecraft("com.mojang:minecraft:$minecraftVersion")
|
||||
mappings("net.fabricmc:yarn:${property("yarn_mappings")}:v2")
|
||||
modImplementation("net.fabricmc:fabric-loader:$loaderVersion")
|
||||
modImplementation("net.fabricmc.fabric-api:fabric-api:${property("fabric_api_version")}")
|
||||
|
||||
// Saturn's emote library (limb bending, Emotecraft emotes)
|
||||
include(modImplementation("org.saturnclient:emotes-fabric:${property("emotes_version")}+$minecraftVersion")!!)
|
||||
|
||||
implementation(project(":common"))
|
||||
|
||||
for (library in listOf(
|
||||
"dev.selimaj:session-java:0.2.0",
|
||||
"de.javagl:obj:0.4.0",
|
||||
"com.formdev:svgSalamander:1.1.4",
|
||||
"com.fasterxml.jackson.core:jackson-databind:2.17.0",
|
||||
"com.fasterxml.jackson.core:jackson-annotations:2.17.0",
|
||||
"com.fasterxml.jackson.core:jackson-core:2.17.0",
|
||||
)) {
|
||||
include(implementation(library)!!)
|
||||
}
|
||||
}
|
||||
|
||||
// Injects the version values into fabric.mod.json.
|
||||
tasks.processResources {
|
||||
val values = mapOf(
|
||||
"version" to project.version.toString(),
|
||||
"minecraft_version" to minecraftVersion,
|
||||
"loader_version" to loaderVersion,
|
||||
)
|
||||
inputs.properties(values)
|
||||
filteringCharset = "UTF-8"
|
||||
filesMatching("fabric.mod.json") { expand(values) }
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
options.release = 21
|
||||
}
|
||||
|
||||
java {
|
||||
// Loom attaches the sources jar to remapSourcesJar and build.
|
||||
withSourcesJar()
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
val archivesName = base.archivesName
|
||||
inputs.property("archivesName", archivesName)
|
||||
from("LICENSE") {
|
||||
rename { "${it}_${archivesName.get()}" }
|
||||
}
|
||||
from(project(":common").extensions.getByType<SourceSetContainer>()["main"].output)
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("mavenJava") {
|
||||
artifactId = property("archives_base_name") as String
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
||||
Submodule
+1
Submodule common added at b0d75af8c4
File diff suppressed because one or more lines are too long
+12
-10
@@ -1,13 +1,15 @@
|
||||
# Done to increase the memory available to gradle.
|
||||
org.gradle.jvmargs=-Xmx1G
|
||||
org.gradle.jvmargs=-Xmx3G
|
||||
org.gradle.parallel=true
|
||||
# Reuse task outputs (e.g. compiled classes) when inputs haven't changed.
|
||||
org.gradle.caching=true
|
||||
|
||||
minecraft_version=1.21.4
|
||||
yarn_mappings=1.21.4+build.7
|
||||
loader_version=0.16.10
|
||||
fabric_version=0.114.0+1.21.4
|
||||
# IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349
|
||||
org.gradle.configuration-cache=false
|
||||
|
||||
mod_version = 0.0.5-beta2
|
||||
maven_group = org.saturnclient
|
||||
archives_base_name = saturnclient
|
||||
mod_version=0.1.4-beta
|
||||
|
||||
player_anim = 2.0.3+1.21.4
|
||||
# Saturn's emote library (git.selimaj.dev/saturnclientmc/emotes)
|
||||
emotes_version=0.1.1
|
||||
|
||||
# Loom, for every Minecraft version (read in settings.gradle.kts).
|
||||
loom_version=1.17.21
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# 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.
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
# gradlew start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
@@ -29,7 +29,7 @@
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
# ksh gradlew
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
@@ -57,7 +57,7 @@
|
||||
# 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
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/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/.
|
||||
@@ -114,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -172,7 +171,6 @@ fi
|
||||
# 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" )
|
||||
|
||||
@@ -212,8 +210,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
Vendored
+52
-34
@@ -19,12 +19,39 @@
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem gradlew startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
@rem Catch executions from older scripts and ensure they exit cleanly.
|
||||
@rem This can be removed once we can be reasonably confident that few people
|
||||
@rem will be migrating directly to this new wrapper.
|
||||
goto afterSafetyNet
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
goto exitWithErrorLevel
|
||||
:afterSafetyNet
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@@ -45,13 +72,14 @@ 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
|
||||
1>&2 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.
|
||||
|
||||
goto fail
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto exitWithErrorLevel
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
@@ -59,36 +87,26 @@ 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
|
||||
1>&2 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.
|
||||
|
||||
goto fail
|
||||
"%COMSPEC%" /c exit 1
|
||||
goto exitWithErrorLevel
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel
|
||||
|
||||
: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
|
||||
@rem This label must not be changed. We rely on old scripts being able to jump to this point.
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 591 KiB |
@@ -1,9 +0,0 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven {
|
||||
name = 'Fabric'
|
||||
url = 'https://maven.fabricmc.net/'
|
||||
}
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven("https://maven.fabricmc.net/")
|
||||
maven("https://maven.kikugie.dev/releases")
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
// Loom's version for build.gradle.kts, which can't read properties in its plugins block.
|
||||
val loom_version: String by settings
|
||||
plugins {
|
||||
id("net.fabricmc.fabric-loom-remap") version loom_version
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.kikugie.stonecutter") version "0.9.8"
|
||||
}
|
||||
|
||||
rootProject.name = "saturn-client"
|
||||
|
||||
include("common")
|
||||
|
||||
// Versions built by Stonecutter from the shared src/ (see stonecutter.gradle.kts). Each one's
|
||||
// versions/<v>/gradle.properties holds its Minecraft, Yarn and Fabric API versions.
|
||||
val stonecutterVersions = listOf("1.21.4", "1.21.5", "1.21.6", "1.21.7", "1.21.8", "1.21.9", "1.21.10", "1.21.11")
|
||||
|
||||
stonecutter {
|
||||
create(rootProject) {
|
||||
versions(stonecutterVersions)
|
||||
vcsVersion = "1.21.11"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
package org.saturnclient.impl.cosmetics;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Quaternionf;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
import org.saturnclient.client.player.SaturnPlayer;
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.config.Config;
|
||||
import org.saturnclient.cosmetics.Cloaks;
|
||||
import org.saturnclient.emotes.core.Animation;
|
||||
import org.saturnclient.emotes.core.Bone;
|
||||
import org.saturnclient.emotes.core.bend.LimbBend;
|
||||
import org.saturnclient.emotes.core.bend.Vec3;
|
||||
import org.saturnclient.emotes.fabric.access.BendablePart;
|
||||
import org.saturnclient.impl.cosmetics.utils.ShaderUtils;
|
||||
import org.saturnclient.saturnclient.SaturnRenderState;
|
||||
|
||||
import net.minecraft.client.model.ModelPart;
|
||||
import net.minecraft.client.render.OverlayTexture;
|
||||
import net.minecraft.client.render.VertexConsumer;
|
||||
//? if >=1.21.9 {
|
||||
import net.minecraft.client.render.command.OrderedRenderCommandQueue;
|
||||
//?} else
|
||||
//import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.render.entity.equipment.EquipmentModel;
|
||||
import net.minecraft.client.render.entity.equipment.EquipmentModelLoader;
|
||||
import net.minecraft.client.render.entity.equipment.EquipmentModel.LayerType;
|
||||
import net.minecraft.client.render.entity.feature.FeatureRenderer;
|
||||
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
|
||||
import net.minecraft.client.render.entity.model.PlayerEntityModel;
|
||||
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.component.DataComponentTypes;
|
||||
import net.minecraft.component.type.EquippableComponent;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.item.equipment.EquipmentAsset;
|
||||
import net.minecraft.registry.RegistryKey;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
|
||||
/**
|
||||
* The cloak renderer
|
||||
* - **Efficient**: uses a image only consisting of the cloak texture, 16x8
|
||||
* whitespace.
|
||||
* - **Curving**: Curves when player velocity increases.
|
||||
* - **Wavey**: Makes the cloak feel like wind, when jump sprinting.
|
||||
*/
|
||||
public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
|
||||
private final EquipmentModelLoader equipmentModelLoader;
|
||||
private static final int PARTS = 24;
|
||||
private final float[] segmentValues = new float[PARTS];
|
||||
private static final int H_PARTS = 6;
|
||||
private float horizontalCurve = 0.0f;
|
||||
|
||||
private static final float TEX_W = 176f;
|
||||
private static final float TEX_H = 138f;
|
||||
|
||||
private static final class PxRect {
|
||||
final int x, y, w, h;
|
||||
|
||||
PxRect(int x, int y, int w, int h) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.w = w;
|
||||
this.h = h;
|
||||
}
|
||||
|
||||
float u1() {
|
||||
return x / TEX_W;
|
||||
}
|
||||
|
||||
float v1() {
|
||||
return y / TEX_H;
|
||||
}
|
||||
|
||||
float u2() {
|
||||
return (x + w) / TEX_W;
|
||||
}
|
||||
|
||||
float v2() {
|
||||
return (y + h) / TEX_H;
|
||||
}
|
||||
}
|
||||
|
||||
private static final PxRect FRONT_RECT = new PxRect(8, 8, 80, 128);
|
||||
private static final PxRect BACK_RECT = new PxRect(96, 8, 80, 128);
|
||||
private static final PxRect LEFT_EDGE_RECT = new PxRect(0, 8, 8, 128);
|
||||
private static final PxRect RIGHT_EDGE_RECT = new PxRect(88, 8, 8, 128);
|
||||
private static final PxRect TOP_RECT = new PxRect(8, 0, 80, 8);
|
||||
private static final PxRect BOTTOM_RECT = new PxRect(88, 0, 80, 8);
|
||||
|
||||
public CloakFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context,
|
||||
EquipmentModelLoader equipmentModelLoader) {
|
||||
super(context);
|
||||
this.equipmentModelLoader = equipmentModelLoader;
|
||||
Arrays.fill(segmentValues, 0.0f);
|
||||
}
|
||||
|
||||
private boolean hasCustomModelForLayer(ItemStack stack, EquipmentModel.LayerType layerType) {
|
||||
EquippableComponent equippableComponent = (EquippableComponent) stack
|
||||
.get(DataComponentTypes.EQUIPPABLE);
|
||||
if (equippableComponent != null && !equippableComponent.assetId().isEmpty()) {
|
||||
EquipmentModel equipmentModel = this.equipmentModelLoader
|
||||
.get((RegistryKey<EquipmentAsset>) equippableComponent.assetId().get());
|
||||
return !equipmentModel.getLayers(layerType).isEmpty();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void vertex(VertexConsumer vc, Matrix4f posMat, Matrix3f normalMat,
|
||||
Vec3d pos, int color,
|
||||
float u, float v, int overlay, int light,
|
||||
float nx, float ny, float nz) {
|
||||
|
||||
Vector4f p = new Vector4f((float) pos.x, (float) pos.y, (float) pos.z, 1.0f);
|
||||
p.mul(posMat);
|
||||
|
||||
Vector3f n = new Vector3f(nx, ny, nz);
|
||||
n.mul(normalMat);
|
||||
|
||||
vc.vertex(
|
||||
p.x(), p.y(), p.z(),
|
||||
color,
|
||||
u, v,
|
||||
overlay, light,
|
||||
n.x(), n.y(), n.z());
|
||||
}
|
||||
|
||||
private void renderCapeQuad(VertexConsumer vertexConsumer, MatrixStack.Entry entry,
|
||||
Vec3d bottomLeft, Vec3d bottomRight, Vec3d topRight, Vec3d topLeft,
|
||||
float u1, float v1, float u2, float v2, int light, int overlay,
|
||||
float normalX, float normalY, float normalZ, boolean flipWinding) {
|
||||
// Extract matrix (you must transform positions manually now)
|
||||
Matrix4f pos = entry.getPositionMatrix();
|
||||
Matrix3f normalMat = entry.getNormalMatrix();
|
||||
|
||||
int color = 0xFFFFFFFF; // white RGBA
|
||||
|
||||
if (!flipWinding) {
|
||||
// CCW
|
||||
vertex(vertexConsumer, pos, normalMat, bottomLeft, color, u1, v2, overlay, light, normalX, normalY,
|
||||
normalZ);
|
||||
vertex(vertexConsumer, pos, normalMat, bottomRight, color, u2, v2, overlay, light, normalX, normalY,
|
||||
normalZ);
|
||||
vertex(vertexConsumer, pos, normalMat, topRight, color, u2, v1, overlay, light, normalX, normalY, normalZ);
|
||||
vertex(vertexConsumer, pos, normalMat, topLeft, color, u1, v1, overlay, light, normalX, normalY, normalZ);
|
||||
} else {
|
||||
// CW
|
||||
vertex(vertexConsumer, pos, normalMat, topLeft, color, u1, v1, overlay, light, normalX, normalY, normalZ);
|
||||
vertex(vertexConsumer, pos, normalMat, topRight, color, u2, v1, overlay, light, normalX, normalY, normalZ);
|
||||
vertex(vertexConsumer, pos, normalMat, bottomRight, color, u2, v2, overlay, light, normalX, normalY,
|
||||
normalZ);
|
||||
vertex(vertexConsumer, pos, normalMat, bottomLeft, color, u1, v2, overlay, light, normalX, normalY,
|
||||
normalZ);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderCape(MatrixStack.Entry entry, VertexConsumer vertexConsumer,
|
||||
PlayerEntityRenderState playerState, int light, int overlay) {
|
||||
|
||||
float capeWidth = 10.0f / 16.0f;
|
||||
float capeHeight = 16.0f / 16.0f;
|
||||
float capeDepth = 1.0f / 16.0f;
|
||||
|
||||
final float frontU1 = FRONT_RECT.u1();
|
||||
final float frontU2 = FRONT_RECT.u2();
|
||||
final float frontV1 = FRONT_RECT.v1();
|
||||
final float frontV2 = FRONT_RECT.v2();
|
||||
final float frontPartV = (frontV2 - frontV1) / PARTS;
|
||||
|
||||
final float backU1 = BACK_RECT.u1();
|
||||
final float backU2 = BACK_RECT.u2();
|
||||
final float backV1 = BACK_RECT.v1();
|
||||
final float backV2 = BACK_RECT.v2();
|
||||
final float backPartV = (backV2 - backV1) / PARTS;
|
||||
|
||||
final float leftU1 = LEFT_EDGE_RECT.u1();
|
||||
final float leftU2 = LEFT_EDGE_RECT.u2();
|
||||
final float leftV1 = LEFT_EDGE_RECT.v1();
|
||||
final float leftV2 = LEFT_EDGE_RECT.v2();
|
||||
final float leftPartV = (leftV2 - leftV1) / PARTS;
|
||||
|
||||
final float rightU1 = RIGHT_EDGE_RECT.u1();
|
||||
final float rightU2 = RIGHT_EDGE_RECT.u2();
|
||||
final float rightV1 = RIGHT_EDGE_RECT.v1();
|
||||
final float rightV2 = RIGHT_EDGE_RECT.v2();
|
||||
final float rightPartV = (rightV2 - rightV1) / PARTS;
|
||||
|
||||
final float topU1 = TOP_RECT.u1();
|
||||
final float topU2 = TOP_RECT.u2();
|
||||
final float topV1 = TOP_RECT.v1();
|
||||
final float topV2 = TOP_RECT.v2();
|
||||
|
||||
final float bottomU1 = BOTTOM_RECT.u1();
|
||||
final float bottomU2 = BOTTOM_RECT.u2();
|
||||
final float bottomV1 = BOTTOM_RECT.v1();
|
||||
final float bottomV2 = BOTTOM_RECT.v2();
|
||||
|
||||
// =============================
|
||||
// 1. Spine
|
||||
// =============================
|
||||
|
||||
float[] spineY = new float[PARTS + 1];
|
||||
float[] spineZ = new float[PARTS + 1];
|
||||
|
||||
float partHeight = capeHeight / PARTS;
|
||||
|
||||
for (int i = 0; i < PARTS; i++) {
|
||||
float angle = (2.0f - segmentValues[i]) * ((float) Math.PI / 2f);
|
||||
float dirY = -(float) Math.cos(angle);
|
||||
float dirZ = (float) Math.sin(angle);
|
||||
|
||||
spineY[i + 1] = spineY[i] + dirY * partHeight;
|
||||
spineZ[i + 1] = spineZ[i] + dirZ * partHeight;
|
||||
}
|
||||
|
||||
// =============================
|
||||
// 2. Grid
|
||||
// =============================
|
||||
|
||||
Vec3d[][] inner = new Vec3d[H_PARTS + 1][PARTS + 1];
|
||||
Vec3d[][] outer = new Vec3d[H_PARTS + 1][PARTS + 1];
|
||||
|
||||
for (int x = 0; x <= H_PARTS; x++) {
|
||||
|
||||
float t = (float) x / H_PARTS;
|
||||
float xPos = -capeWidth / 2.0f + capeWidth * t;
|
||||
|
||||
float centered = t - 0.5f;
|
||||
float curve = (centered * centered) * 0.3f;
|
||||
float zCurve = (curve * horizontalCurve) / PARTS;
|
||||
|
||||
for (int i = 0; i <= PARTS; i++) {
|
||||
float y = spineY[i];
|
||||
float z = spineZ[i];
|
||||
|
||||
int idx = Math.min(i, PARTS - 1);
|
||||
// angle: 0 is horizontal (y-flat), PI/2 is vertical (z-flat)
|
||||
float angle = (2.0f - segmentValues[idx]) * ((float) Math.PI / 2f);
|
||||
|
||||
float cosA = (float) Math.cos(angle);
|
||||
float sinA = (float) Math.sin(angle);
|
||||
|
||||
// Calculate how much the horizontal curve shifts the vertex
|
||||
float currentCurve = zCurve * i;
|
||||
|
||||
// Rotate the curve offset so it aligns with the cape's tilt
|
||||
float curveY = sinA * currentCurve;
|
||||
float curveZ = cosA * currentCurve;
|
||||
|
||||
float thickY = sinA * capeDepth;
|
||||
float thickZ = cosA * capeDepth;
|
||||
|
||||
inner[x][i] = new Vec3d(xPos, y - curveY, z - curveZ);
|
||||
outer[x][i] = new Vec3d(xPos, (y - curveY) - thickY, (z - curveZ) - thickZ);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =============================
|
||||
// 3. Front + Back
|
||||
// =============================
|
||||
|
||||
for (int x = 0; x < H_PARTS; x++) {
|
||||
|
||||
float uStart = frontU1 + (frontU2 - frontU1) * (1.0f - ((float) x / H_PARTS));
|
||||
float uEnd = frontU1 + (frontU2 - frontU1) * (1.0f - ((float) (x + 1) / H_PARTS));
|
||||
|
||||
float buStart = backU1 + (backU2 - backU1) * ((float) x / H_PARTS);
|
||||
float buEnd = backU1 + (backU2 - backU1) * ((float) (x + 1) / H_PARTS);
|
||||
|
||||
for (int i = 0; i < PARTS; i++) {
|
||||
|
||||
float vStart = frontV1 + frontPartV * i;
|
||||
float vEnd = frontV1 + frontPartV * (i + 1);
|
||||
|
||||
float bvStart = backV1 + backPartV * i;
|
||||
float bvEnd = backV1 + backPartV * (i + 1);
|
||||
|
||||
Vec3d innerTL = inner[x + 1][i];
|
||||
Vec3d innerTR = inner[x][i];
|
||||
Vec3d innerBL = inner[x + 1][i + 1];
|
||||
Vec3d innerBR = inner[x][i + 1];
|
||||
|
||||
Vec3d outerTL = outer[x + 1][i];
|
||||
Vec3d outerTR = outer[x][i];
|
||||
Vec3d outerBL = outer[x + 1][i + 1];
|
||||
Vec3d outerBR = outer[x][i + 1];
|
||||
|
||||
// FRONT
|
||||
renderCapeQuad(vertexConsumer, entry,
|
||||
outerBR, outerBL, outerTL, outerTR,
|
||||
uStart, vStart, uEnd, vEnd,
|
||||
light, overlay, 0, 0, 1, false);
|
||||
|
||||
// BACK
|
||||
renderCapeQuad(vertexConsumer, entry,
|
||||
innerBL, innerBR, innerTR, innerTL,
|
||||
buEnd, bvStart, buStart, bvEnd,
|
||||
light, overlay, 0, 0, -1, false);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================
|
||||
// 4. LEFT SIDE (x = H_PARTS)
|
||||
// =============================
|
||||
|
||||
for (int i = 0; i < PARTS; i++) {
|
||||
|
||||
Vec3d innerTop = inner[H_PARTS][i];
|
||||
Vec3d innerBot = inner[H_PARTS][i + 1];
|
||||
Vec3d outerTop = outer[H_PARTS][i];
|
||||
Vec3d outerBot = outer[H_PARTS][i + 1];
|
||||
|
||||
float vStart = leftV1 + leftPartV * i;
|
||||
float vEnd = leftV1 + leftPartV * (i + 1);
|
||||
|
||||
renderCapeQuad(vertexConsumer, entry,
|
||||
innerTop, outerTop, outerBot, innerBot,
|
||||
leftU1, vEnd,
|
||||
leftU2, vStart,
|
||||
light, overlay, -1, 0, 0, false);
|
||||
}
|
||||
|
||||
// =============================
|
||||
// 5. RIGHT SIDE (x = 0)
|
||||
// =============================
|
||||
|
||||
for (int i = 0; i < PARTS; i++) {
|
||||
|
||||
Vec3d innerTop = inner[0][i];
|
||||
Vec3d innerBot = inner[0][i + 1];
|
||||
Vec3d outerTop = outer[0][i];
|
||||
Vec3d outerBot = outer[0][i + 1];
|
||||
|
||||
float vStart = rightV1 + rightPartV * i;
|
||||
float vEnd = rightV1 + rightPartV * (i + 1);
|
||||
|
||||
renderCapeQuad(vertexConsumer, entry,
|
||||
outerTop, innerTop, innerBot, outerBot,
|
||||
rightU1, vEnd,
|
||||
rightU2, vStart,
|
||||
light, overlay, 1, 0, 0, false);
|
||||
}
|
||||
|
||||
// =============================
|
||||
// 6. TOP (i = 0)
|
||||
// =============================
|
||||
|
||||
for (int x = 0; x < H_PARTS; x++) {
|
||||
|
||||
Vec3d outerL = outer[x][0];
|
||||
Vec3d outerR = outer[x + 1][0];
|
||||
Vec3d innerR = inner[x + 1][0];
|
||||
Vec3d innerL = inner[x][0];
|
||||
|
||||
float uStart = topU1 + (topU2 - topU1) * ((float) x / H_PARTS);
|
||||
float uEnd = topU1 + (topU2 - topU1) * ((float) (x + 1) / H_PARTS);
|
||||
|
||||
renderCapeQuad(vertexConsumer, entry,
|
||||
outerL, outerR, innerR, innerL,
|
||||
uStart, topV1, uEnd, topV2,
|
||||
light, overlay, 0, 1, 0, false);
|
||||
}
|
||||
|
||||
// =============================
|
||||
// 7. BOTTOM (i = PARTS)
|
||||
// =============================
|
||||
|
||||
for (int x = 0; x < H_PARTS; x++) {
|
||||
|
||||
Vec3d innerL = inner[x][PARTS];
|
||||
Vec3d innerR = inner[x + 1][PARTS];
|
||||
Vec3d outerR = outer[x + 1][PARTS];
|
||||
Vec3d outerL = outer[x][PARTS];
|
||||
|
||||
float uStart = bottomU1 + (bottomU2 - bottomU1) * ((float) x / H_PARTS);
|
||||
float uEnd = bottomU1 + (bottomU2 - bottomU1) * ((float) (x + 1) / H_PARTS);
|
||||
|
||||
renderCapeQuad(vertexConsumer, entry,
|
||||
innerL, innerR, outerR, outerL,
|
||||
uStart, bottomV1, uEnd, bottomV2,
|
||||
light, overlay, 0, -1, 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
/** A torso bend past this curves the back more than a flat cloak can follow. */
|
||||
private static final float MAX_CLOAK_BEND = (float) Math.toRadians(10);
|
||||
/** Whether each animation bends the torso past that at any point. */
|
||||
private static final Map<Animation, Boolean> BENDS_TORSO = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
/**
|
||||
* Whether the player is playing an emote that curves the torso (a twerk, the griddy). The cloak
|
||||
* would cut through the arched back, so it's hidden for the whole emote rather than only while
|
||||
* the back is curved, which would flicker.
|
||||
*/
|
||||
private static boolean playingTorsoBend(SaturnPlayer player) {
|
||||
return org.saturnclient.emotes.fabric.Emotes.playing(player.uuid)
|
||||
.map(playing -> BENDS_TORSO.computeIfAbsent(playing.animation(), CloakFeatureRenderer::bendsTorso))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
private static boolean bendsTorso(Animation animation) {
|
||||
for (float tick = 0; tick <= animation.length(); tick += 0.5f) {
|
||||
if (animation.sample(tick).get(Bone.BODY).filter(body -> Math.abs(body.bend()) > MAX_CLOAK_BEND).isPresent()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* An emote that bends the torso a little leans the chest from the hips, so the cloak hangs from
|
||||
* where the top of the back ends up, turned as much as the torso is there.
|
||||
*/
|
||||
private static void followBend(MatrixStack matrices, ModelPart body) {
|
||||
LimbBend bend = ((BendablePart) (Object) body).saturnEmotes$getBend();
|
||||
if (bend == null || bend.isStraight()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The top of the torso, in the body's own pixels
|
||||
float[] top = new float[3];
|
||||
bend.bendPoint(0, 0, 0, top);
|
||||
matrices.translate(top[0] / 16.0f, top[1] / 16.0f, top[2] / 16.0f);
|
||||
|
||||
Vec3 axis = bend.axis();
|
||||
matrices.multiply(new Quaternionf().rotationAxis(bend.angleAt(0), axis.x(), axis.y(), axis.z()));
|
||||
}
|
||||
|
||||
// Shift array left and insert new value at the start
|
||||
private void updateSegmentValues(float value) {
|
||||
// Shift all elements one position to the right (from end to start)
|
||||
for (int i = segmentValues.length - 1; i > 0; i--) {
|
||||
segmentValues[i] = segmentValues[i - 1];
|
||||
}
|
||||
|
||||
// Insert the new value at index 0
|
||||
segmentValues[0] = value;
|
||||
}
|
||||
|
||||
private long lastUpdate = 0;
|
||||
|
||||
@Override
|
||||
public void render(MatrixStack matrices,
|
||||
//? if >=1.21.9 {
|
||||
OrderedRenderCommandQueue output,
|
||||
//?} else
|
||||
//VertexConsumerProvider output,
|
||||
int light, PlayerEntityRenderState state, float limbAngle, float limbDistance) {
|
||||
if (state.invisible || !state.capeVisible
|
||||
//? if >=1.21.9 {
|
||||
|| state.skinTextures.cape() != null) {
|
||||
//?} else
|
||||
//|| state.skinTextures.capeTexture() != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
//? if >=1.21.8 {
|
||||
SaturnPlayer player = state.getData(SaturnRenderState.saturnDataKey);
|
||||
//?} else
|
||||
//SaturnPlayer player = SaturnPlayer.get(state.name);
|
||||
|
||||
if (player == null || playingTorsoBend(player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
IdentifierRef customCape = Cloaks.getCurrentCloakTexture(player.cloak);
|
||||
if (customCape == null
|
||||
|| this.hasCustomModelForLayer(state.equippedChestStack, LayerType.WINGS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int minBrightness = 7;
|
||||
int blockLight = (light >> 4) & 0xF;
|
||||
int skyLight = (light >> 20) & 0xF;
|
||||
blockLight = Math.max(blockLight, minBrightness);
|
||||
|
||||
skyLight = Math.max(skyLight, minBrightness);
|
||||
light = (skyLight << 20) | (blockLight << 4);
|
||||
|
||||
matrices.push();
|
||||
|
||||
// On the back: the whole player (which emotes like the backflip turn), then the body's position
|
||||
// and turn (which crouching and emotes move), then the chest's lean when an emote bends the torso
|
||||
PlayerEntityModel model = this.getContextModel();
|
||||
//? if >=1.21.5 {
|
||||
model.getRootPart().applyTransform(matrices);
|
||||
model.body.applyTransform(matrices);
|
||||
//?} else {
|
||||
/*model.getRootPart().rotate(matrices);
|
||||
model.body.rotate(matrices);
|
||||
*///?}
|
||||
followBend(matrices, model.body);
|
||||
|
||||
matrices.translate(0.0f, 0.0f, 0.12f);
|
||||
|
||||
if (this.hasCustomModelForLayer(state.equippedChestStack, LayerType.HUMANOID)) {
|
||||
matrices.translate(0.0F, -0.053125F, 0.06875F);
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastUpdate >= 20) {
|
||||
float velX = Math.min(0.8f, state.field_53537 / 108.0f);
|
||||
float rawVelY = state.field_53536;
|
||||
float velY = (rawVelY > 3.4f ? rawVelY : rawVelY < -3.4f ? rawVelY : 0.0f) / 16;
|
||||
|
||||
float value = Math.max(0.0f, Math.min(2.0f, state.isSwimming ? 0.0f : velX + velY));
|
||||
|
||||
if (Config.cloakPhysics.value) {
|
||||
updateSegmentValues(segmentValues[0] + (value - segmentValues[0]) * 0.3f);
|
||||
} else {
|
||||
Arrays.fill(segmentValues, segmentValues[0] + (value - segmentValues[0]) * 0.05f);
|
||||
}
|
||||
lastUpdate = now;
|
||||
}
|
||||
|
||||
final int l = light;
|
||||
|
||||
horizontalCurve = MathHelper.clamp(state.field_53537 / 150.0f, -1.0f, 1.0f);
|
||||
|
||||
//? if >=1.21.9 {
|
||||
output.submitCustom(matrices, ShaderUtils.getRenderLayer(customCape), (entry, vertexConsumer) -> {
|
||||
renderCape(entry, vertexConsumer, state, l, OverlayTexture.DEFAULT_UV);
|
||||
});
|
||||
//?} else
|
||||
//renderCape(matrices.peek(), output.getBuffer(ShaderUtils.getRenderLayer(customCape)), state, l, OverlayTexture.DEFAULT_UV);
|
||||
|
||||
matrices.pop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.saturnclient.impl.cosmetics;
|
||||
|
||||
import org.saturnclient.client.player.SaturnPlayer;
|
||||
import org.saturnclient.impl.cosmetics.obj.MtlLoader;
|
||||
import org.saturnclient.impl.cosmetics.obj.ObjModel;
|
||||
import org.saturnclient.saturnclient.SaturnRenderState;
|
||||
|
||||
import net.minecraft.client.render.OverlayTexture;
|
||||
//? if >=1.21.9 {
|
||||
import net.minecraft.client.render.command.OrderedRenderCommandQueue;
|
||||
//?} else
|
||||
//import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.render.entity.feature.FeatureRenderer;
|
||||
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
|
||||
import net.minecraft.client.render.entity.model.PlayerEntityModel;
|
||||
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
|
||||
public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
|
||||
public HatFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(MatrixStack matrices,
|
||||
//? if >=1.21.9 {
|
||||
OrderedRenderCommandQueue output,
|
||||
//?} else
|
||||
//VertexConsumerProvider output,
|
||||
int light, PlayerEntityRenderState state, float limbAngle, float limbDistance) {
|
||||
//? if >=1.21.8 {
|
||||
SaturnPlayer player = state.getData(SaturnRenderState.saturnDataKey);
|
||||
//?} else
|
||||
//SaturnPlayer player = SaturnPlayer.get(state.name);
|
||||
|
||||
if (state.invisible || player == null || player.hat.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
matrices.push();
|
||||
|
||||
// On the head, as vanilla places head items: the whole player (which emotes like the backflip
|
||||
// turn), then the head's position, turn and scale (which crouching and emotes move)
|
||||
PlayerEntityModel model = this.getContextModel();
|
||||
//? if >=1.21.5 {
|
||||
model.getRootPart().applyTransform(matrices);
|
||||
model.head.applyTransform(matrices);
|
||||
//?} else {
|
||||
/*model.getRootPart().rotate(matrices);
|
||||
model.head.rotate(matrices);
|
||||
*///?}
|
||||
|
||||
ObjModel.cosmetic("hat", player.hat).render(MtlLoader.cosmetic(player.hat),
|
||||
matrices, output, light,
|
||||
OverlayTexture.DEFAULT_UV);
|
||||
|
||||
matrices.pop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.saturnclient.impl.cosmetics.obj;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import de.javagl.obj.*;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public class MtlLoader {
|
||||
static Map<Identifier, Map<String, Mtl>> loadedMtls = new HashMap<>();
|
||||
|
||||
static Map<String, Mtl> loadMtl(Identifier mtlId) throws IOException {
|
||||
try (InputStream is = MinecraftClient.getInstance().getResourceManager()
|
||||
.getResource(mtlId).get().getInputStream()) {
|
||||
List<Mtl> mtlList = MtlReader.read(is);
|
||||
Map<String, Mtl> mtlMap = new java.util.LinkedHashMap<>();
|
||||
for (Mtl mtl : mtlList) {
|
||||
mtlMap.put(mtl.getName(), mtl);
|
||||
}
|
||||
return mtlMap;
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, Mtl> of(Identifier mtlId) {
|
||||
if (loadedMtls.containsKey(mtlId))
|
||||
return loadedMtls.get(mtlId);
|
||||
|
||||
try {
|
||||
Map<String, Mtl> mtl = loadMtl(mtlId);
|
||||
loadedMtls.put(mtlId, mtl);
|
||||
|
||||
return mtl;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static Map<String, Mtl> cosmetic(String cosmeticId) {
|
||||
int index = cosmeticId.indexOf('_');
|
||||
|
||||
String mtlId = (index == -1) ? "" : cosmeticId.substring(index + 1);
|
||||
|
||||
if (mtlId.isEmpty())
|
||||
mtlId = "white";
|
||||
|
||||
return of(Identifier.of("saturnclient", "models/cosmetic/materials/" + mtlId + ".mtl"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package org.saturnclient.impl.cosmetics.obj;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import de.javagl.obj.Mtl;
|
||||
import de.javagl.obj.Obj;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
//? if >=1.21.9 {
|
||||
import net.minecraft.client.render.command.OrderedRenderCommandQueue;
|
||||
//?} else
|
||||
//import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.resource.Resource;
|
||||
import net.minecraft.resource.ResourceManager;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public class ObjModel {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
public static final Map<Identifier, ObjModel> loadedObjModels = new HashMap<>();
|
||||
|
||||
public record Config(
|
||||
@JsonProperty("translate") Vec3d transform,
|
||||
@JsonProperty("scale") Vec3d scale,
|
||||
@JsonProperty("rotation") Vec3d rotation) {
|
||||
|
||||
public Config {
|
||||
transform = transform == null ? new Vec3d(0, 0, 0) : transform;
|
||||
scale = scale == null ? new Vec3d(1, 1, 1) : scale;
|
||||
rotation = rotation == null ? new Vec3d(0, 0, 0) : rotation;
|
||||
}
|
||||
|
||||
@JsonFormat(shape = JsonFormat.Shape.ARRAY)
|
||||
public record Vec3d(float x, float y, float z) {
|
||||
}
|
||||
}
|
||||
|
||||
Obj obj;
|
||||
Config config;
|
||||
|
||||
public static ObjModel cosmetic(String cosmeticKind, String id) {
|
||||
return of(Identifier.of("saturnclient", "models/cosmetic/" + cosmeticKind + "/" + id.split("_")[0] + "/model"));
|
||||
}
|
||||
|
||||
public static ObjModel of(Identifier obj) {
|
||||
if (loadedObjModels.containsKey(obj))
|
||||
return loadedObjModels.get(obj);
|
||||
|
||||
ObjModel o = new ObjModel(obj);
|
||||
loadedObjModels.put(obj, o);
|
||||
return o;
|
||||
}
|
||||
|
||||
ObjModel(Identifier identifier) {
|
||||
Identifier objId = Identifier.of(identifier.getNamespace(), identifier.getPath() + ".obj");
|
||||
Identifier jsonId = Identifier.of(identifier.getNamespace(), identifier.getPath() + ".json");
|
||||
|
||||
try {
|
||||
this.obj = ObjRenderer.loadObj(objId);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// Try loading JSON config if it exists
|
||||
ResourceManager resourceManager = MinecraftClient.getInstance().getResourceManager();
|
||||
Optional<Resource> resource = resourceManager.getResource(jsonId);
|
||||
|
||||
if (resource.isPresent()) {
|
||||
try (InputStream stream = resource.get().getInputStream()) {
|
||||
this.config = MAPPER.readValue(stream, Config.class);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void render(Map<String, Mtl> mtlMap, MatrixStack matrices,
|
||||
//? if >=1.21.9 {
|
||||
OrderedRenderCommandQueue output,
|
||||
//?} else
|
||||
//VertexConsumerProvider output,
|
||||
int light, int overlay) {
|
||||
matrices.push();
|
||||
|
||||
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(180.0f));
|
||||
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Y.rotationDegrees(180.0f));
|
||||
|
||||
if (this.config != null) {
|
||||
matrices.translate(config.transform.x, config.transform.y, config.transform.z);
|
||||
|
||||
matrices.scale(config.scale.x, config.scale.y, config.scale.z);
|
||||
|
||||
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(config.rotation.x));
|
||||
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Y.rotationDegrees(config.rotation.y));
|
||||
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Z.rotationDegrees(config.rotation.z));
|
||||
}
|
||||
|
||||
ObjRenderer.renderObj(this.obj, mtlMap, matrices, output, light, overlay);
|
||||
|
||||
matrices.pop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package org.saturnclient.impl.cosmetics.obj;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.impl.cosmetics.utils.ShaderUtils;
|
||||
|
||||
import de.javagl.obj.*;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
import net.minecraft.client.render.VertexConsumer;
|
||||
//? if >=1.21.9 {
|
||||
import net.minecraft.client.render.command.OrderedRenderCommandQueue;
|
||||
//?} else
|
||||
//import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public class ObjRenderer {
|
||||
|
||||
public static Obj loadObj(Identifier objId) throws IOException {
|
||||
try (InputStream is = MinecraftClient.getInstance().getResourceManager()
|
||||
.getResource(objId).get().getInputStream()) {
|
||||
return ObjReader.read(is);
|
||||
}
|
||||
}
|
||||
|
||||
public static void renderObj(Obj obj,
|
||||
Map<String, Mtl> mtlMap,
|
||||
MatrixStack matrices,
|
||||
//? if >=1.21.9 {
|
||||
OrderedRenderCommandQueue output,
|
||||
//?} else
|
||||
//VertexConsumerProvider output,
|
||||
int light,
|
||||
int overlay) {
|
||||
|
||||
Map<String, Obj> materialGroups = ObjSplitting.splitByMaterialGroups(obj);
|
||||
|
||||
for (Map.Entry<String, Obj> group : materialGroups.entrySet()) {
|
||||
String materialName = group.getKey();
|
||||
Obj groupObj = group.getValue();
|
||||
|
||||
Mtl mtl = mtlMap != null ? mtlMap.get(materialName) : null;
|
||||
|
||||
IdentifierRef texture = IdentifierRef.ofVanilla("textures/misc/white.png");
|
||||
|
||||
if (mtl != null) {
|
||||
String mapKd = mtl.getMapKd();
|
||||
if (mapKd != null && !mapKd.startsWith("#")) {
|
||||
texture = IdentifierRef.of(mapKd);
|
||||
}
|
||||
}
|
||||
|
||||
float r = 1f, g = 1f, b = 1f;
|
||||
if (mtl != null) {
|
||||
FloatTuple kd = mtl.getKd();
|
||||
if (kd != null) {
|
||||
r = kd.getX();
|
||||
g = kd.getY();
|
||||
b = kd.getZ();
|
||||
}
|
||||
}
|
||||
|
||||
RenderLayer layer = ShaderUtils.getRenderLayer(texture);
|
||||
|
||||
final float fr = r, fg = g, fb = b;
|
||||
final Obj finalObj = groupObj;
|
||||
|
||||
//? if >=1.21.9 {
|
||||
output.submitCustom(matrices, layer,
|
||||
(entry, consumer) -> drawGroup(finalObj, consumer, entry, light, overlay, fr, fg, fb));
|
||||
//?} else
|
||||
//drawGroup(finalObj, output.getBuffer(layer), matrices.peek(), light, overlay, fr, fg, fb);
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawGroup(Obj obj, VertexConsumer consumer, MatrixStack.Entry entry, int light, int overlay,
|
||||
float r, float g, float b) {
|
||||
int faces = obj.getNumFaces();
|
||||
|
||||
for (int f = 0; f < faces; f++) {
|
||||
ObjFace face = obj.getFace(f);
|
||||
int verts = face.getNumVertices();
|
||||
|
||||
for (int i = verts - 1; i >= 0; i--) {
|
||||
writeVertex(obj, face, i, consumer, entry, light, overlay, r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeVertex(Obj obj, ObjFace face, int index, VertexConsumer consumer,
|
||||
MatrixStack.Entry matrix, int light, int overlay,
|
||||
float r, float g, float b) {
|
||||
|
||||
FloatTuple pos = obj.getVertex(face.getVertexIndex(index));
|
||||
float x = pos.getX();
|
||||
float y = pos.getY();
|
||||
float z = pos.getZ();
|
||||
|
||||
float nx = 0f, ny = 1f, nz = 0f;
|
||||
int normalIndex = face.getNormalIndex(index);
|
||||
if (normalIndex >= 0) {
|
||||
FloatTuple n = obj.getNormal(normalIndex);
|
||||
nx = n.getX();
|
||||
ny = n.getY();
|
||||
nz = n.getZ();
|
||||
}
|
||||
|
||||
float u = 0f, v = 0f;
|
||||
int texIndex = face.getTexCoordIndex(index);
|
||||
if (texIndex >= 0) {
|
||||
FloatTuple tc = obj.getTexCoord(texIndex);
|
||||
u = tc.getX();
|
||||
v = 1f - tc.getY(); // Flip V: OBJ bottom-left origin -> Minecraft top-left
|
||||
}
|
||||
|
||||
consumer.vertex(matrix, x, y, z)
|
||||
.color(r, g, b, 1f)
|
||||
.texture(u, v)
|
||||
.overlay(overlay)
|
||||
.light(light)
|
||||
.normal(matrix, nx, ny, nz);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.saturnclient.impl.cosmetics.utils;
|
||||
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
|
||||
import net.minecraft.client.texture.NativeImage;
|
||||
import net.minecraft.client.texture.NativeImageBackedTexture;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.util.Map;
|
||||
|
||||
public class IdentifierUtils {
|
||||
|
||||
public static void registerBufferedImageTextureFast(IdentifierRef identifierRef, BufferedImage bufferedImage) {
|
||||
try {
|
||||
Identifier identifier = (Identifier) (Object) identifierRef;
|
||||
|
||||
int width = bufferedImage.getWidth();
|
||||
int height = bufferedImage.getHeight();
|
||||
|
||||
NativeImage nativeImage = new NativeImage(NativeImage.Format.RGBA, width, height, false);
|
||||
|
||||
if (bufferedImage.getType() == BufferedImage.TYPE_INT_ARGB) {
|
||||
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
|
||||
|
||||
int index = 0;
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
nativeImage.setColorArgb(x, y, pixels[index++]);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
nativeImage.setColorArgb(x, y, bufferedImage.getRGB(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SaturnClient.client.execute(() -> {
|
||||
//? if >=1.21.5 {
|
||||
NativeImageBackedTexture texture = new NativeImageBackedTexture(
|
||||
() -> identifier.toString(),
|
||||
nativeImage);
|
||||
//?} else
|
||||
//NativeImageBackedTexture texture = new NativeImageBackedTexture(nativeImage);
|
||||
|
||||
SaturnClient.client.getTextureManager().registerTexture(identifier, texture);
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
SaturnClient.LOGGER.error("Failed to register texture: {}", identifierRef, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void registerBufferedImageTexturesBulk(Map<IdentifierRef, BufferedImage> textures) {
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
textures.forEach(IdentifierUtils::registerBufferedImageTextureFast);
|
||||
|
||||
long end = System.currentTimeMillis();
|
||||
|
||||
SaturnClient.LOGGER.info(
|
||||
"Registered {} textures in {}ms",
|
||||
textures.size(),
|
||||
(end - start));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.saturnclient.impl.cosmetics.utils;
|
||||
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.cosmetics.utils.GifUtils;
|
||||
|
||||
//? if >=1.21.5
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
|
||||
//? if >=1.21.5
|
||||
import net.minecraft.client.gl.RenderPipelines;
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
//? if <1.21.5 {
|
||||
/*import net.minecraft.client.render.RenderPhase;
|
||||
import net.minecraft.client.render.VertexFormat.DrawMode;
|
||||
import net.minecraft.client.render.VertexFormats;
|
||||
*///?}
|
||||
//? if >=1.21.11 {
|
||||
import net.minecraft.client.render.RenderLayers;
|
||||
import net.minecraft.client.render.RenderSetup;
|
||||
import net.minecraft.client.render.block.entity.AbstractEndPortalBlockEntityRenderer;
|
||||
//?} else {
|
||||
/*import net.minecraft.client.render.RenderPhase.Textures;
|
||||
import net.minecraft.client.render.block.entity.EndPortalBlockEntityRenderer;
|
||||
*///?}
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public class ShaderUtils {
|
||||
// Render pipelines exist from 1.21.5; before that the layer names a shader program directly.
|
||||
//? if >=1.21.5 {
|
||||
public static final RenderPipeline END_GATEWAY_PIPELINE = RenderPipelines.register(
|
||||
RenderPipeline.builder(new RenderPipeline.Snippet[] {
|
||||
RenderPipelines.RENDERTYPE_END_PORTAL_SNIPPET })
|
||||
.withLocation("pipeline/end_gateway")
|
||||
.withShaderDefine("PORTAL_LAYERS", 16)
|
||||
.withCull(false)
|
||||
.build());
|
||||
//?}
|
||||
|
||||
//? if >=1.21.11 {
|
||||
public static final RenderLayer END_GATEWAY = RenderLayer.of("end_gateway",
|
||||
RenderSetup.builder(END_GATEWAY_PIPELINE)
|
||||
.texture("Sampler0", AbstractEndPortalBlockEntityRenderer.SKY_TEXTURE)
|
||||
.texture("Sampler1", AbstractEndPortalBlockEntityRenderer.PORTAL_TEXTURE).build());
|
||||
//?} else if >=1.21.6 {
|
||||
/*public static final RenderLayer END_GATEWAY = RenderLayer.of("end_gateway", 1536, false, false,
|
||||
END_GATEWAY_PIPELINE,
|
||||
RenderLayer.MultiPhaseParameters.builder()
|
||||
.texture(Textures.create().add(EndPortalBlockEntityRenderer.SKY_TEXTURE, false)
|
||||
.add(EndPortalBlockEntityRenderer.PORTAL_TEXTURE, false).build())
|
||||
.build(false));
|
||||
*///?} else if >=1.21.5 {
|
||||
/*public static final RenderLayer END_GATEWAY = RenderLayer.of("end_gateway", 1536, false, false,
|
||||
END_GATEWAY_PIPELINE,
|
||||
RenderLayer.MultiPhaseParameters.builder()
|
||||
.texture(Textures.create().add(EndPortalBlockEntityRenderer.SKY_TEXTURE, false, false)
|
||||
.add(EndPortalBlockEntityRenderer.PORTAL_TEXTURE, false, false).build())
|
||||
.build(false));
|
||||
*///?} else {
|
||||
/*public static final RenderLayer END_GATEWAY = RenderLayer.of(
|
||||
"end_gateway",
|
||||
VertexFormats.POSITION,
|
||||
DrawMode.QUADS,
|
||||
1536,
|
||||
false,
|
||||
false,
|
||||
RenderLayer.MultiPhaseParameters.builder()
|
||||
.program(RenderLayer.END_GATEWAY_PROGRAM)
|
||||
.texture(Textures.create()
|
||||
.add(EndPortalBlockEntityRenderer.SKY_TEXTURE, false, false)
|
||||
.add(EndPortalBlockEntityRenderer.PORTAL_TEXTURE, false, false)
|
||||
.build())
|
||||
.cull(RenderPhase.DISABLE_CULLING)
|
||||
.build(false));
|
||||
*///?}
|
||||
|
||||
public static RenderLayer getRenderLayer(IdentifierRef texture) {
|
||||
String path = texture.toString();
|
||||
if (path.endsWith(".gif")) {
|
||||
return entityAlpha((Identifier) (Object) GifUtils.get(texture));
|
||||
} else if (path.endsWith("end.png")) {
|
||||
return END_GATEWAY;
|
||||
} else {
|
||||
return entityAlpha((Identifier) (Object) texture);
|
||||
}
|
||||
}
|
||||
|
||||
private static RenderLayer entityAlpha(Identifier texture) {
|
||||
//? if >=1.21.11 {
|
||||
return RenderLayers.entityAlpha(texture);
|
||||
//?} else
|
||||
//return RenderLayer.getEntityAlpha(texture);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.saturnclient.impl.features.entity;
|
||||
|
||||
public interface CameraOverriddenEntity {
|
||||
float freelook$getCameraPitch();
|
||||
|
||||
float freelook$getCameraYaw();
|
||||
|
||||
void freelook$setCameraPitch(float pitch);
|
||||
|
||||
void freelook$setCameraYaw(float yaw);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.saturnclient.impl.features.entity;
|
||||
|
||||
import org.saturnclient.common.feature.EntityFeature;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.util.hit.EntityHitResult;
|
||||
import net.minecraft.util.hit.HitResult;
|
||||
import net.minecraft.entity.LivingEntity;
|
||||
|
||||
/**
|
||||
* Fabric implementation of {@link EntityFeature}.
|
||||
*
|
||||
* Crosshair targeting is resolved via {@link MinecraftClient#crosshairTarget}.
|
||||
* Entity state for nametags is built by LivingEntityRendererMixin, which
|
||||
* writes health into the render state and then constructs a
|
||||
* {@link RenderStateEntityState} from that snapshot for NametagsMod.
|
||||
*/
|
||||
public class EntityFeatureImpl implements EntityFeature {
|
||||
|
||||
private final MinecraftClient mc;
|
||||
|
||||
public EntityFeatureImpl(MinecraftClient mc) {
|
||||
this.mc = mc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingLivingEntity() {
|
||||
HitResult target = mc.crosshairTarget;
|
||||
if (target == null || target.getType() != HitResult.Type.ENTITY)
|
||||
return false;
|
||||
Entity hit = ((EntityHitResult) target).getEntity();
|
||||
return hit instanceof LivingEntity;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Render-state adapter
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Wraps a {@link HealthRenderState} (already injected into the
|
||||
* Minecraft render-state object) as a platform-neutral
|
||||
* {@link EntityState} for use by {@link NametagsFeature}.
|
||||
*
|
||||
* This is constructed in LivingEntityRendererMixin, where both the
|
||||
* render-state mixin interface and the entity name are available.
|
||||
*/
|
||||
public static final class RenderStateEntityState implements EntityState {
|
||||
|
||||
private final String customName;
|
||||
private final HealthRenderState hrs;
|
||||
|
||||
public RenderStateEntityState(String customName, HealthRenderState hrs) {
|
||||
this.customName = customName;
|
||||
this.hrs = hrs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCustomName() {
|
||||
return customName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getHealth() {
|
||||
return hrs.saturn$getHealth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getMaxHealth() {
|
||||
return hrs.saturn$getMaxHealth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityType getEntityType() {
|
||||
return hrs.saturn$getEntityType();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.saturnclient.impl.features.entity;
|
||||
|
||||
import org.saturnclient.common.feature.EntityFeature;
|
||||
|
||||
/**
|
||||
* Injected interface on
|
||||
* {@link net.minecraft.client.render.entity.state.LivingEntityRenderState}.
|
||||
*
|
||||
* Carries the entity's health snapshot and classification into the render
|
||||
* render state, so NametagsMod's replacement nametag (applied in
|
||||
* LivingEntityRendererMixin) can use them without touching the live entity again.
|
||||
*/
|
||||
public interface HealthRenderState {
|
||||
|
||||
float saturn$getHealth();
|
||||
|
||||
float saturn$getMaxHealth();
|
||||
|
||||
void saturn$setHealth(float health, float maxHealth);
|
||||
|
||||
EntityFeature.EntityType saturn$getEntityType();
|
||||
|
||||
void saturn$setEntityType(EntityFeature.EntityType type);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.saturnclient.impl.features.mixins.entity;
|
||||
|
||||
import org.saturnclient.impl.features.entity.CameraOverriddenEntity;
|
||||
import org.saturnclient.mod.mods.FreelookMod;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import net.minecraft.client.network.ClientPlayerEntity;
|
||||
import net.minecraft.client.render.Camera;
|
||||
import net.minecraft.entity.Entity;
|
||||
//? if >=1.21.11 {
|
||||
import net.minecraft.world.World;
|
||||
//?} else
|
||||
//import net.minecraft.world.BlockView;
|
||||
|
||||
@Mixin(Camera.class)
|
||||
public abstract class CameraMixin {
|
||||
@Unique
|
||||
boolean firstTime = true;
|
||||
|
||||
@Shadow
|
||||
protected abstract void setRotation(float yaw, float pitch);
|
||||
|
||||
@Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/Camera;setRotation(FF)V", ordinal = 1, shift = At.Shift.AFTER))
|
||||
public void lockRotation(
|
||||
//? if >=1.21.11 {
|
||||
World focusedBlock,
|
||||
//?} else
|
||||
//BlockView focusedBlock,
|
||||
Entity cameraEntity,
|
||||
boolean isThirdPerson,
|
||||
boolean isFrontFacing,
|
||||
float tickDelta,
|
||||
CallbackInfo ci) {
|
||||
if (FreelookMod.isFreeLooking() && cameraEntity instanceof ClientPlayerEntity) {
|
||||
CameraOverriddenEntity cameraOverriddenEntity = (CameraOverriddenEntity) cameraEntity;
|
||||
|
||||
if (firstTime && SaturnClient.client.player != null) {
|
||||
cameraOverriddenEntity.freelook$setCameraPitch(SaturnClient.client.player.getPitch());
|
||||
cameraOverriddenEntity.freelook$setCameraYaw(SaturnClient.client.player.getYaw());
|
||||
firstTime = false;
|
||||
}
|
||||
this.setRotation(cameraOverriddenEntity.freelook$getCameraYaw(),
|
||||
cameraOverriddenEntity.freelook$getCameraPitch());
|
||||
|
||||
}
|
||||
if (!FreelookMod.isFreeLooking() && cameraEntity instanceof ClientPlayerEntity) {
|
||||
firstTime = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.saturnclient.impl.features.mixins.entity;
|
||||
|
||||
import org.saturnclient.impl.features.entity.CameraOverriddenEntity;
|
||||
import org.saturnclient.mod.mods.FreelookMod;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import net.minecraft.client.network.ClientPlayerEntity;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
|
||||
@Mixin(Entity.class)
|
||||
public class EntityMixin implements CameraOverriddenEntity {
|
||||
@Unique
|
||||
private float cameraPitch;
|
||||
|
||||
@Unique
|
||||
private float cameraYaw;
|
||||
|
||||
@Inject(method = "changeLookDirection", at = @At("HEAD"), cancellable = true)
|
||||
public void changeCameraLookDirection(double xDelta, double yDelta, CallbackInfo ci) {
|
||||
// noinspection ConstantValue// IntelliJ is incorrect here, this code block is
|
||||
// reachable
|
||||
if (FreelookMod.isFreeLooking() && (Object) this instanceof ClientPlayerEntity) {
|
||||
double pitchDelta = (yDelta * 0.15);
|
||||
double yawDelta = (xDelta * 0.15);
|
||||
|
||||
this.cameraPitch = MathHelper.clamp(this.cameraPitch + (float) pitchDelta, -90.0f, 90.0f);
|
||||
this.cameraYaw += (float) yawDelta;
|
||||
|
||||
ci.cancel();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Unique
|
||||
public float freelook$getCameraPitch() {
|
||||
return this.cameraPitch;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Unique
|
||||
public float freelook$getCameraYaw() {
|
||||
return this.cameraYaw;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Unique
|
||||
public void freelook$setCameraPitch(float pitch) {
|
||||
this.cameraPitch = pitch;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Unique
|
||||
public void freelook$setCameraYaw(float yaw) {
|
||||
this.cameraYaw = yaw;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package org.saturnclient.impl.features.mixins.render;
|
||||
|
||||
//? if >=1.21.6 {
|
||||
import net.minecraft.client.render.fog.FogRenderer;
|
||||
//?} else
|
||||
//import net.minecraft.client.render.BackgroundRenderer;
|
||||
|
||||
import org.saturnclient.mod.mods.NoFogMod;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||
|
||||
//? if >=1.21.6 {
|
||||
@Mixin(FogRenderer.class)
|
||||
//?} else
|
||||
//@Mixin(BackgroundRenderer.class)
|
||||
public class BackgroundRendererMixin {
|
||||
// fog
|
||||
@ModifyVariable(method = "applyFog", at = @At("HEAD"), ordinal = 0, argsOnly = true)
|
||||
private static float modifyFogStart(float viewDistance) {
|
||||
if (NoFogMod.isActive()) {
|
||||
return viewDistance * 2.0f;
|
||||
}
|
||||
|
||||
return viewDistance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.saturnclient.impl.features.mixins.render;
|
||||
|
||||
//? if >=1.21.6
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
//? if >=1.21.6
|
||||
import net.minecraft.client.option.GameOptions;
|
||||
import net.minecraft.client.render.GameRenderer;
|
||||
|
||||
//? if >=1.21.6
|
||||
import org.saturnclient.impl.ui.SaturnScreenFabric;
|
||||
import org.saturnclient.mod.mods.FreelookMod;
|
||||
import org.saturnclient.mod.mods.ZoomMod;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
//? if >=1.21.6
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/**
|
||||
* Overrides the game's field-of-view when {@link ZoomMod} is active.
|
||||
*
|
||||
* The old code checked both {@code Zoom.isZooming} and
|
||||
* {@code Zoom.shouldZoom()} separately. The refactored
|
||||
* {@link ZoomMod#shouldZoom()} already ORs both conditions
|
||||
* ({@code enabled.value && isZooming}), so only one check is needed.
|
||||
*
|
||||
* No {@link org.saturnclient.common.module.ModProvider} is required
|
||||
* here because zooming is a pure render-time override with no per-tick
|
||||
* engine queries.
|
||||
*/
|
||||
@Mixin(GameRenderer.class)
|
||||
public class GameRendererMixin {
|
||||
|
||||
/**
|
||||
* After the vanilla FOV has been computed, divide it by the zoom
|
||||
* level to produce the narrowed field-of-view.
|
||||
*/
|
||||
@Inject(method = "getFov", at = @At("RETURN"), cancellable = true)
|
||||
private void onGetFov(CallbackInfoReturnable<Float> cir) {
|
||||
if (ZoomMod.shouldZoom()) {
|
||||
float zoomedFov = cir.getReturnValue() / ZoomMod.getZoomLevel();
|
||||
cir.setReturnValue(zoomedFov);
|
||||
} else if (FreelookMod.shouldZoomOut()) {
|
||||
float zoomedFov = cir.getReturnValue() / FreelookMod.getZoomLevel();
|
||||
cir.setReturnValue(zoomedFov);
|
||||
}
|
||||
}
|
||||
|
||||
//? if >=1.21.6 {
|
||||
// From 1.21.6 the menu blur's strength is the vanilla setting, read here every frame. While a
|
||||
// Saturn screen is open, it's Saturn's background blur instead, faded in with the screen.
|
||||
@Redirect(method = "render", at = @At(value = "INVOKE",
|
||||
target = "Lnet/minecraft/client/option/GameOptions;getMenuBackgroundBlurrinessValue()I"))
|
||||
private int useSaturnBlur(GameOptions options) {
|
||||
if (MinecraftClient.getInstance().currentScreen instanceof SaturnScreenFabric saturnScreen) {
|
||||
return saturnScreen.screen.getBackgroundBlur();
|
||||
}
|
||||
|
||||
return options.getMenuBackgroundBlurrinessValue();
|
||||
}
|
||||
//?}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package org.saturnclient.impl.features.mixins.render;
|
||||
|
||||
// Code approach adapted from Gamma Utils: https://github.com/Sjouwer/gamma-utils
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import net.minecraft.client.render.LightmapTextureManager;
|
||||
|
||||
import org.saturnclient.mod.mods.FullbrightMod;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
/**
|
||||
* Overrides the lightmap's gamma computation when
|
||||
* {@link FullbrightMod} is active, replacing the vanilla
|
||||
* brightness with a user-configured value.
|
||||
*
|
||||
* The old import was {@code org.saturnclient.feature.features.Fullbright};
|
||||
* it is now {@link FullbrightMod}. The injection target and
|
||||
* strategy are unchanged.
|
||||
*/
|
||||
@Mixin(LightmapTextureManager.class)
|
||||
public class LightmapTextureManagerMixin {
|
||||
|
||||
/**
|
||||
* If fullbright is active, substitute the feature's brightness
|
||||
* value for whatever vanilla calculated.
|
||||
*/
|
||||
@ModifyExpressionValue(method = "update", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F", ordinal = 0))
|
||||
private float modifyGamma(float original) {
|
||||
if (FullbrightMod.shouldOverrideBrightness()) {
|
||||
return FullbrightMod.getBrightnessValue();
|
||||
}
|
||||
return original;
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package org.saturnclient.impl.features.mixins.render;
|
||||
|
||||
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
|
||||
|
||||
import org.saturnclient.common.feature.EntityFeature;
|
||||
import org.saturnclient.impl.features.entity.HealthRenderState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
|
||||
/**
|
||||
* Implements {@link HealthRenderState} on
|
||||
* {@link LivingEntityRenderState}, injecting health and entity-type
|
||||
* storage into every living entity's render-state snapshot.
|
||||
*
|
||||
* Previously imported {@code NametagsInterface.EntityType}; the enum
|
||||
* now lives in {@link EntityFeature.EntityType} which is the single
|
||||
* canonical definition shared by the mixin layer and the feature layer.
|
||||
*
|
||||
* The stored values are written by
|
||||
* {@link LivingEntityRendererMixin#saturn$captureHealth} each time a
|
||||
* render state is extracted from a live entity, and read there right away for
|
||||
* NametagsMod's replacement nametag.
|
||||
*/
|
||||
@Mixin(LivingEntityRenderState.class)
|
||||
public class LivingEntityRenderStateMixin implements HealthRenderState {
|
||||
|
||||
@Unique
|
||||
private float saturn$health = 0f;
|
||||
@Unique
|
||||
private float saturn$maxHealth = 1f;
|
||||
@Unique
|
||||
private EntityFeature.EntityType saturn$entityType = EntityFeature.EntityType.OTHER;
|
||||
|
||||
@Override
|
||||
public float saturn$getHealth() {
|
||||
return saturn$health;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float saturn$getMaxHealth() {
|
||||
return saturn$maxHealth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saturn$setHealth(float health, float maxHealth) {
|
||||
this.saturn$health = health;
|
||||
this.saturn$maxHealth = maxHealth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityFeature.EntityType saturn$getEntityType() {
|
||||
return saturn$entityType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saturn$setEntityType(EntityFeature.EntityType type) {
|
||||
saturn$entityType = type;
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package org.saturnclient.impl.features.mixins.render;
|
||||
|
||||
import net.minecraft.client.render.entity.LivingEntityRenderer;
|
||||
import net.minecraft.client.render.entity.state.LivingEntityRenderState;
|
||||
import net.minecraft.entity.LivingEntity;
|
||||
import net.minecraft.entity.mob.HostileEntity;
|
||||
import net.minecraft.entity.passive.PassiveEntity;
|
||||
import net.minecraft.entity.player.PlayerEntity;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
import org.saturnclient.common.feature.EntityFeature;
|
||||
import org.saturnclient.impl.features.entity.EntityFeatureImpl;
|
||||
import org.saturnclient.impl.features.entity.HealthRenderState;
|
||||
import org.saturnclient.mod.mods.NametagsMod;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/**
|
||||
* Captures each living entity's health and type into the render state during
|
||||
* {@code updateRenderState}, and applies {@link NametagsMod}'s replacement nametag to the state's
|
||||
* display name.
|
||||
*
|
||||
* Replacing the name here, while the render state is built, works the same on every version. Hooking
|
||||
* label rendering instead doesn't: 1.21.9 moved it to the render command queue. Player role icons are
|
||||
* added after this, in PlayerEntityRendererMixin.
|
||||
*/
|
||||
@Mixin(LivingEntityRenderer.class)
|
||||
public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extends LivingEntityRenderState> {
|
||||
|
||||
@Inject(method = "updateRenderState", at = @At("TAIL"))
|
||||
private void saturn$captureHealth(T entity, S state, float tickDelta, CallbackInfo ci) {
|
||||
if (!(state instanceof HealthRenderState hrs))
|
||||
return;
|
||||
|
||||
hrs.saturn$setHealth(entity.getHealth(), entity.getMaxHealth());
|
||||
|
||||
// Classify entity type for the nametag feature's filtering logic
|
||||
EntityFeature.EntityType type;
|
||||
if (entity instanceof PlayerEntity)
|
||||
type = EntityFeature.EntityType.PLAYER;
|
||||
else if (entity instanceof HostileEntity)
|
||||
type = EntityFeature.EntityType.HOSTILE;
|
||||
else if (entity instanceof PassiveEntity)
|
||||
type = EntityFeature.EntityType.PASSIVE;
|
||||
else
|
||||
type = EntityFeature.EntityType.OTHER;
|
||||
|
||||
hrs.saturn$setEntityType(type);
|
||||
|
||||
// Only entities that already show a label get a replacement one, as when this hooked label
|
||||
// rendering.
|
||||
if (NametagsMod.shouldReplaceName() && state.displayName != null) {
|
||||
String replacement = NametagsMod.getNametagString(
|
||||
new EntityFeatureImpl.RenderStateEntityState(state.displayName.getString(), hrs));
|
||||
if (replacement != null) {
|
||||
state.displayName = Text.literal(replacement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.saturnclient.impl.features.mixins.render;
|
||||
|
||||
import net.minecraft.client.font.TextRenderer;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
import net.minecraft.client.gui.hud.InGameHud;
|
||||
import net.minecraft.client.render.RenderTickCounter;
|
||||
|
||||
import org.saturnclient.impl.ui.RenderScopeImpl;
|
||||
import org.saturnclient.impl.ui.SaturnScreenFabric;
|
||||
import org.saturnclient.mod.HudMod;
|
||||
import org.saturnclient.mod.Mod;
|
||||
import org.saturnclient.mod.ModLayout;
|
||||
import org.saturnclient.mod.ModManager;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.screens.HudEditor;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/**
|
||||
* Drives per-frame rendering for all registered features.
|
||||
*
|
||||
* This mixin is intentionally kept thin: it creates a {@link RenderScope},
|
||||
* iterates the enabled feature list from {@link ModManager}, and
|
||||
* dispatches either {@link HudMod#renderHud} (for HUD elements) or
|
||||
* {@link Mod#render} (for world-overlay features like Crosshair).
|
||||
*
|
||||
* No feature-specific logic lives here — all decisions about what to
|
||||
* draw are made inside each feature class.
|
||||
*
|
||||
* Changes from the original:
|
||||
* - Package imports updated to the refactored feature names (no
|
||||
* more {@code feature.features.*}).
|
||||
* - The HUD-editor screen check still uses the existing
|
||||
* {@link SaturnScreenFabric} / {@link HudEditor} pair; that is
|
||||
* unchanged infrastructure.
|
||||
*/
|
||||
@Mixin(InGameHud.class)
|
||||
public class RenderMixin {
|
||||
|
||||
@Inject(method = "renderChat", at = @At("TAIL"))
|
||||
public void renderMods(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) {
|
||||
TextRenderer textRenderer = SaturnClient.client.textRenderer;
|
||||
|
||||
// Skip rendering while the HUD editor is open so the editor
|
||||
// can draw its own preview layout without double-drawing.
|
||||
if (textRenderer == null)
|
||||
return;
|
||||
if (SaturnClient.client.currentScreen instanceof SaturnScreenFabric f
|
||||
&& f.screen instanceof HudEditor)
|
||||
return;
|
||||
|
||||
RenderScope scope = new RenderScopeImpl(context);
|
||||
|
||||
for (Mod feature : ModManager.ENABLED_MODS) {
|
||||
if (!feature.isEnabled())
|
||||
continue;
|
||||
|
||||
if (feature instanceof HudMod hud) {
|
||||
ModLayout dim = hud.getDimensions();
|
||||
|
||||
scope.getMatrixStack().push();
|
||||
scope.getMatrixStack().translate(dim.x.value, (float) dim.y.value);
|
||||
scope.getMatrixStack().scale(dim.scale.value, dim.scale.value);
|
||||
|
||||
if (dim.renderBackground) {
|
||||
scope.drawRoundedRectangle(0, 0, dim.width, dim.height,
|
||||
dim.radius.value, dim.bgColor.value);
|
||||
}
|
||||
|
||||
hud.renderHud(scope);
|
||||
scope.getMatrixStack().pop();
|
||||
|
||||
} else {
|
||||
// Non-HUD features (e.g. CrosshairFeature) use the plain render hook.
|
||||
feature.render(scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.saturnclient.impl.features.mixins.world;
|
||||
|
||||
import net.minecraft.client.network.ClientPlayNetworkHandler;
|
||||
import net.minecraft.network.packet.s2c.play.WorldTimeUpdateS2CPacket;
|
||||
|
||||
import org.saturnclient.mod.mods.TpsMod;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/**
|
||||
* Feeds world-time packets into {@link TpsMod}'s TPS estimator.
|
||||
*
|
||||
* This is the only place in the entire codebase that reads a network
|
||||
* packet and passes data to a feature. The feature's
|
||||
* {@link TpsMod#onTimePacket(long)} method handles all averaging
|
||||
* logic; the mixin is intentionally reduced to a single forwarding call.
|
||||
*
|
||||
* The only change from the original is renaming the import from
|
||||
* {@code feature.features.Tps} to {@link TpsMod}.
|
||||
*/
|
||||
@Mixin(ClientPlayNetworkHandler.class)
|
||||
public class TpsMixin {
|
||||
|
||||
@Inject(method = "onWorldTimeUpdate", at = @At("HEAD"))
|
||||
private void onWorldTimeUpdate(WorldTimeUpdateS2CPacket packet, CallbackInfo ci) {
|
||||
TpsMod.onTimePacket(packet.time());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.saturnclient.impl.features.network;
|
||||
|
||||
import org.saturnclient.common.feature.NetworkFeature;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.network.PlayerListEntry;
|
||||
|
||||
/**
|
||||
* Fabric implementation of {@link NetworkFeature}.
|
||||
*
|
||||
* Connection presence is checked via the network handler.
|
||||
* Ping is read from the player's {@link PlayerListEntry} which the
|
||||
* server populates; returns {@code -1} when unavailable.
|
||||
*/
|
||||
public class NetworkFeatureImpl implements NetworkFeature {
|
||||
|
||||
private final MinecraftClient mc;
|
||||
|
||||
public NetworkFeatureImpl(MinecraftClient mc) {
|
||||
this.mc = mc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNetwork() {
|
||||
return mc.getNetworkHandler() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPing() {
|
||||
if (mc.player == null || mc.getNetworkHandler() == null)
|
||||
return -1;
|
||||
|
||||
PlayerListEntry entry = mc.getNetworkHandler().getPlayerListEntry(mc.player.getUuid());
|
||||
|
||||
return entry != null ? entry.getLatency() : -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package org.saturnclient.impl.features.player;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.network.ClientPlayerEntity;
|
||||
import net.minecraft.entity.EquipmentSlot;
|
||||
import net.minecraft.entity.effect.StatusEffectInstance;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.item.Items;
|
||||
import net.minecraft.registry.Registries;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
import org.saturnclient.common.feature.PlayerFeature;
|
||||
import org.saturnclient.common.ref.game.EffectRef;
|
||||
import org.saturnclient.common.ref.game.ItemStackRef;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Fabric implementation of {@link PlayerFeature}.
|
||||
*
|
||||
* All access to {@link MinecraftClient} and {@link ClientPlayerEntity}
|
||||
* is encapsulated here. Features only ever see the {@link PlayerFeature}
|
||||
* interface; they never import Minecraft classes directly.
|
||||
*
|
||||
* Every method guards against a missing player and returns a safe
|
||||
* default so features are free of null checks.
|
||||
*/
|
||||
public class PlayerFeatureImpl implements PlayerFeature {
|
||||
|
||||
private final MinecraftClient mc;
|
||||
|
||||
public PlayerFeatureImpl(MinecraftClient mc) {
|
||||
this.mc = mc;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Internal helper — never returns null
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/** Returns the local player, or {@code null} if one is not loaded. */
|
||||
private ClientPlayerEntity player() {
|
||||
return mc.player;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Presence
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public boolean hasPlayer() {
|
||||
return mc.player != null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Position
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public int getX() {
|
||||
return hasPlayer() ? (int) player().getX() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getY() {
|
||||
return hasPlayer() ? (int) player().getY() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getZ() {
|
||||
return hasPlayer() ? (int) player().getZ() : 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Input / movement
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public boolean isForwardPressed() {
|
||||
return mc.options.forwardKey.isPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBackPressed() {
|
||||
return mc.options.backKey.isPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLeftPressed() {
|
||||
return mc.options.leftKey.isPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRightPressed() {
|
||||
return mc.options.rightKey.isPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isJumpPressed() {
|
||||
return mc.options.jumpKey.isPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAttackPressed() {
|
||||
return mc.options.attackKey.isPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUsePressed() {
|
||||
return mc.options.useKey.isPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSneaking() {
|
||||
return hasPlayer() && player().isSneaking();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUsingItem() {
|
||||
return hasPlayer() && player().isUsingItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasHorizontalCollision() {
|
||||
return hasPlayer() && player().horizontalCollision;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnGround() {
|
||||
return hasPlayer() && player().isOnGround();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSprinting(boolean sprinting) {
|
||||
if (hasPlayer())
|
||||
player().setSprinting(sprinting);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Velocity
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public Velocity getVelocity() {
|
||||
if (!hasPlayer())
|
||||
return Velocity.ZERO;
|
||||
var v = player().getVelocity();
|
||||
return new Velocity(v.x, v.y, v.z);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Stats
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public float getHealth() {
|
||||
return hasPlayer() ? player().getHealth() : 0f;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Equipment
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public ItemStackRef getMainHand() {
|
||||
return wrap(hasPlayer() ? player().getMainHandStack() : ItemStack.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStackRef getHelmet() {
|
||||
//? if >=1.21.5 {
|
||||
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.HEAD) : ItemStack.EMPTY);
|
||||
//?} else
|
||||
//return wrap(hasPlayer() ? player().getInventory().armor.get(3) : ItemStack.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStackRef getChestplate() {
|
||||
//? if >=1.21.5 {
|
||||
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.CHEST) : ItemStack.EMPTY);
|
||||
//?} else
|
||||
//return wrap(hasPlayer() ? player().getInventory().armor.get(2) : ItemStack.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStackRef getLeggings() {
|
||||
//? if >=1.21.5 {
|
||||
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.LEGS) : ItemStack.EMPTY);
|
||||
//?} else
|
||||
//return wrap(hasPlayer() ? player().getInventory().armor.get(1) : ItemStack.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStackRef getBoots() {
|
||||
//? if >=1.21.5 {
|
||||
return wrap(hasPlayer() ? player().getEquippedStack(EquipmentSlot.FEET) : ItemStack.EMPTY);
|
||||
//?} else
|
||||
//return wrap(hasPlayer() ? player().getInventory().armor.get(0) : ItemStack.EMPTY);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Dummy equipment
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public ItemStackRef getDummyMainHand() {
|
||||
return wrap(new ItemStack(Items.DIAMOND_SWORD));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStackRef getDummyHelmet() {
|
||||
return wrap(new ItemStack(Items.DIAMOND_HELMET));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStackRef getDummyChestplate() {
|
||||
return wrap(new ItemStack(Items.DIAMOND_CHESTPLATE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStackRef getDummyLeggings() {
|
||||
return wrap(new ItemStack(Items.DIAMOND_LEGGINGS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStackRef getDummyBoots() {
|
||||
return wrap(new ItemStack(Items.DIAMOND_BOOTS));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Status effects
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public List<? extends EffectRef> getActiveEffects() {
|
||||
if (!hasPlayer())
|
||||
return Collections.emptyList();
|
||||
|
||||
var effects = player().getActiveStatusEffects().values();
|
||||
List<EffectRef> result = new ArrayList<>(effects.size());
|
||||
for (StatusEffectInstance instance : effects) {
|
||||
result.add((EffectRef) instance);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends EffectRef> getDummyEffects() {
|
||||
return List.of(
|
||||
(EffectRef) new StatusEffectInstance(
|
||||
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("speed")).get(), 12000, 2),
|
||||
(EffectRef) new StatusEffectInstance(
|
||||
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("strength")).get(), 12000, 2),
|
||||
(EffectRef) new StatusEffectInstance(
|
||||
Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("fire_resistance")).get(), 12000, 2));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private static ItemStackRef wrap(ItemStack stack) {
|
||||
return (ItemStackRef) (Object) stack;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.saturnclient.impl.features.render;
|
||||
|
||||
import org.saturnclient.common.feature.RenderFeature;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.option.Perspective;
|
||||
|
||||
/**
|
||||
* Fabric implementation of {@link RenderFeature}.
|
||||
*
|
||||
* Window dimensions and FPS are read directly from
|
||||
* {@link MinecraftClient}. Camera perspective control delegates to
|
||||
* {@link net.minecraft.client.option.GameOptions}.
|
||||
*/
|
||||
public class RenderFeatureImpl implements RenderFeature {
|
||||
|
||||
private final MinecraftClient mc;
|
||||
|
||||
public RenderFeatureImpl(MinecraftClient mc) {
|
||||
this.mc = mc;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Window
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public int getScaledWindowWidth() {
|
||||
return mc.getWindow().getScaledWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScaledWindowHeight() {
|
||||
return mc.getWindow().getScaledHeight();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Performance
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public int getFps() {
|
||||
return mc.getCurrentFps();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Camera perspective
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public boolean isFirstPerson() {
|
||||
return mc.options.getPerspective() == Perspective.FIRST_PERSON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFirstPerson() {
|
||||
mc.options.setPerspective(Perspective.FIRST_PERSON);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThirdPersonBack() {
|
||||
mc.options.setPerspective(Perspective.THIRD_PERSON_BACK);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.saturnclient.impl.features.world;
|
||||
|
||||
import org.saturnclient.common.feature.WorldFeature;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
|
||||
/**
|
||||
* Fabric implementation of {@link WorldFeature}.
|
||||
*
|
||||
* Reads the client world's time directly from {@link MinecraftClient}.
|
||||
* Returns safe defaults when no world is loaded.
|
||||
*/
|
||||
public class WorldFeatureImpl implements WorldFeature {
|
||||
|
||||
private final MinecraftClient mc;
|
||||
|
||||
public WorldFeatureImpl(MinecraftClient mc) {
|
||||
this.mc = mc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasWorld() {
|
||||
return mc.world != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total world age in ticks, or {@code 0} when no world
|
||||
* is loaded. Used by {@link TpsFeature#onTimePacket} (which is
|
||||
* called from the mixin and stores its own copy) and by
|
||||
* {@link DayCounterFeature} for the day count.
|
||||
*/
|
||||
@Override
|
||||
public long getWorldAge() {
|
||||
// World.getTime() was a shortcut for this, and 1.21.11 removed it.
|
||||
return hasWorld() ? mc.world.getLevelProperties().getTime() : 0L;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.saturnclient.impl.mixins;
|
||||
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
@Mixin(Identifier.class)
|
||||
public abstract class IdentifierMixin implements IdentifierRef {
|
||||
@Shadow
|
||||
public abstract String toString();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.saturnclient.impl.mixins;
|
||||
|
||||
import org.saturnclient.common.ref.game.ItemStackRef;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import net.minecraft.item.ItemStack;
|
||||
|
||||
@Mixin(ItemStack.class)
|
||||
public abstract class ItemStackMixin implements ItemStackRef {
|
||||
@Shadow
|
||||
public abstract boolean isEmpty();
|
||||
|
||||
@Shadow
|
||||
public abstract int getMaxDamage();
|
||||
|
||||
@Shadow
|
||||
public abstract int getDamage();
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.saturnclient.impl.mixins;
|
||||
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
//? if <1.21.6 {
|
||||
/*import org.saturnclient.common.ref.render.MatrixStackRef;
|
||||
//? if >=1.21.5
|
||||
import org.joml.Quaternionfc;
|
||||
//? if <1.21.5
|
||||
//import org.joml.Quaternionf;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import net.minecraft.util.math.RotationAxis;
|
||||
*///?}
|
||||
|
||||
/**
|
||||
* Makes MatrixStack a MatrixStackRef, for GUI drawing before 1.21.6. From 1.21.6 the GUI uses a 2D
|
||||
* Matrix3x2fStack (Matrix3x2fStackRef) instead, so this mixin is empty but stays registered, keeping
|
||||
* impl.mixins.json the same for every version.
|
||||
*/
|
||||
@Mixin(MatrixStack.class)
|
||||
//? if <1.21.6 {
|
||||
/*public abstract class MatrixStackMixin implements MatrixStackRef {
|
||||
@Shadow
|
||||
public abstract void push();
|
||||
|
||||
@Shadow
|
||||
public abstract void pop();
|
||||
|
||||
@Shadow
|
||||
public abstract void translate(float x, float y, float z);
|
||||
|
||||
@Shadow
|
||||
public abstract void scale(float x, float y, float z);
|
||||
|
||||
@Shadow
|
||||
//? if >=1.21.5
|
||||
public abstract void multiply(Quaternionfc quaternion);
|
||||
//? if <1.21.5
|
||||
//public abstract void multiply(Quaternionf quaternion);
|
||||
|
||||
@Shadow
|
||||
//? if >=1.21.5
|
||||
public abstract void multiply(Quaternionfc quaternion, float originX, float originY, float originZ);
|
||||
//? if <1.21.5
|
||||
//public abstract void multiply(Quaternionf quaternion, float originX, float originY, float originZ);
|
||||
|
||||
@Override
|
||||
public void translate(float x, float y) {
|
||||
translate(x, y, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scale(float x, float y) {
|
||||
scale(x, y, 1.0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rotate(float angle, float originX, float originY, float originZ) {
|
||||
multiply(RotationAxis.NEGATIVE_Z.rotationDegrees(angle), originX, originY, originZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rotate(float angle) {
|
||||
multiply(RotationAxis.NEGATIVE_Z.rotationDegrees(angle));
|
||||
}
|
||||
}
|
||||
*///?} else
|
||||
public abstract class MatrixStackMixin {}
|
||||
@@ -0,0 +1,168 @@
|
||||
package org.saturnclient.impl.mixins;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.common.ref.game.MinecraftClientRef;
|
||||
import org.saturnclient.common.ref.render.WindowRef;
|
||||
import org.saturnclient.impl.ui.SaturnScreenFabric;
|
||||
import org.saturnclient.ui.SaturnScreen;
|
||||
import org.saturnclient.ui.screens.TitleMenu;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen;
|
||||
import net.minecraft.client.gui.screen.option.OptionsScreen;
|
||||
import net.minecraft.client.gui.screen.world.SelectWorldScreen;
|
||||
import net.minecraft.client.option.GameOptions;
|
||||
import net.minecraft.client.session.Session;
|
||||
import net.minecraft.client.util.Window;
|
||||
import net.minecraft.resource.ReloadableResourceManagerImpl;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
@Mixin(MinecraftClient.class)
|
||||
public abstract class MinecraftClientMixin implements MinecraftClientRef {
|
||||
@Shadow
|
||||
public File runDirectory;
|
||||
|
||||
@Shadow
|
||||
private Window window;
|
||||
|
||||
@Shadow
|
||||
private ReloadableResourceManagerImpl resourceManager;
|
||||
|
||||
@Shadow
|
||||
public GameOptions options;
|
||||
|
||||
@Shadow
|
||||
private Session session;
|
||||
|
||||
@Shadow
|
||||
public abstract void setScreen(@Nullable Screen screen);
|
||||
|
||||
@Shadow
|
||||
@Override
|
||||
public abstract void scheduleStop();
|
||||
|
||||
@Override
|
||||
public File getRunDirectory() {
|
||||
return runDirectory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getResource(IdentifierRef identifier) {
|
||||
try {
|
||||
return resourceManager.getResource((Identifier) (Object) identifier).get().getInputStream();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowRef getWindow() {
|
||||
return (WindowRef) (Object) this.window;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScreen(SaturnScreen screen) {
|
||||
this.setScreen(new SaturnScreenFabric(screen));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScreen(MinecraftScreen screen) {
|
||||
SaturnScreenFabric exitTarget = new SaturnScreenFabric(new TitleMenu());
|
||||
|
||||
switch (screen) {
|
||||
case SelectWorld:
|
||||
this.setScreen(new SelectWorldScreen(exitTarget));
|
||||
break;
|
||||
|
||||
case Multiplayer:
|
||||
this.setScreen(new MultiplayerScreen(exitTarget));
|
||||
break;
|
||||
|
||||
case Options:
|
||||
this.setScreen(new OptionsScreen(exitTarget, this.options));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAccessToken() {
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.session.getAccessToken();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getUuid() {
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
return this.session.getUuidOrNull();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
return this.session.getUsername();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClientStopping(Runnable handler) {
|
||||
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> handler.run());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeOnThread(Runnable runnable) {
|
||||
((java.util.concurrent.Executor) (Object) this).execute(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("resource")
|
||||
public String getPlayerListEntry(UUID uuid) {
|
||||
var client = (MinecraftClient) (Object) this;
|
||||
var handler = client.getNetworkHandler();
|
||||
|
||||
if (handler == null)
|
||||
return null;
|
||||
|
||||
var entry = handler.getPlayerListEntry(uuid);
|
||||
if (entry == null)
|
||||
return null;
|
||||
|
||||
//? if >=1.21.9 {
|
||||
return entry.getProfile().name();
|
||||
//?} else
|
||||
//return entry.getProfile().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("resource")
|
||||
public UUID getPlayerListEntryUUID(String name) {
|
||||
var client = (MinecraftClient) (Object) this;
|
||||
var handler = client.getNetworkHandler();
|
||||
|
||||
if (handler == null)
|
||||
return null;
|
||||
|
||||
var entry = handler.getPlayerListEntry(name);
|
||||
if (entry == null)
|
||||
return null;
|
||||
|
||||
//? if >=1.21.9 {
|
||||
return entry.getProfile().id();
|
||||
//?} else
|
||||
//return entry.getProfile().getId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.saturnclient.impl.mixins;
|
||||
|
||||
import org.joml.Quaternionf;
|
||||
import org.saturnclient.common.ref.render.QuaternionfRef;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
|
||||
@Mixin(Quaternionf.class)
|
||||
public abstract class QuaternionfMixin implements QuaternionfRef {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.saturnclient.impl.mixins;
|
||||
|
||||
import net.minecraft.client.texture.Sprite;
|
||||
import org.saturnclient.common.ref.asset.SpriteRef;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
|
||||
@Mixin(Sprite.class)
|
||||
public abstract class SpriteMixin implements SpriteRef {
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.saturnclient.impl.mixins;
|
||||
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.common.ref.asset.SpriteRef;
|
||||
import org.saturnclient.common.ref.game.EffectRef;
|
||||
//? if <1.21.9
|
||||
//import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import net.minecraft.client.gui.hud.InGameHud;
|
||||
import net.minecraft.entity.effect.StatusEffect;
|
||||
import net.minecraft.entity.effect.StatusEffectInstance;
|
||||
import net.minecraft.registry.entry.RegistryEntry;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
@Mixin(StatusEffectInstance.class)
|
||||
public abstract class StatusEffectMixin implements EffectRef {
|
||||
|
||||
@Shadow
|
||||
public abstract boolean shouldShowIcon();
|
||||
|
||||
@Shadow
|
||||
public abstract boolean isInfinite();
|
||||
|
||||
@Shadow
|
||||
public abstract int getDuration();
|
||||
|
||||
@Shadow
|
||||
public abstract RegistryEntry<StatusEffect> getEffectType();
|
||||
|
||||
@Override
|
||||
public SpriteRef getIcon() {
|
||||
// 1.21.9 changed the GUI atlas, so newer versions draw the effect's texture (getIconId) instead.
|
||||
//? if >=1.21.9 {
|
||||
return null;
|
||||
//?} else if >=1.21.6 {
|
||||
//return (SpriteRef) SaturnClient.client.getGuiAtlasManager().getSprite((InGameHud.getEffectTexture(getEffectType())));
|
||||
//?} else
|
||||
//return (SpriteRef) SaturnClient.client.getStatusEffectSpriteManager().getSprite(getEffectType());
|
||||
}
|
||||
|
||||
// Before 1.21.6 there's no InGameHud.getEffectTexture; getIcon's sprite is used there instead.
|
||||
//? if >=1.21.6 {
|
||||
@Override
|
||||
public IdentifierRef getIconId() {
|
||||
Identifier id = InGameHud.getEffectTexture(getEffectType());
|
||||
|
||||
return IdentifierRef.of(id.getNamespace(), "textures/" + id.getPath() + ".png");
|
||||
}
|
||||
//?}
|
||||
|
||||
@Override
|
||||
public String getInfiniteText() {
|
||||
return "∞";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDurationSeconds() {
|
||||
return getDuration() / 20; // ticks → seconds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.saturnclient.impl.mixins;
|
||||
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.common.ref.render.TextRef;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
|
||||
import net.minecraft.text.Style;
|
||||
import net.minecraft.text.Text;
|
||||
//? if >=1.21.9
|
||||
import net.minecraft.text.StyleSpriteSource.Font;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
@Mixin(Text.class)
|
||||
public interface TextMixin extends TextRef {
|
||||
|
||||
@Override
|
||||
public default TextRef withFont(IdentifierRef font) {
|
||||
//? if >=1.21.9 {
|
||||
return (TextRef) (Text) (((Text) this).copy().setStyle(Style.EMPTY.withFont(new Font((Identifier) (Object) font))));
|
||||
//?} else
|
||||
//return (TextRef) (((Text) this).copy().setStyle(Style.EMPTY.withFont((Identifier) (Object) font)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public default int getWidth() {
|
||||
return SaturnClient.client.textRenderer.getWidth((Text) this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.saturnclient.impl.mixins;
|
||||
|
||||
import org.saturnclient.common.ref.render.WindowRef;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import net.minecraft.client.util.Window;
|
||||
|
||||
@Mixin(Window.class)
|
||||
public abstract class WindowMixin implements WindowRef {
|
||||
@Shadow
|
||||
public abstract int getWidth();
|
||||
|
||||
@Shadow
|
||||
public abstract int getHeight();
|
||||
|
||||
@Shadow
|
||||
public abstract int getFramebufferWidth();
|
||||
|
||||
@Shadow
|
||||
public abstract int getFramebufferHeight();
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.saturnclient.impl.provider;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
|
||||
import org.saturnclient.common.feature.*;
|
||||
import org.saturnclient.common.provider.FeatureProvider;
|
||||
import org.saturnclient.impl.features.entity.EntityFeatureImpl;
|
||||
import org.saturnclient.impl.features.network.NetworkFeatureImpl;
|
||||
import org.saturnclient.impl.features.player.PlayerFeatureImpl;
|
||||
import org.saturnclient.impl.features.render.RenderFeatureImpl;
|
||||
import org.saturnclient.impl.features.world.WorldFeatureImpl;
|
||||
|
||||
/**
|
||||
* Fabric implementation of {@link FeatureProvider}.
|
||||
*
|
||||
* Instantiated once during client startup (e.g. in the mod initialiser)
|
||||
* and passed to every feature constructor. All five sub-modules share
|
||||
* the same {@link MinecraftClient} reference so there is no
|
||||
* inconsistency between modules reading state within the same tick.
|
||||
*
|
||||
* <pre>{@code
|
||||
* FeatureProvider provider = new FeatureProviderFabric(MinecraftClient.getInstance());
|
||||
* FeatureManager.register(new AutoSprintFeature(provider));
|
||||
* FeatureManager.register(new CoordinatesFeature(provider));
|
||||
* // …
|
||||
* }</pre>
|
||||
*/
|
||||
public class FeatureProviderFabric implements FeatureProvider {
|
||||
|
||||
private final PlayerFeature player;
|
||||
private final WorldFeature world;
|
||||
private final EntityFeature entity;
|
||||
private final RenderFeature render;
|
||||
private final NetworkFeature network;
|
||||
|
||||
public FeatureProviderFabric(MinecraftClient mc) {
|
||||
this.player = new PlayerFeatureImpl(mc);
|
||||
this.world = new WorldFeatureImpl(mc);
|
||||
this.entity = new EntityFeatureImpl(mc);
|
||||
this.render = new RenderFeatureImpl(mc);
|
||||
this.network = new NetworkFeatureImpl(mc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayerFeature player() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorldFeature world() {
|
||||
return world;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityFeature entity() {
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenderFeature render() {
|
||||
return render;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NetworkFeature network() {
|
||||
return network;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.saturnclient.impl.provider;
|
||||
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.saturnclient.common.provider.GLFWProvider;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
|
||||
public class GLFWProviderImpl implements GLFWProvider {
|
||||
@Override
|
||||
public boolean isKeyPressed(int key) {
|
||||
return GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), key) == GLFW.GLFW_PRESS
|
||||
&& SaturnClient.client.currentScreen == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeyName(int key) {
|
||||
return GLFW.glfwGetKeyName(key, GLFW.glfwGetKeyScancode(key));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.saturnclient.impl.provider;
|
||||
|
||||
import org.saturnclient.common.provider.RefConstructorProvider;
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.common.ref.render.TextRef;
|
||||
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public class RefConstructorImpl implements RefConstructorProvider {
|
||||
@Override
|
||||
public IdentifierRef identifier(String fullPath) {
|
||||
return (IdentifierRef) (Object) Identifier.of(fullPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentifierRef identifier(String namespace, String path) {
|
||||
return (IdentifierRef) (Object) Identifier.of(namespace, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextRef text(String text) {
|
||||
return (TextRef) (Text) Text.literal(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package org.saturnclient.impl.provider;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.toast.SystemToast;
|
||||
//? if >=1.21.9 {
|
||||
import net.minecraft.entity.player.SkinTextures;
|
||||
//?} else
|
||||
//import net.minecraft.client.util.SkinTextures;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
import org.saturnclient.common.provider.SaturnProvider;
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.common.ref.game.MinecraftClientRef;
|
||||
import org.saturnclient.cosmetics.Emotes;
|
||||
import org.saturnclient.impl.cosmetics.utils.IdentifierUtils;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
|
||||
public class SaturnProviderImpl implements SaturnProvider {
|
||||
/** Looks up the signed-in player's skin; made once, since the skin provider caches it */
|
||||
private Supplier<Identifier> playerSkin;
|
||||
|
||||
@Override
|
||||
public MinecraftClientRef getClient() {
|
||||
return (MinecraftClientRef) SaturnClient.client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playEmote(UUID fromPlayerUuid, String emoteIdOrNull) {
|
||||
if (fromPlayerUuid == null) {
|
||||
return;
|
||||
}
|
||||
// An empty or unknown ID stops whatever the player is playing.
|
||||
Emotes.get(emoteIdOrNull == null ? "" : emoteIdOrNull).ifPresentOrElse(
|
||||
emote -> org.saturnclient.emotes.fabric.Emotes.play(fromPlayerUuid, emote.animation()),
|
||||
() -> org.saturnclient.emotes.fabric.Emotes.stop(fromPlayerUuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logInfo(String message) {
|
||||
SaturnClient.LOGGER.info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logError(String message) {
|
||||
SaturnClient.LOGGER.error(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logError(String message, Throwable throwable) {
|
||||
SaturnClient.LOGGER.error(message, throwable);
|
||||
}
|
||||
|
||||
/** Shown longer than the vanilla default, since these ask the player to act. */
|
||||
private static final SystemToast.Type WARNING_TOAST = new SystemToast.Type(15_000L);
|
||||
|
||||
@Override
|
||||
public IdentifierRef getPlayerSkin() {
|
||||
if (playerSkin == null) {
|
||||
MinecraftClient client = MinecraftClient.getInstance();
|
||||
// 1.21.9 moved SkinTextures and renamed the skin provider's lookup.
|
||||
//? if >=1.21.9 {
|
||||
Supplier<SkinTextures> textures = client.getSkinProvider().supplySkinTextures(client.getGameProfile(), false);
|
||||
playerSkin = () -> textures.get().body().texturePath();
|
||||
//?} else {
|
||||
/*Supplier<SkinTextures> textures = client.getSkinProvider().getSkinTexturesSupplier(client.getGameProfile());
|
||||
playerSkin = () -> textures.get().texture();
|
||||
*///?}
|
||||
}
|
||||
|
||||
return (IdentifierRef) (Object) playerSkin.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModVersion() {
|
||||
return FabricLoader.getInstance().getModContainer(SaturnClient.MOD_ID)
|
||||
.map(mod -> mod.getMetadata().getVersion().getFriendlyString())
|
||||
.orElse("unknown");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showWarning(String title, String message) {
|
||||
MinecraftClient client = MinecraftClient.getInstance();
|
||||
client.execute(() -> client.getToastManager().add(
|
||||
SystemToast.create(client, WARNING_TOAST, Text.literal(title), Text.literal(message))));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerBufferedImageTexture(IdentifierRef i, BufferedImage bi) {
|
||||
IdentifierUtils.registerBufferedImageTextureFast(i, bi);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.saturnclient.impl.ref;
|
||||
|
||||
import org.joml.Matrix3x2fStack;
|
||||
import org.saturnclient.common.ref.render.MatrixStackRef;
|
||||
|
||||
public class Matrix3x2fStackRef implements MatrixStackRef {
|
||||
public final Matrix3x2fStack stack;
|
||||
|
||||
public Matrix3x2fStackRef(Matrix3x2fStack stack) {
|
||||
this.stack = stack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void push() {
|
||||
stack.pushMatrix();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pop() {
|
||||
stack.popMatrix();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scale(float x, float y) {
|
||||
stack.scale(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void translate(float x, float y) {
|
||||
stack.translate(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rotate(float angle) {
|
||||
stack.rotate((float) Math.toRadians(angle));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rotate(float angle, float originX, float originY, float originZ) {
|
||||
translate(originX, originY);
|
||||
rotate(angle);
|
||||
translate(-originX, -originY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package org.saturnclient.impl.ui;
|
||||
|
||||
// Entities in the UI are drawn through the GUI render state from 1.21.6, and straight into vertex
|
||||
// consumers before. This file has one implementation for each side.
|
||||
//? if >=1.21.6 {
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Quaternionf;
|
||||
import org.joml.Vector3f;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.components.SkinPreview.EntityDrawer;
|
||||
|
||||
//? if >=1.21.9 {
|
||||
import net.minecraft.client.render.entity.EntityRenderManager;
|
||||
//?} else
|
||||
//import net.minecraft.client.render.entity.EntityRenderDispatcher;
|
||||
import net.minecraft.client.render.entity.EntityRenderer;
|
||||
import net.minecraft.client.render.entity.state.EntityRenderState;
|
||||
import net.minecraft.entity.LivingEntity;
|
||||
|
||||
public class EntityDrawerImpl implements EntityDrawer {
|
||||
@Override
|
||||
public boolean render(RenderScope renderScope, int width, int height, float yaw) {
|
||||
LivingEntity entity = SaturnClient.client.player;
|
||||
|
||||
if (entity == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The player is about 1.8 blocks tall; leave some room above and below.
|
||||
drawEntity(renderScope, 0, 0, width, height, (int) (height / 2.2F), 0.0625F, yaw, entity);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Draws the entity in the box, turned by yaw degrees (0 faces the viewer) with its head straight.
|
||||
public static void drawEntity(RenderScope renderScope, int x1, int y1, int x2, int y2, int size, float scale,
|
||||
float yaw, LivingEntity entity) {
|
||||
renderScope.enableScissor(x1, y1, x2, y2);
|
||||
Quaternionf quaternionf = (new Quaternionf()).rotateZ((float) Math.PI);
|
||||
Quaternionf quaternionf2 = new Quaternionf();
|
||||
float j = entity.bodyYaw;
|
||||
float k = entity.getYaw();
|
||||
float l = entity.getPitch();
|
||||
float m = entity.lastHeadYaw;
|
||||
float n = entity.headYaw;
|
||||
entity.bodyYaw = 180.0F + yaw;
|
||||
entity.setYaw(180.0F + yaw);
|
||||
entity.setPitch(0.0F);
|
||||
entity.headYaw = entity.getYaw();
|
||||
entity.lastHeadYaw = entity.getYaw();
|
||||
float o = entity.getScale();
|
||||
Vector3f vector3f = new Vector3f(0.0F, entity.getHeight() / 2.0F + scale * o, 0.0F);
|
||||
float p = (float) size / o;
|
||||
drawEntity(renderScope, x1, y1, x2, y2, p, vector3f, quaternionf, quaternionf2, entity);
|
||||
entity.bodyYaw = j;
|
||||
entity.setYaw(k);
|
||||
entity.setPitch(l);
|
||||
entity.lastHeadYaw = m;
|
||||
entity.headYaw = n;
|
||||
renderScope.disableScissor();
|
||||
}
|
||||
|
||||
public static void drawEntity(RenderScope renderScope, int x1, int y1, int x2, int y2, float scale,
|
||||
Vector3f translation, Quaternionf rotation, @Nullable Quaternionf overrideCameraAngle,
|
||||
LivingEntity entity) {
|
||||
//? if >=1.21.9 {
|
||||
EntityRenderManager entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
|
||||
//?} else
|
||||
//EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
|
||||
EntityRenderer<? super LivingEntity, ?> entityRenderer = entityRenderDispatcher.getRenderer(entity);
|
||||
EntityRenderState entityRenderState = entityRenderer.getAndUpdateRenderState(entity, 1.0F);
|
||||
//? if <1.21.11
|
||||
//entityRenderState.hitbox = null;
|
||||
|
||||
if (renderScope instanceof RenderScopeImpl i)
|
||||
i.addEntity(entityRenderState, scale, translation, rotation, overrideCameraAngle, x1, y1, x2, y2);
|
||||
}
|
||||
}
|
||||
//?} else {
|
||||
/*import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Quaternionf;
|
||||
import org.joml.Vector3f;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.components.SkinPreview.EntityDrawer;
|
||||
|
||||
import net.minecraft.client.render.DiffuseLighting;
|
||||
import net.minecraft.client.render.entity.EntityRenderDispatcher;
|
||||
import net.minecraft.entity.LivingEntity;
|
||||
|
||||
public class EntityDrawerImpl implements EntityDrawer {
|
||||
// Draws the entity in the box, turned by yaw degrees (0 faces the viewer) with its head straight.
|
||||
public static void drawEntity(RenderScope renderScope, int x1, int y1, int x2, int y2, int size, float f,
|
||||
float yaw, LivingEntity entity) {
|
||||
float g = (float) (x1 + x2) / 2.0F;
|
||||
float h = (float) (y1 + y2) / 2.0F;
|
||||
renderScope.enableScissor(x1, y1, x2, y2);
|
||||
Quaternionf quaternionf = (new Quaternionf()).rotateZ(3.1415927F);
|
||||
float k = entity.bodyYaw;
|
||||
float l = entity.getYaw();
|
||||
float m = entity.getPitch();
|
||||
//? if >=1.21.5
|
||||
float n = entity.lastHeadYaw;
|
||||
//? if <1.21.5
|
||||
//float n = entity.prevHeadYaw;
|
||||
float o = entity.headYaw;
|
||||
entity.setYaw(180.0F + yaw);
|
||||
entity.bodyYaw = 180.0F + yaw;
|
||||
entity.setPitch(0.0F);
|
||||
entity.headYaw = entity.getYaw();
|
||||
//? if >=1.21.5
|
||||
entity.lastHeadYaw = entity.getHeadYaw();
|
||||
//? if <1.21.5
|
||||
//entity.prevHeadYaw = entity.getHeadYaw();
|
||||
entity.setCustomNameVisible(false);
|
||||
|
||||
float p = entity.getScale();
|
||||
Vector3f vector3f = new Vector3f(0.0F, entity.getHeight() / 2.0F + f * p, 0.0F);
|
||||
float q = (float) size / p;
|
||||
drawEntity(renderScope, g, h, q, vector3f, quaternionf, null, entity);
|
||||
|
||||
entity.setCustomNameVisible(true);
|
||||
entity.bodyYaw = k;
|
||||
entity.setYaw(l);
|
||||
entity.setPitch(m);
|
||||
//? if >=1.21.5
|
||||
entity.lastHeadYaw = n;
|
||||
//? if <1.21.5
|
||||
//entity.prevHeadYaw = n;
|
||||
entity.headYaw = o;
|
||||
renderScope.disableScissor();
|
||||
}
|
||||
|
||||
public static void drawEntity(RenderScope renderScope, float x, float y, float size, Vector3f vector3f,
|
||||
Quaternionf quaternionf, @Nullable Quaternionf quaternionf2, LivingEntity entity) {
|
||||
if (renderScope instanceof RenderScopeImpl scopeImpl) {
|
||||
scopeImpl.matrices.push();
|
||||
scopeImpl.matrices.translate((double) x, (double) y, 0);
|
||||
scopeImpl.matrices.scale(size, size, -size);
|
||||
scopeImpl.matrices.translate(vector3f.x, vector3f.y, vector3f.z);
|
||||
scopeImpl.matrices.multiply(quaternionf);
|
||||
scopeImpl.draw();
|
||||
//? if >=1.21.5
|
||||
DiffuseLighting.disableGuiDepthLighting();
|
||||
//? if <1.21.5
|
||||
//DiffuseLighting.method_34742();
|
||||
EntityRenderDispatcher entityRenderDispatcher = SaturnClient.client.getEntityRenderDispatcher();
|
||||
|
||||
entityRenderDispatcher.setRenderShadows(false);
|
||||
scopeImpl.draw((vertexConsumers) -> {
|
||||
entityRenderDispatcher.render(entity, 0.0, 0.0, 0.0, 1.0F,
|
||||
scopeImpl.matrices, vertexConsumers, 15728880);
|
||||
});
|
||||
|
||||
scopeImpl.draw();
|
||||
entityRenderDispatcher.setRenderShadows(true);
|
||||
scopeImpl.getMatrixStack().pop();
|
||||
DiffuseLighting.enableGuiDepthLighting();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean render(RenderScope renderScope, int width, int height, float yaw) {
|
||||
LivingEntity entity = SaturnClient.client.player;
|
||||
if (entity == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The player is about 1.8 blocks tall; leave some room above and below.
|
||||
drawEntity(renderScope, 0, 0, width, height, (int) (height / 2.2F), 0.0625F, yaw, entity);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
*///?}
|
||||
@@ -0,0 +1,380 @@
|
||||
package org.saturnclient.impl.ui;
|
||||
|
||||
// Glass: a copy of the game behind the screen, blurred, drawn through each panel's rounded rectangle
|
||||
// texture as a mask. There's a copy for each blur strength in use (menus and widgets have their own),
|
||||
// blurred with vanilla's blur at that radius, so it holds the world or panorama without the screen's
|
||||
// own animated background blur.
|
||||
//
|
||||
// The GUI rewrites split this into three implementations:
|
||||
// - 1.21.6+: the GUI is drawn after the screen builds it, so the copy is taken from the main
|
||||
// framebuffer while the screen builds its GUI, and glass is a GUI element with its own pipeline
|
||||
// (shaders/core/glass.fsh). The blur is vanilla's effect rebuilt with the radius set.
|
||||
// - 1.21.5: the screen draws straight into the framebuffer, blurring it first, so the game is saved
|
||||
// before the blur (saveBackground). Glass is a render layer with its own pipeline
|
||||
// (shaders/core/glass_pre_1_21_6.fsh), and the blur takes the radius as it runs.
|
||||
// - 1.21.4: as 1.21.5, with GL framebuffers copied by blitting, and a shader program
|
||||
// (shaders/core/glass.json) instead of a pipeline.
|
||||
//? if >=1.21.6 {
|
||||
import java.io.Reader;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.textures.FilterMode;
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
import com.mojang.serialization.JsonOps;
|
||||
|
||||
import org.saturnclient.config.Config;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
|
||||
import net.minecraft.client.gl.Framebuffer;
|
||||
import net.minecraft.client.gl.PostEffectPipeline;
|
||||
import net.minecraft.client.gl.PostEffectProcessor;
|
||||
import net.minecraft.client.gl.RenderPipelines;
|
||||
import net.minecraft.client.gl.SimpleFramebuffer;
|
||||
import net.minecraft.client.render.DefaultFramebufferSet;
|
||||
import net.minecraft.client.render.ProjectionMatrix2;
|
||||
import net.minecraft.client.texture.AbstractTexture;
|
||||
import net.minecraft.client.texture.TextureSetup;
|
||||
import net.minecraft.client.util.Pool;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public final class GlassRenderer {
|
||||
public static final RenderPipeline PIPELINE = RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET)
|
||||
.withLocation(Identifier.of("saturnclient", "pipeline/glass"))
|
||||
.withFragmentShader(Identifier.of("saturnclient", "core/glass"))
|
||||
.withSampler("Sampler1")
|
||||
// The transforms block ends with LineWidth before 1.21.11 (see glass.fsh).
|
||||
//? if <1.21.11
|
||||
//.withShaderDefine("HAS_LINE_WIDTH")
|
||||
.build();
|
||||
|
||||
private static final Identifier BLUR = Identifier.ofVanilla("post_effect/blur.json");
|
||||
private static final ProjectionMatrix2 PROJECTION = new ProjectionMatrix2("saturn glass", 0.1f, 1000f, false);
|
||||
// For the blurs' intermediate targets, freed once unused for a few frames
|
||||
private static final Pool POOL = new Pool(3);
|
||||
|
||||
// By blur strength. A blur that failed to load is kept as null, so it isn't retried every frame.
|
||||
private static final Map<Integer, PostEffectProcessor> BLURS = new HashMap<>();
|
||||
private static final Map<Integer, SimpleFramebuffer> BACKDROPS = new HashMap<>();
|
||||
|
||||
private GlassRenderer() {
|
||||
}
|
||||
|
||||
// Copies the main framebuffer and blurs the copy by blur. Call once a frame for each blur, while
|
||||
// the screen builds its GUI, before drawing glass with it.
|
||||
public static void captureBackdrop(int blur) {
|
||||
Framebuffer main = SaturnClient.client.getFramebuffer();
|
||||
int width = main.textureWidth;
|
||||
int height = main.textureHeight;
|
||||
|
||||
SimpleFramebuffer backdrop = BACKDROPS.get(blur);
|
||||
if (backdrop == null) {
|
||||
backdrop = new SimpleFramebuffer("Saturn glass " + blur, width, height, false);
|
||||
BACKDROPS.put(blur, backdrop);
|
||||
} else if (backdrop.textureWidth != width || backdrop.textureHeight != height) {
|
||||
backdrop.resize(width, height);
|
||||
}
|
||||
//? if <1.21.11
|
||||
//backdrop.getColorAttachment().setTextureFilter(FilterMode.LINEAR, false);
|
||||
|
||||
RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture(
|
||||
main.getColorAttachment(), backdrop.getColorAttachment(), 0, 0, 0, 0, 0, width, height);
|
||||
|
||||
PostEffectProcessor processor = getBlur(blur);
|
||||
if (processor != null) {
|
||||
processor.render(backdrop, POOL);
|
||||
}
|
||||
POOL.decrementLifespan();
|
||||
release(blur);
|
||||
}
|
||||
|
||||
// The mask, then the copy blurred by blur, as the glass pipeline samples them
|
||||
public static TextureSetup textures(AbstractTexture mask, int blur) {
|
||||
GpuTextureView backdrop = BACKDROPS.get(blur).getColorAttachmentView();
|
||||
//? if >=1.21.11
|
||||
return TextureSetup.of(mask.getGlTextureView(), mask.getSampler(), backdrop, RenderSystem.getSamplerCache().get(FilterMode.LINEAR));
|
||||
//? if <1.21.11
|
||||
//return TextureSetup.of(mask.getGlTextureView(), backdrop);
|
||||
}
|
||||
|
||||
// Frees the copies and blurs of strengths that are no longer set, besides kept
|
||||
private static void release(int kept) {
|
||||
BACKDROPS.entrySet().removeIf(entry -> {
|
||||
boolean unused = entry.getKey() != kept && !isSet(entry.getKey());
|
||||
if (unused) {
|
||||
entry.getValue().delete();
|
||||
}
|
||||
return unused;
|
||||
});
|
||||
BLURS.entrySet().removeIf(entry -> {
|
||||
boolean unused = entry.getKey() != kept && !isSet(entry.getKey());
|
||||
if (unused && entry.getValue() != null) {
|
||||
entry.getValue().close();
|
||||
}
|
||||
return unused;
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean isSet(int blur) {
|
||||
return blur == Config.menuBlur.value || blur == Config.widgetBlur.value;
|
||||
}
|
||||
|
||||
private static PostEffectProcessor getBlur(int blur) {
|
||||
if (!BLURS.containsKey(blur)) {
|
||||
BLURS.put(blur, loadBlur(blur));
|
||||
}
|
||||
return BLURS.get(blur);
|
||||
}
|
||||
|
||||
// Vanilla's blur, with its radius set to blur. Vanilla's own leaves it at 0, which makes the shader
|
||||
// read the menu blur setting (Saturn's animated background blur while a Saturn screen is open).
|
||||
private static PostEffectProcessor loadBlur(int blur) {
|
||||
try (Reader reader = SaturnClient.client.getResourceManager().getResourceOrThrow(BLUR).getReader()) {
|
||||
JsonElement json = JsonParser.parseReader(reader);
|
||||
setRadius(json, blur);
|
||||
|
||||
PostEffectPipeline pipeline = PostEffectPipeline.CODEC.parse(JsonOps.INSTANCE, json).getOrThrow();
|
||||
return PostEffectProcessor.parseEffect(pipeline, SaturnClient.client.getTextureManager(),
|
||||
DefaultFramebufferSet.MAIN_ONLY, Identifier.of("saturnclient", "glass_blur_" + blur), PROJECTION);
|
||||
} catch (Exception e) {
|
||||
SaturnClient.LOGGER.error("Couldn't load the glass blur", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Sets every "Radius" uniform in the effect
|
||||
private static void setRadius(JsonElement json, int radius) {
|
||||
if (json.isJsonArray()) {
|
||||
json.getAsJsonArray().forEach(element -> setRadius(element, radius));
|
||||
} else if (json.isJsonObject()) {
|
||||
JsonObject object = json.getAsJsonObject();
|
||||
if (object.has("name") && object.get("name").getAsString().equals("Radius")) {
|
||||
object.addProperty("value", (float) radius);
|
||||
}
|
||||
object.entrySet().forEach(entry -> setRadius(entry.getValue(), radius));
|
||||
}
|
||||
}
|
||||
}
|
||||
//?} else if >=1.21.5 {
|
||||
/*import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.textures.FilterMode;
|
||||
|
||||
import org.saturnclient.config.Config;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
|
||||
import net.minecraft.client.gl.Framebuffer;
|
||||
import net.minecraft.client.gl.PostEffectProcessor;
|
||||
import net.minecraft.client.gl.RenderPipelines;
|
||||
import net.minecraft.client.gl.SimpleFramebuffer;
|
||||
import net.minecraft.client.render.DefaultFramebufferSet;
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
import net.minecraft.client.render.RenderPhase;
|
||||
import net.minecraft.client.util.Pool;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public final class GlassRenderer {
|
||||
private static final RenderPipeline PIPELINE = RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET)
|
||||
.withLocation(Identifier.of("saturnclient", "pipeline/glass"))
|
||||
.withFragmentShader(Identifier.of("saturnclient", "core/glass_pre_1_21_6"))
|
||||
.withSampler("Sampler1")
|
||||
.build();
|
||||
|
||||
private static final Identifier BLUR = Identifier.ofVanilla("blur");
|
||||
// For the blur's intermediate target, freed once unused for a few frames
|
||||
private static final Pool POOL = new Pool(3);
|
||||
|
||||
private static final Map<Integer, SimpleFramebuffer> BACKDROPS = new HashMap<>();
|
||||
// By mask and blur
|
||||
private static final Map<String, RenderLayer> LAYERS = new HashMap<>();
|
||||
// The game behind the screen before the screen blurs it, which glass is blurred from
|
||||
private static SimpleFramebuffer background;
|
||||
|
||||
private GlassRenderer() {
|
||||
}
|
||||
|
||||
// Saves the main framebuffer for glass. Call each frame before the screen blurs its background.
|
||||
public static void saveBackground() {
|
||||
background = copy(SaturnClient.client.getFramebuffer(), background, "Saturn glass background");
|
||||
}
|
||||
|
||||
// Copies the saved background and blurs the copy by blur. Call once a frame for each blur, before
|
||||
// drawing glass with it.
|
||||
public static void captureBackdrop(int blur) {
|
||||
Framebuffer from = background != null ? background : SaturnClient.client.getFramebuffer();
|
||||
SimpleFramebuffer backdrop = copy(from, BACKDROPS.get(blur), "Saturn glass " + blur);
|
||||
BACKDROPS.put(blur, backdrop);
|
||||
|
||||
PostEffectProcessor processor = SaturnClient.client.getShaderLoader().loadPostEffect(BLUR,
|
||||
DefaultFramebufferSet.MAIN_ONLY);
|
||||
if (processor != null) {
|
||||
processor.render(backdrop, POOL, pass -> pass.setUniform("Radius", (float) blur));
|
||||
}
|
||||
POOL.decrementLifespan();
|
||||
release(blur);
|
||||
}
|
||||
|
||||
// Draws glass through mask with the copy blurred by blur
|
||||
public static RenderLayer getLayer(Identifier mask, int blur) {
|
||||
return LAYERS.computeIfAbsent(mask + " " + blur, key -> RenderLayer.of("saturn_glass", 786432, PIPELINE,
|
||||
RenderLayer.MultiPhaseParameters.builder()
|
||||
.texture(new RenderPhase.TextureBase(() -> {
|
||||
RenderSystem.setShaderTexture(0,
|
||||
SaturnClient.client.getTextureManager().getTexture(mask).getGlTexture());
|
||||
RenderSystem.setShaderTexture(1, BACKDROPS.get(blur).getColorAttachment());
|
||||
}, () -> {
|
||||
}))
|
||||
.build(false)));
|
||||
}
|
||||
|
||||
// Frees the copies of strengths that are no longer set, besides kept
|
||||
private static void release(int kept) {
|
||||
BACKDROPS.entrySet().removeIf(entry -> {
|
||||
boolean unused = entry.getKey() != kept && entry.getKey() != Config.menuBlur.value
|
||||
&& entry.getKey() != Config.widgetBlur.value;
|
||||
if (unused) {
|
||||
entry.getValue().delete();
|
||||
}
|
||||
return unused;
|
||||
});
|
||||
}
|
||||
|
||||
// Copies from into to, made or resized to match
|
||||
private static SimpleFramebuffer copy(Framebuffer from, SimpleFramebuffer to, String name) {
|
||||
int width = from.textureWidth;
|
||||
int height = from.textureHeight;
|
||||
|
||||
if (to == null) {
|
||||
to = new SimpleFramebuffer(name, width, height, false);
|
||||
} else if (to.textureWidth != width || to.textureHeight != height) {
|
||||
to.resize(width, height);
|
||||
}
|
||||
to.getColorAttachment().setTextureFilter(FilterMode.LINEAR, false);
|
||||
|
||||
RenderSystem.getDevice().createCommandEncoder().copyTextureToTexture(
|
||||
from.getColorAttachment(), to.getColorAttachment(), 0, 0, 0, 0, 0, width, height);
|
||||
return to;
|
||||
}
|
||||
}
|
||||
*///?} else {
|
||||
/*import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
|
||||
import com.mojang.blaze3d.platform.GlStateManager;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
|
||||
import org.saturnclient.config.Config;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
|
||||
import net.minecraft.client.gl.Defines;
|
||||
import net.minecraft.client.gl.Framebuffer;
|
||||
import net.minecraft.client.gl.PostEffectProcessor;
|
||||
import net.minecraft.client.gl.ShaderProgramKey;
|
||||
import net.minecraft.client.gl.SimpleFramebuffer;
|
||||
import net.minecraft.client.render.DefaultFramebufferSet;
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
import net.minecraft.client.render.RenderPhase;
|
||||
import net.minecraft.client.render.VertexFormat;
|
||||
import net.minecraft.client.render.VertexFormats;
|
||||
import net.minecraft.client.util.Pool;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public final class GlassRenderer {
|
||||
private static final ShaderProgramKey PROGRAM = new ShaderProgramKey(Identifier.of("saturnclient", "core/glass"),
|
||||
VertexFormats.POSITION_TEXTURE_COLOR, Defines.EMPTY);
|
||||
|
||||
private static final Identifier BLUR = Identifier.ofVanilla("blur");
|
||||
// For the blur's intermediate target, freed once unused for a few frames
|
||||
private static final Pool POOL = new Pool(3);
|
||||
|
||||
private static final Map<Integer, SimpleFramebuffer> BACKDROPS = new HashMap<>();
|
||||
// By mask and blur
|
||||
private static final Map<String, RenderLayer> LAYERS = new HashMap<>();
|
||||
// The game behind the screen before the screen blurs it, which glass is blurred from
|
||||
private static SimpleFramebuffer background;
|
||||
|
||||
private GlassRenderer() {
|
||||
}
|
||||
|
||||
// Saves the main framebuffer for glass. Call each frame before the screen blurs its background.
|
||||
public static void saveBackground() {
|
||||
background = copy(SaturnClient.client.getFramebuffer(), background);
|
||||
}
|
||||
|
||||
// Copies the saved background and blurs the copy by blur. Call once a frame for each blur, before
|
||||
// drawing glass with it.
|
||||
public static void captureBackdrop(int blur) {
|
||||
Framebuffer from = background != null ? background : SaturnClient.client.getFramebuffer();
|
||||
SimpleFramebuffer backdrop = copy(from, BACKDROPS.get(blur));
|
||||
BACKDROPS.put(blur, backdrop);
|
||||
|
||||
PostEffectProcessor processor = SaturnClient.client.getShaderLoader().loadPostEffect(BLUR,
|
||||
DefaultFramebufferSet.MAIN_ONLY);
|
||||
if (processor != null) {
|
||||
processor.setUniforms("Radius", blur);
|
||||
processor.render(backdrop, POOL);
|
||||
}
|
||||
POOL.decrementLifespan();
|
||||
release(blur);
|
||||
SaturnClient.client.getFramebuffer().beginWrite(false);
|
||||
}
|
||||
|
||||
// Draws glass through mask with the copy blurred by blur
|
||||
public static RenderLayer getLayer(Identifier mask, int blur) {
|
||||
return LAYERS.computeIfAbsent(mask + " " + blur, key -> RenderLayer.of("saturn_glass",
|
||||
VertexFormats.POSITION_TEXTURE_COLOR, VertexFormat.DrawMode.QUADS, 786432,
|
||||
RenderLayer.MultiPhaseParameters.builder()
|
||||
.texture(new RenderPhase.TextureBase(() -> {
|
||||
RenderSystem.setShaderTexture(0, mask);
|
||||
RenderSystem.setShaderTexture(1, BACKDROPS.get(blur).getColorAttachment());
|
||||
}, () -> {
|
||||
}))
|
||||
.program(new RenderPhase.ShaderProgram(PROGRAM))
|
||||
.transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
|
||||
.build(false)));
|
||||
}
|
||||
|
||||
// Frees the copies of strengths that are no longer set, besides kept
|
||||
private static void release(int kept) {
|
||||
BACKDROPS.entrySet().removeIf(entry -> {
|
||||
boolean unused = entry.getKey() != kept && entry.getKey() != Config.menuBlur.value
|
||||
&& entry.getKey() != Config.widgetBlur.value;
|
||||
if (unused) {
|
||||
entry.getValue().delete();
|
||||
}
|
||||
return unused;
|
||||
});
|
||||
}
|
||||
|
||||
// Copies from into to, made or resized to match
|
||||
private static SimpleFramebuffer copy(Framebuffer from, SimpleFramebuffer to) {
|
||||
int width = from.textureWidth;
|
||||
int height = from.textureHeight;
|
||||
|
||||
if (to == null) {
|
||||
to = new SimpleFramebuffer(width, height, false);
|
||||
} else if (to.textureWidth != width || to.textureHeight != height) {
|
||||
to.resize(width, height);
|
||||
}
|
||||
to.setTexFilter(GL11.GL_LINEAR);
|
||||
|
||||
GlStateManager._glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, from.fbo);
|
||||
GlStateManager._glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, to.fbo);
|
||||
GlStateManager._glBlitFrameBuffer(0, 0, width, height, 0, 0, width, height, GL11.GL_COLOR_BUFFER_BIT,
|
||||
GL11.GL_NEAREST);
|
||||
SaturnClient.client.getFramebuffer().beginWrite(false);
|
||||
return to;
|
||||
}
|
||||
}
|
||||
*///?}
|
||||
@@ -0,0 +1,839 @@
|
||||
package org.saturnclient.impl.ui;
|
||||
|
||||
// 1.21.6 rewrote GUI rendering: render states and a 2D matrix stack (Matrix3x2fStack) instead of
|
||||
// drawing into vertex consumers with a MatrixStack. This file has one implementation for each side.
|
||||
//? if >=1.21.6 {
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix3x2f;
|
||||
import org.joml.Matrix3x2fStack;
|
||||
import org.joml.Quaternionf;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.common.ref.asset.SpriteRef;
|
||||
import org.saturnclient.common.ref.game.ItemStackRef;
|
||||
import org.saturnclient.common.ref.render.MatrixStackRef;
|
||||
import org.saturnclient.impl.ref.Matrix3x2fStackRef;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
import org.saturnclient.ui.resources.SvgTexture;
|
||||
|
||||
import net.minecraft.client.gl.RenderPipelines;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
import net.minecraft.client.gui.ScreenRect;
|
||||
import net.minecraft.client.gui.render.state.ColoredQuadGuiElementRenderState;
|
||||
import net.minecraft.client.gui.render.state.GuiRenderState;
|
||||
import net.minecraft.client.gui.render.state.ItemGuiElementRenderState;
|
||||
import net.minecraft.client.gui.render.state.TextGuiElementRenderState;
|
||||
import net.minecraft.client.gui.render.state.TexturedQuadGuiElementRenderState;
|
||||
import net.minecraft.client.gui.render.state.special.EntityGuiElementRenderState;
|
||||
import net.minecraft.client.render.entity.state.EntityRenderState;
|
||||
//? if >=1.21.7 {
|
||||
import net.minecraft.client.render.item.KeyedItemRenderState;
|
||||
//?} else
|
||||
//import net.minecraft.client.render.item.ItemRenderState;
|
||||
import net.minecraft.client.texture.AbstractTexture;
|
||||
import net.minecraft.client.texture.Sprite;
|
||||
import net.minecraft.client.texture.TextureSetup;
|
||||
import net.minecraft.entity.LivingEntity;
|
||||
import net.minecraft.item.ItemDisplayContext;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.crash.CrashException;
|
||||
import net.minecraft.util.crash.CrashReport;
|
||||
import net.minecraft.util.crash.CrashReportSection;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
public class RenderScopeImpl implements RenderScope {
|
||||
|
||||
public final Matrix3x2fStackRef matrices;
|
||||
public final GuiRenderState state;
|
||||
|
||||
private ScissorStack scissorStack;
|
||||
private int opacity = 0xFF << 24;
|
||||
// The glass blurs made for this scope's frame
|
||||
private final Set<Integer> capturedBlurs = new HashSet<>();
|
||||
|
||||
public RenderScopeImpl(DrawContext context) {
|
||||
this(context.getMatrices(), ((DrawContextAccessor) (Object) context).getState());
|
||||
}
|
||||
|
||||
public RenderScopeImpl(Matrix3x2fStack matrices, GuiRenderState state) {
|
||||
this.matrices = new Matrix3x2fStackRef(matrices);
|
||||
this.state = state;
|
||||
this.scissorStack = new ScissorStack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MatrixStackRef getMatrixStack() {
|
||||
return matrices;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOpacity(float alpha) {
|
||||
this.opacity = ((int) (alpha * 255)) << 24;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColor(int color) {
|
||||
int originalAlpha = (color >>> 24) & 0xFF;
|
||||
int newAlpha = (opacity >>> 24) & 0xFF;
|
||||
int mixedAlpha = (originalAlpha * newAlpha) / 255;
|
||||
return (mixedAlpha << 24) | (color & 0x00FFFFFF);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fill(int x1, int y1, int x2, int y2, int color) {
|
||||
this.state.addSimpleElement(
|
||||
new ColoredQuadGuiElementRenderState(
|
||||
RenderPipelines.GUI,
|
||||
TextureSetup.empty(),
|
||||
new Matrix3x2f(this.matrices.stack),
|
||||
x1, y1, x2, y2, color, color,
|
||||
this.scissorStack.peekLast()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawRectangle(int x, int y, int width, int height, int color) {
|
||||
if (color == 0)
|
||||
return;
|
||||
color = getColor(color);
|
||||
|
||||
int x1 = x;
|
||||
int x2 = x + width;
|
||||
int y1 = y;
|
||||
int y2 = y + height;
|
||||
|
||||
if (x2 < x1) {
|
||||
int tmp = x1;
|
||||
x1 = x2;
|
||||
x2 = tmp;
|
||||
}
|
||||
if (y2 < y1) {
|
||||
int tmp = y1;
|
||||
y1 = y2;
|
||||
y2 = tmp;
|
||||
}
|
||||
|
||||
this.fill(x1, y1, x2, y2, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawText(String text, int x, int y, int font, int color) {
|
||||
drawText(1.0f, text, x, y, font, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawText(float scale, String text, int x, int y, int font, int color) {
|
||||
if (color == 0)
|
||||
return;
|
||||
if (font == 0)
|
||||
scale *= 2;
|
||||
|
||||
int lineIndex = 0;
|
||||
for (String line : text.split("\n")) {
|
||||
int finalColor = getColor(color);
|
||||
|
||||
matrices.push();
|
||||
matrices.translate(x, y + (lineIndex * Fonts.getHeight()));
|
||||
matrices.scale(scale, scale);
|
||||
|
||||
this.state.addText(
|
||||
new TextGuiElementRenderState(
|
||||
SaturnClient.client.textRenderer,
|
||||
((Text) Fonts.setFont(line, font)).asOrderedText(),
|
||||
new Matrix3x2f(this.matrices.stack),
|
||||
0, font == 0 ? 1 : 8,
|
||||
//? if >=1.21.11 {
|
||||
finalColor, 0, false, false, this.scissorStack.peekLast()));
|
||||
//?} else
|
||||
//finalColor, 0, false, this.scissorStack.peekLast()));
|
||||
|
||||
matrices.pop();
|
||||
|
||||
lineIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScaledWindowWidth() {
|
||||
return SaturnClient.client.getWindow().getScaledWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScaledWindowHeight() {
|
||||
return SaturnClient.client.getWindow().getScaledHeight();
|
||||
}
|
||||
|
||||
// Texture rendering
|
||||
@Override
|
||||
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height) {
|
||||
drawTexture(sprite, x, y, u, v, width, height, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height, int color) {
|
||||
drawTexture(sprite, x, y, u, v, width, height, width, height, width, height, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height,
|
||||
int regionWidth, int regionHeight, int textureWidth, int textureHeight) {
|
||||
drawTexture(sprite, x, y, u, v, width, height, regionWidth, regionHeight, textureWidth, textureHeight, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height,
|
||||
int regionWidth, int regionHeight, int textureWidth, int textureHeight, int color) {
|
||||
drawTexturedQuad((Identifier) (Object) sprite, x, x + width, y, y + height,
|
||||
(u + 0f) / textureWidth,
|
||||
(u + regionWidth) / textureWidth,
|
||||
(v + 0f) / textureHeight,
|
||||
(v + regionHeight) / textureHeight,
|
||||
color);
|
||||
}
|
||||
|
||||
private void drawTexturedQuad(Identifier sprite, int x1, int x2, int y1, int y2,
|
||||
float u1, float u2, float v1, float v2, int color) {
|
||||
if (color == 0)
|
||||
return;
|
||||
color = getColor(color);
|
||||
|
||||
if (sprite.getPath().endsWith(".svg")) {
|
||||
sprite = (Identifier) (Object) SvgTexture.getSvg(
|
||||
(IdentifierRef) (Object) sprite,
|
||||
(x2 - x1) * 2, (y2 - y1) * 2);
|
||||
}
|
||||
|
||||
GpuTextureView gpuTextureView = SaturnClient.client.getTextureManager()
|
||||
.getTexture(sprite).getGlTextureView();
|
||||
|
||||
//? if >=1.21.11
|
||||
AbstractTexture abstractTexture = SaturnClient.client.getTextureManager().getTexture(sprite);
|
||||
|
||||
this.state.addSimpleElement(new TexturedQuadGuiElementRenderState(
|
||||
RenderPipelines.GUI_TEXTURED,
|
||||
//? if >=1.21.11 {
|
||||
TextureSetup.of(gpuTextureView, abstractTexture.getSampler()),
|
||||
//?} else
|
||||
//TextureSetup.withoutGlTexture(gpuTextureView),
|
||||
new Matrix3x2f(this.matrices.stack),
|
||||
x1, y1, x2, y2, u1, u2, v1, v2, color,
|
||||
this.scissorStack.peekLast()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawGlass(IdentifierRef mask, int x, int y, int width, int height, int blur) {
|
||||
int color = getColor(0xFFFFFFFF);
|
||||
if (mask == null || color >>> 24 == 0)
|
||||
return;
|
||||
|
||||
if (capturedBlurs.add(blur)) {
|
||||
GlassRenderer.captureBackdrop(blur);
|
||||
}
|
||||
|
||||
AbstractTexture maskTexture = SaturnClient.client.getTextureManager().getTexture((Identifier) (Object) mask);
|
||||
this.state.addSimpleElement(new TexturedQuadGuiElementRenderState(
|
||||
GlassRenderer.PIPELINE,
|
||||
GlassRenderer.textures(maskTexture, blur),
|
||||
new Matrix3x2f(this.matrices.stack),
|
||||
x, y, x + width, y + height, 0, 1, 0, 1, color,
|
||||
this.scissorStack.peekLast()));
|
||||
}
|
||||
|
||||
// Scissor management
|
||||
@Override
|
||||
public void enableScissor(int x1, int y1, int x2, int y2) {
|
||||
ScreenRect rect = new ScreenRect(x1, y1, x2 - x1, y2 - y1).transform(this.matrices.stack);
|
||||
scissorStack.push(rect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableScissor() {
|
||||
scissorStack.pop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean scissorContains(int x, int y) {
|
||||
return scissorStack.contains(x, y);
|
||||
}
|
||||
|
||||
public static class ScissorStack {
|
||||
private final Deque<ScreenRect> stack = new ArrayDeque<>();
|
||||
|
||||
public ScreenRect push(ScreenRect rect) {
|
||||
ScreenRect last = stack.peekLast();
|
||||
if (last != null) {
|
||||
ScreenRect intersection = Objects.requireNonNullElse(rect.intersection(last), ScreenRect.empty());
|
||||
stack.addLast(intersection);
|
||||
return intersection;
|
||||
} else {
|
||||
stack.addLast(rect);
|
||||
return rect;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ScreenRect pop() {
|
||||
if (stack.isEmpty())
|
||||
throw new IllegalStateException("Scissor stack underflow");
|
||||
stack.removeLast();
|
||||
return stack.peekLast();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ScreenRect peekLast() {
|
||||
return stack.peekLast();
|
||||
}
|
||||
|
||||
public boolean contains(int x, int y) {
|
||||
return stack.isEmpty() || stack.peekLast().contains(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
// Item rendering
|
||||
@Override
|
||||
public void drawItem(ItemStackRef stack, int x, int y) {
|
||||
drawItem(null, SaturnClient.client.world, stack, x, y, 0);
|
||||
}
|
||||
|
||||
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStackRef stack, int x, int y,
|
||||
int seed) {
|
||||
drawItem(entity, world, (ItemStack) (Object) stack, x, y, seed, 0);
|
||||
}
|
||||
|
||||
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed,
|
||||
int z) {
|
||||
if (!stack.isEmpty()) {
|
||||
//? if >=1.21.7 {
|
||||
KeyedItemRenderState keyedItemRenderState = new KeyedItemRenderState();
|
||||
//?} else
|
||||
//ItemRenderState keyedItemRenderState = new ItemRenderState();
|
||||
SaturnClient.client.getItemModelManager().clearAndUpdate(keyedItemRenderState, stack,
|
||||
ItemDisplayContext.GUI, world,
|
||||
entity, seed);
|
||||
|
||||
try {
|
||||
this.state.addItem(new ItemGuiElementRenderState(stack.getItem().getName().toString(),
|
||||
new Matrix3x2f(this.matrices.stack), keyedItemRenderState, x, y,
|
||||
this.scissorStack.peekLast()));
|
||||
} catch (Throwable throwable) {
|
||||
CrashReport crashReport = CrashReport.create(throwable, "Rendering item");
|
||||
CrashReportSection crashReportSection = crashReport.addElement("Item being rendered");
|
||||
crashReportSection.add("Item Type", () -> String.valueOf(stack.getItem()));
|
||||
crashReportSection.add("Item Components", () -> String.valueOf(stack.getComponents()));
|
||||
crashReportSection.add("Item Foil", () -> String.valueOf(stack.hasGlint()));
|
||||
throw new CrashException(crashReport);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawSpriteStretched(SpriteRef sprite, int x, int y, int width, int height) {
|
||||
drawSpriteStretched(sprite, x, y, width, height, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawSpriteStretched(SpriteRef sprite, int x, int y, int width, int height, int color) {
|
||||
Sprite mcSprite = (Sprite) sprite;
|
||||
if (color == 0 || width == 0 || height == 0)
|
||||
return;
|
||||
|
||||
drawTexturedQuad(mcSprite.getAtlasId(), x, x + width, y, y + height,
|
||||
mcSprite.getMinU(), mcSprite.getMaxU(), mcSprite.getMinV(), mcSprite.getMaxV(), color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw() {
|
||||
// No-op for now
|
||||
}
|
||||
|
||||
public void drawBorder(int x, int y, int width, int height, int color) {
|
||||
fill(x, y, x + width, y + 1, color);
|
||||
fill(x, y + height - 1, x + width, y + height, color);
|
||||
fill(x, y + 1, x + 1, y + height - 1, color);
|
||||
fill(x + width - 1, y + 1, x + width, y + height - 1, color);
|
||||
}
|
||||
|
||||
public void addEntity(EntityRenderState entityState, float scale, Vector3f translation, Quaternionf rotation,
|
||||
@Nullable Quaternionf overrideCameraAngle, int x1, int y1, int x2, int y2) {
|
||||
|
||||
Matrix3x2f mat = this.matrices.stack;
|
||||
float offsetX = mat.m20;
|
||||
float offsetY = mat.m21;
|
||||
float scaleX = mat.m00; // current matrix X scale
|
||||
float scaleY = mat.m11; // current matrix Y scale
|
||||
|
||||
int adjustedX1 = (int) (x1 * scaleX + offsetX);
|
||||
int adjustedY1 = (int) (y1 * scaleY + offsetY);
|
||||
int adjustedX2 = (int) (x2 * scaleX + offsetX);
|
||||
int adjustedY2 = (int) (y2 * scaleY + offsetY);
|
||||
|
||||
float adjustedScale = scale * scaleX; // apply matrix scale to entity scale too
|
||||
|
||||
this.state.addSpecialElement(new EntityGuiElementRenderState(entityState, translation, rotation,
|
||||
overrideCameraAngle, adjustedX1, adjustedY1, adjustedX2, adjustedY2, adjustedScale,
|
||||
this.scissorStack.peekLast()));
|
||||
}
|
||||
}
|
||||
//?} else {
|
||||
/*import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.common.ref.asset.SpriteRef;
|
||||
import org.saturnclient.common.ref.game.ItemStackRef;
|
||||
import org.saturnclient.common.ref.render.MatrixStackRef;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
import org.saturnclient.ui.resources.SvgTexture;
|
||||
|
||||
import net.minecraft.client.font.TextRenderer;
|
||||
import net.minecraft.client.font.TextRenderer.TextLayerType;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
import net.minecraft.client.gui.ScreenRect;
|
||||
import net.minecraft.client.render.DiffuseLighting;
|
||||
import net.minecraft.client.render.OverlayTexture;
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
import net.minecraft.client.render.VertexConsumer;
|
||||
import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.render.item.ItemRenderState;
|
||||
import net.minecraft.client.texture.Sprite;
|
||||
import net.minecraft.client.util.Window;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.entity.LivingEntity;
|
||||
//? if >=1.21.5
|
||||
import net.minecraft.item.ItemDisplayContext;
|
||||
//? if <1.21.5
|
||||
//import net.minecraft.item.ModelTransformationMode;
|
||||
import net.minecraft.item.ItemStack;
|
||||
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.crash.CrashException;
|
||||
import net.minecraft.util.crash.CrashReport;
|
||||
import net.minecraft.util.crash.CrashReportSection;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
public class RenderScopeImpl implements RenderScope {
|
||||
// ----------------------------
|
||||
// Fields
|
||||
// ----------------------------
|
||||
public MatrixStack matrices;
|
||||
public VertexConsumerProvider.Immediate vertexConsumers;
|
||||
private ScissorStack scissorStack = new ScissorStack();
|
||||
private int opacity = 255 << 24;
|
||||
// The glass blurs made for this scope's frame
|
||||
private final Set<Integer> capturedBlurs = new HashSet<>();
|
||||
private final ItemRenderState itemRenderState;
|
||||
|
||||
// ----------------------------
|
||||
// Constructors
|
||||
// ----------------------------
|
||||
public RenderScopeImpl(DrawContext context) {
|
||||
this(context.getMatrices(), ((DrawContextAccessor) context).getVertexConsumers());
|
||||
}
|
||||
|
||||
public RenderScopeImpl(MatrixStack matrices, VertexConsumerProvider.Immediate vertexConsumers) {
|
||||
this.matrices = matrices;
|
||||
this.vertexConsumers = vertexConsumers;
|
||||
this.scissorStack = new ScissorStack();
|
||||
this.itemRenderState = new ItemRenderState();
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Matrix & Transform
|
||||
// ----------------------------
|
||||
@Override
|
||||
public MatrixStackRef getMatrixStack() {
|
||||
return (MatrixStackRef) matrices;
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Color & Opacity
|
||||
// ----------------------------
|
||||
@Override
|
||||
public void setOpacity(float alpha) {
|
||||
this.opacity = (int) (alpha * 255) << 24;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColor(int color) {
|
||||
int originalAlpha = (color >>> 24) & 0xFF;
|
||||
int newAlpha = (opacity >>> 24) & 0xFF;
|
||||
int mixedAlpha = (originalAlpha * newAlpha) / 255;
|
||||
return (mixedAlpha << 24) | (color & 0x00FFFFFF);
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Draw / Window Info
|
||||
// ----------------------------
|
||||
@Override
|
||||
public void draw() {
|
||||
this.vertexConsumers.draw();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScaledWindowWidth() {
|
||||
return SaturnClient.client.getWindow().getScaledWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScaledWindowHeight() {
|
||||
return SaturnClient.client.getWindow().getScaledHeight();
|
||||
}
|
||||
|
||||
public void draw(Consumer<VertexConsumerProvider> drawer) {
|
||||
drawer.accept(this.vertexConsumers);
|
||||
this.vertexConsumers.draw();
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Shapes
|
||||
// ----------------------------
|
||||
@Override
|
||||
public void drawRectangle(int x, int y, int width, int height, int color) {
|
||||
if (color == 0)
|
||||
return;
|
||||
color = getColor(color);
|
||||
|
||||
Matrix4f matrix4f = this.matrices.peek().getPositionMatrix();
|
||||
|
||||
int x1 = x;
|
||||
int x2 = x + width;
|
||||
int y1 = y;
|
||||
int y2 = y + height;
|
||||
|
||||
if (x1 < x2) {
|
||||
int tmp = x1;
|
||||
x1 = x2;
|
||||
x2 = tmp;
|
||||
}
|
||||
if (y1 < y2) {
|
||||
int tmp = y1;
|
||||
y1 = y2;
|
||||
y2 = tmp;
|
||||
}
|
||||
|
||||
VertexConsumer vertexConsumer = this.vertexConsumers.getBuffer(RenderLayer.getGui());
|
||||
vertexConsumer.vertex(matrix4f, x1, y1, 0).color(color);
|
||||
vertexConsumer.vertex(matrix4f, x1, y2, 0).color(color);
|
||||
vertexConsumer.vertex(matrix4f, x2, y2, 0).color(color);
|
||||
vertexConsumer.vertex(matrix4f, x2, y1, 0).color(color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fill(int x1, int y1, int x2, int y2, int color) {
|
||||
fill(x1, y1, x2, y2, 0, color);
|
||||
}
|
||||
|
||||
public void fill(int x1, int y1, int x2, int y2, int z, int color) {
|
||||
fill(RenderLayer.getGui(), x1, y1, x2, y2, z, color);
|
||||
}
|
||||
|
||||
public void fill(RenderLayer layer, int x1, int y1, int x2, int y2, int color) {
|
||||
fill(layer, x1, y1, x2, y2, 0, color);
|
||||
}
|
||||
|
||||
public void fill(RenderLayer layer, int x1, int y1, int x2, int y2, int z, int color) {
|
||||
Matrix4f matrix4f = this.matrices.peek().getPositionMatrix();
|
||||
if (x1 < x2) {
|
||||
int tmp = x1;
|
||||
x1 = x2;
|
||||
x2 = tmp;
|
||||
}
|
||||
if (y1 < y2) {
|
||||
int tmp = y1;
|
||||
y1 = y2;
|
||||
y2 = tmp;
|
||||
}
|
||||
|
||||
VertexConsumer vertexConsumer = this.vertexConsumers.getBuffer(layer);
|
||||
vertexConsumer.vertex(matrix4f, x1, y1, z).color(color);
|
||||
vertexConsumer.vertex(matrix4f, x1, y2, z).color(color);
|
||||
vertexConsumer.vertex(matrix4f, x2, y2, z).color(color);
|
||||
vertexConsumer.vertex(matrix4f, x2, y1, z).color(color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawBorder(int x, int y, int width, int height, int color) {
|
||||
fill(x, y, x + width, y + 1, color);
|
||||
fill(x, y + height - 1, x + width, y + height, color);
|
||||
fill(x, y + 1, x + 1, y + height - 1, color);
|
||||
fill(x + width - 1, y + 1, x + width, y + height - 1, color);
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Text
|
||||
// ----------------------------
|
||||
@Override
|
||||
public void drawText(String text, int x, int y, int font, int color) {
|
||||
drawText(1.0f, text, x, y, font, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawText(float scale, String text, int x, int y, int font, int color) {
|
||||
if (color == 0)
|
||||
return;
|
||||
if (font == 0)
|
||||
scale *= 2;
|
||||
|
||||
int i = 0;
|
||||
for (String line : text.split("\n")) {
|
||||
color = getColor(color);
|
||||
matrices.push();
|
||||
matrices.translate(x, y + i * Fonts.getHeight(), 0);
|
||||
matrices.scale(scale, scale, 1.0f);
|
||||
TextRenderer textRenderer = SaturnClient.client.textRenderer;
|
||||
textRenderer.draw((Text) Fonts.setFont(line, font), 0, font == 0 ? 1 : 7, color, false,
|
||||
matrices.peek().getPositionMatrix(), vertexConsumers, TextLayerType.NORMAL, 0, 15728880);
|
||||
matrices.pop();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Textures & Sprites
|
||||
// ----------------------------
|
||||
@Override
|
||||
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height) {
|
||||
drawTexture(sprite, x, y, u, v, width, height, width, height, width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height, int color) {
|
||||
drawTexture(sprite, x, y, u, v, width, height, width, height, width, height, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height,
|
||||
int regionWidth, int regionHeight, int textureWidth, int textureHeight) {
|
||||
drawTexture(sprite, x, y, u, v, width, height, regionWidth, regionHeight, textureWidth, textureHeight, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawTexture(IdentifierRef sprite, int x, int y, float u, float v, int width, int height,
|
||||
int regionWidth, int regionHeight, int textureWidth, int textureHeight, int color) {
|
||||
drawTexturedQuad((Identifier) (Object) sprite, x, x + width, y, y + height,
|
||||
(u + 0.0f) / textureWidth, (u + regionWidth) / textureWidth,
|
||||
(v + 0.0f) / textureHeight, (v + regionHeight) / textureHeight, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawSpriteStretched(SpriteRef sprite, int x, int y, int width, int height) {
|
||||
drawSpriteStretched(sprite, x, y, width, height, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawSpriteStretched(SpriteRef saturnSprite, int x, int y, int width, int height, int color) {
|
||||
Sprite sprite = (Sprite) saturnSprite;
|
||||
if (color == 0)
|
||||
return;
|
||||
if (width != 0 && height != 0) {
|
||||
drawTexturedQuad(sprite.getAtlasId(), x, x + width, y, y + height,
|
||||
sprite.getMinU(), sprite.getMaxU(), sprite.getMinV(), sprite.getMaxV(), color);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawTexturedQuad(Identifier sprite, int x1, int x2, int y1, int y2,
|
||||
float u1, float u2, float v1, float v2, int color) {
|
||||
if (sprite.toString().endsWith(".svg")) {
|
||||
sprite = (Identifier) (Object) SvgTexture.getSvg((IdentifierRef) (Object) sprite, (x2 - x1) * 2,
|
||||
(y2 - y1) * 2);
|
||||
}
|
||||
if (color == 0)
|
||||
return;
|
||||
|
||||
x1 *= 4;
|
||||
x2 *= 4;
|
||||
y1 *= 4;
|
||||
y2 *= 4;
|
||||
color = getColor(color);
|
||||
|
||||
matrices.push();
|
||||
matrices.scale(0.25f, 0.25f, 1.0f);
|
||||
//? if <1.21.5
|
||||
//RenderSystem.setShaderTexture(0, sprite);
|
||||
RenderLayer renderLayer = RenderLayer.getGuiTextured(sprite);
|
||||
Matrix4f matrix4f = matrices.peek().getPositionMatrix();
|
||||
VertexConsumer vertexConsumer = vertexConsumers.getBuffer(renderLayer);
|
||||
vertexConsumer.vertex(matrix4f, x1, y1, 0).texture(u1, v1).color(color);
|
||||
vertexConsumer.vertex(matrix4f, x1, y2, 0).texture(u1, v2).color(color);
|
||||
vertexConsumer.vertex(matrix4f, x2, y2, 0).texture(u2, v2).color(color);
|
||||
vertexConsumer.vertex(matrix4f, x2, y1, 0).texture(u2, v1).color(color);
|
||||
matrices.pop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawGlass(IdentifierRef mask, int x, int y, int width, int height, int blur) {
|
||||
int color = getColor(0xFFFFFFFF);
|
||||
if (mask == null || color >>> 24 == 0)
|
||||
return;
|
||||
|
||||
if (capturedBlurs.add(blur)) {
|
||||
GlassRenderer.captureBackdrop(blur);
|
||||
}
|
||||
|
||||
// In quarter units, as drawTexturedQuad draws
|
||||
int x1 = x * 4;
|
||||
int x2 = (x + width) * 4;
|
||||
int y1 = y * 4;
|
||||
int y2 = (y + height) * 4;
|
||||
|
||||
matrices.push();
|
||||
matrices.scale(0.25f, 0.25f, 1.0f);
|
||||
Matrix4f matrix4f = matrices.peek().getPositionMatrix();
|
||||
VertexConsumer vertexConsumer = vertexConsumers.getBuffer(GlassRenderer.getLayer((Identifier) (Object) mask, blur));
|
||||
vertexConsumer.vertex(matrix4f, x1, y1, 0).texture(0, 0).color(color);
|
||||
vertexConsumer.vertex(matrix4f, x1, y2, 0).texture(0, 1).color(color);
|
||||
vertexConsumer.vertex(matrix4f, x2, y2, 0).texture(1, 1).color(color);
|
||||
vertexConsumer.vertex(matrix4f, x2, y1, 0).texture(1, 0).color(color);
|
||||
matrices.pop();
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Items
|
||||
// ----------------------------
|
||||
@Override
|
||||
public void drawItem(ItemStackRef item, int x, int y) {
|
||||
drawItem(SaturnClient.client.player, SaturnClient.client.world, item, x, y, 0);
|
||||
}
|
||||
|
||||
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStackRef stack, int x, int y,
|
||||
int seed) {
|
||||
drawItem(entity, world, (ItemStack) (Object) stack, x, y, seed, 0);
|
||||
}
|
||||
|
||||
private void drawItem(@Nullable LivingEntity entity, @Nullable World world, ItemStack stack, int x, int y, int seed,
|
||||
int z) {
|
||||
if (stack.isEmpty())
|
||||
return;
|
||||
|
||||
//? if >=1.21.5
|
||||
SaturnClient.client.getItemModelManager().update(itemRenderState, stack, ItemDisplayContext.GUI, world, entity, seed);
|
||||
//? if >=1.21.5
|
||||
SaturnClient.client.getItemModelManager().clearAndUpdate(this.itemRenderState, stack, ItemDisplayContext.GUI, world, entity, seed);
|
||||
//? if <1.21.5
|
||||
//SaturnClient.client.getItemModelManager().update(itemRenderState, stack, ModelTransformationMode.GUI, false, world, entity, seed);
|
||||
|
||||
this.matrices.push();
|
||||
//? if >=1.21.5
|
||||
this.matrices.translate((float) (x + 8), (float) (y + 8), (float) (150 + z));
|
||||
//? if <1.21.5
|
||||
//this.matrices.translate(x + 8, y + 8, 150 + (itemRenderState.hasDepth() ? z : 0));
|
||||
|
||||
try {
|
||||
this.matrices.scale(16.0F, -16.0F, 16.0F);
|
||||
boolean bl = !this.itemRenderState.isSideLit();
|
||||
if (bl) {
|
||||
this.draw();
|
||||
DiffuseLighting.disableGuiDepthLighting();
|
||||
}
|
||||
|
||||
this.itemRenderState.render(this.matrices, this.vertexConsumers, 15728880, OverlayTexture.DEFAULT_UV);
|
||||
this.draw();
|
||||
if (bl) {
|
||||
DiffuseLighting.enableGuiDepthLighting();
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
CrashReport crashReport = CrashReport.create(throwable, "Rendering item");
|
||||
CrashReportSection crashReportSection = crashReport.addElement("Item being rendered");
|
||||
crashReportSection.add("Item Type", () -> String.valueOf(stack.getItem()));
|
||||
crashReportSection.add("Item Components", () -> String.valueOf(stack.getComponents()));
|
||||
crashReportSection.add("Item Foil", () -> String.valueOf(stack.hasGlint()));
|
||||
throw new CrashException(crashReport);
|
||||
}
|
||||
|
||||
this.matrices.pop();
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Scissor / Clipping
|
||||
// ----------------------------
|
||||
@Override
|
||||
public void enableScissor(int x1, int y1, int x2, int y2) {
|
||||
ScreenRect screenRect = new ScreenRect(x1, y1, x2 - x1, y2 - y1)
|
||||
.transform(matrices.peek().getPositionMatrix());
|
||||
setScissor(scissorStack.push(screenRect));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableScissor() {
|
||||
setScissor(scissorStack.pop());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean scissorContains(int x, int y) {
|
||||
return scissorStack.containsPoint(x, y);
|
||||
}
|
||||
|
||||
private void setScissor(@Nullable ScreenRect rect) {
|
||||
draw();
|
||||
if (rect != null) {
|
||||
Window window = SaturnClient.client.getWindow();
|
||||
int framebufferHeight = window.getFramebufferHeight();
|
||||
double scaleFactor = window.getScaleFactor();
|
||||
RenderSystem.enableScissor(
|
||||
(int) (rect.getLeft() * scaleFactor),
|
||||
(int) (framebufferHeight - rect.getBottom() * scaleFactor),
|
||||
Math.max(0, (int) (rect.width() * scaleFactor)),
|
||||
Math.max(0, (int) (rect.height() * scaleFactor)));
|
||||
} else {
|
||||
RenderSystem.disableScissor();
|
||||
}
|
||||
}
|
||||
|
||||
static class ScissorStack {
|
||||
private final Deque<ScreenRect> stack = new ArrayDeque<>();
|
||||
|
||||
public ScreenRect push(ScreenRect p_281812_) {
|
||||
ScreenRect screenrectangle = this.stack.peekLast();
|
||||
if (screenrectangle != null) {
|
||||
ScreenRect screenrectangle1 = Objects.requireNonNullElse(p_281812_.intersection(screenrectangle),
|
||||
ScreenRect.empty());
|
||||
this.stack.addLast(screenrectangle1);
|
||||
return screenrectangle1;
|
||||
} else {
|
||||
this.stack.addLast(p_281812_);
|
||||
return p_281812_;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ScreenRect pop() {
|
||||
if (this.stack.isEmpty()) {
|
||||
throw new IllegalStateException("Scissor stack underflow");
|
||||
} else {
|
||||
this.stack.removeLast();
|
||||
return this.stack.peekLast();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean containsPoint(int p_329411_, int p_333404_) {
|
||||
return this.stack.isEmpty() ? true : this.stack.peek().contains(p_329411_, p_333404_);
|
||||
}
|
||||
}
|
||||
}
|
||||
*///?}
|
||||
@@ -0,0 +1,264 @@
|
||||
package org.saturnclient.impl.ui;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.saturnclient.config.Config;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.SaturnScreen;
|
||||
import org.saturnclient.ui.SaturnScreen.ScreenProvider;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
//? if >=1.21.9
|
||||
import net.minecraft.client.gui.Click;
|
||||
import net.minecraft.client.gui.CubeMapRenderer;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
import net.minecraft.client.gui.RotatingCubeMapRenderer;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
//? if >=1.21.6 {
|
||||
import net.minecraft.client.texture.CubemapTexture;
|
||||
//?} else {
|
||||
/*import net.minecraft.client.texture.ResourceTexture;
|
||||
import net.minecraft.client.texture.TextureManager;
|
||||
*///?}
|
||||
//? if >=1.21.9
|
||||
import net.minecraft.client.input.KeyInput;
|
||||
//? if <1.21.6 {
|
||||
/*import net.minecraft.client.gl.PostEffectProcessor;
|
||||
import net.minecraft.client.render.DefaultFramebufferSet;
|
||||
import net.minecraft.client.util.Pool;
|
||||
*///?}
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public class SaturnScreenFabric extends Screen implements ScreenProvider {
|
||||
protected static final Identifier PANORAMA = Identifier.of("saturnclient",
|
||||
"textures/gui/title/background/panorama");
|
||||
protected static final CubeMapRenderer PANORAMA_RENDERER = new CubeMapRenderer(PANORAMA);
|
||||
|
||||
public static final RotatingCubeMapRenderer ROTATING_PANORAMA_RENDERER;
|
||||
|
||||
static {
|
||||
ROTATING_PANORAMA_RENDERER = new RotatingCubeMapRenderer(PANORAMA_RENDERER);
|
||||
}
|
||||
|
||||
//? if <1.21.6
|
||||
//private final Pool pool = new Pool(3);
|
||||
public final SaturnScreen screen;
|
||||
|
||||
/** Whether the panorama's textures are loaded; it isn't drawn before (e.g. under the splash). */
|
||||
private static volatile boolean panoramaReady = false;
|
||||
|
||||
public static void preload(MinecraftClient client) {
|
||||
//? if >=1.21.6 {
|
||||
// From 1.21.6 CubeMapRenderer draws from one cube map texture made of panorama_0..5.png.
|
||||
// CubeMapRenderer.registerTextures only stores it for the next resource reload, which has
|
||||
// already happened by now, so register it through the overload that loads it right away.
|
||||
client.getTextureManager().registerTexture(PANORAMA, new CubemapTexture(PANORAMA));
|
||||
//?} else {
|
||||
/*TextureManager textureManager = client.getTextureManager();
|
||||
for (int i = 0; i < 6; i++) {
|
||||
String var10001 = PANORAMA.getPath();
|
||||
Identifier tex = PANORAMA.withPath(var10001 + "_" + i + ".png");
|
||||
ResourceTexture resourceTexture = new ResourceTexture(tex);
|
||||
textureManager.registerTexture(tex, resourceTexture);
|
||||
}
|
||||
*///?}
|
||||
panoramaReady = true;
|
||||
}
|
||||
|
||||
public SaturnScreenFabric(SaturnScreen screen) {
|
||||
super(Text.literal(screen.title));
|
||||
screen.provider = this;
|
||||
this.screen = screen;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
screen.init();
|
||||
}
|
||||
|
||||
//? if <1.21.6
|
||||
//@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||
if (screen.start == null) {
|
||||
screen.start = Instant.now();
|
||||
}
|
||||
long elapsed = Duration.between(screen.start, Instant.now()).toMillis();
|
||||
|
||||
screen.updateScale();
|
||||
|
||||
mouseX = (int) (mouseX / screen.scale);
|
||||
mouseY = (int) (mouseY / screen.scale);
|
||||
|
||||
// From 1.21.6 the background, blur included, is drawn in renderBackground.
|
||||
//? if <1.21.6 {
|
||||
/*if (isShowingPanorama()) {
|
||||
ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, screen.backgroundOpacity, delta);
|
||||
}
|
||||
|
||||
// Glass is blurred from the game as it is before this screen blurs it.
|
||||
if (Config.usesGlass()) {
|
||||
context.draw();
|
||||
GlassRenderer.saveBackground();
|
||||
}
|
||||
|
||||
float radius = screen.getBackgroundBlur();
|
||||
PostEffectProcessor postEffectProcessor = radius < 1 ? null : this.client.getShaderLoader().loadPostEffect(
|
||||
Identifier.ofVanilla("blur"),
|
||||
DefaultFramebufferSet.MAIN_ONLY);
|
||||
|
||||
if (postEffectProcessor != null) {
|
||||
//? if >=1.21.5
|
||||
postEffectProcessor.render(this.client.getFramebuffer(), this.pool, pass -> pass.setUniform("Radius", radius));
|
||||
//? if <1.21.5
|
||||
//postEffectProcessor.setUniforms("Radius", radius);
|
||||
//? if <1.21.5
|
||||
//postEffectProcessor.render(this.client.getFramebuffer(), this.pool);
|
||||
}
|
||||
*///?}
|
||||
|
||||
RenderScope renderScope = new RenderScopeImpl(context);
|
||||
|
||||
screen.render(renderScope, mouseX, mouseY, delta, elapsed);
|
||||
}
|
||||
|
||||
//? if >=1.21.6 {
|
||||
// Vanilla's background would cover Saturn's panorama, and blurs by the vanilla setting. The
|
||||
// strength of this blur comes from GameRendererMixin, which gives Saturn's setting instead.
|
||||
@Override
|
||||
public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||
// The panorama renderer has no opacity (and takes no delta) from 1.21.6, so it's drawn fully
|
||||
// opaque.
|
||||
if (isShowingPanorama()) {
|
||||
ROTATING_PANORAMA_RENDERER.render(context, this.width, this.height, true);
|
||||
}
|
||||
|
||||
if (screen.getBackgroundBlur() >= 1) {
|
||||
context.applyBlur();
|
||||
}
|
||||
|
||||
if (client.world != null) {
|
||||
renderDarkening(context);
|
||||
}
|
||||
}
|
||||
//?}
|
||||
|
||||
/** Whether Saturn's panorama is behind the screen: outside a world, once it's loaded. */
|
||||
private boolean isShowingPanorama() {
|
||||
return panoramaReady && client.world == null && client.getCurrentServerEntry() == null;
|
||||
}
|
||||
|
||||
// 1.21.9 moved mouse and key events to Click and KeyInput.
|
||||
//? if >=1.21.9 {
|
||||
@Override
|
||||
public boolean mouseClicked(Click click, boolean doubled) {
|
||||
if (screen.mouseClicked(click.x(), click.y(), click.button())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.mouseClicked(click, doubled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseDragged(Click click, double offsetX, double offsetY) {
|
||||
// offsetX and offsetY are the drag deltas, as mouseDragged's deltaX and deltaY were before 1.21.9.
|
||||
if (screen.mouseDragged(click.x(), click.y(), click.button(), offsetX, offsetY)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.mouseDragged(click, offsetX, offsetY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseReleased(Click click) {
|
||||
if (screen.mouseReleased(click.x(), click.y(), click.button())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.mouseReleased(click);
|
||||
}
|
||||
//?} else {
|
||||
/*@Override
|
||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (screen.mouseClicked(mouseX, mouseY, button)) {
|
||||
return true;
|
||||
}
|
||||
return super.mouseClicked(mouseX, mouseY, button);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
|
||||
if (screen.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) {
|
||||
return true;
|
||||
}
|
||||
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseReleased(double mouseX, double mouseY, int button) {
|
||||
if (screen.mouseReleased(mouseX, mouseY, button)) {
|
||||
return true;
|
||||
}
|
||||
return super.mouseReleased(mouseX, mouseY, button);
|
||||
}
|
||||
*///?}
|
||||
|
||||
@Override
|
||||
public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
|
||||
if (screen.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) {
|
||||
return true;
|
||||
}
|
||||
return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount);
|
||||
}
|
||||
|
||||
//? if >=1.21.9 {
|
||||
@Override
|
||||
public boolean keyPressed(KeyInput input) {
|
||||
if (screen.keyPressed(input.getKeycode(), input.scancode(), input.modifiers())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.keyPressed(input);
|
||||
}
|
||||
//?} else {
|
||||
/*@Override
|
||||
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||
if (screen.keyPressed(keyCode, scanCode, modifiers)) {
|
||||
return true;
|
||||
}
|
||||
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||
}
|
||||
*///?}
|
||||
|
||||
//? if >=1.21.11 {
|
||||
@Override
|
||||
public void resize(int width, int height) {
|
||||
screen.resize(width, height);
|
||||
super.resize(width, height);
|
||||
}
|
||||
//?} else {
|
||||
/*@Override
|
||||
public void resize(MinecraftClient client, int width, int height) {
|
||||
screen.resize(width, height);
|
||||
super.resize(client, width, height);
|
||||
}
|
||||
*///?}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
this.screen.onClose();
|
||||
super.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.saturnclient.modules;
|
||||
|
||||
import org.saturnclient.ui2.RenderScope;
|
||||
|
||||
public interface HudMod {
|
||||
public ModDimensions getDimensions();
|
||||
|
||||
public void renderDummy(RenderScope scope);
|
||||
|
||||
public void renderHud(RenderScope scope);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package org.saturnclient.modules;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.saturnclient.saturnclient.config.NamedProperty;
|
||||
import org.saturnclient.saturnclient.config.Property;
|
||||
|
||||
public class ModDimensions {
|
||||
public Property<Integer> x = Property.integer(0);
|
||||
public Property<Integer> y = Property.integer(0);
|
||||
public Property<Float> scale = Property.floatProp(1.0f);
|
||||
public Property<Integer> bgColor = Property.color(0x00000000);
|
||||
public Property<Integer> fgColor = Property.color(0xFFffffff);
|
||||
public Property<Integer> radius = Property.integer(0);
|
||||
public Property<Integer> font = Property.font(0);
|
||||
public boolean renderBackground = true;
|
||||
|
||||
public int width = 0;
|
||||
public int height = 0;
|
||||
|
||||
public NamedProperty<Map<String, Property<?>>> prop() {
|
||||
return Property.namespace(Map.of(
|
||||
"X", x,
|
||||
"Y", y,
|
||||
"Scale", scale,
|
||||
"Background Color", bgColor,
|
||||
"Foreground Color", fgColor,
|
||||
"Corner Radius", radius,
|
||||
"Font", font
|
||||
)).named("In-Game Display");
|
||||
}
|
||||
|
||||
public ModDimensions() {}
|
||||
|
||||
public ModDimensions(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public ModDimensions(int x, int y, int width, int height) {
|
||||
this.x.value = x;
|
||||
this.y.value = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public ModDimensions(int x, int y, int width, int height, float scale) {
|
||||
this.x.value = x;
|
||||
this.y.value = y;
|
||||
this.scale.value = scale;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package org.saturnclient.modules;
|
||||
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.saturnclient.modules.mods.*;
|
||||
|
||||
public class ModManager {
|
||||
public static final List<Module> ENABLED_MODS = new ArrayList<>();
|
||||
public static final Module[] ALL_MODS = {
|
||||
new Crosshair(),
|
||||
new AutoSprint(),
|
||||
new ArmorDisplay(),
|
||||
new Fps(),
|
||||
new PotionDisplay(),
|
||||
new Coordinates(),
|
||||
new Freelook(),
|
||||
new Keystrokes()
|
||||
};
|
||||
|
||||
public static void init() {
|
||||
// Pre-filter enabled modules to avoid checking every tick
|
||||
updateEnabledModules();
|
||||
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
// Only iterate over enabled modules
|
||||
for (Module m : ENABLED_MODS) {
|
||||
m.tick();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void updateEnabledModules() {
|
||||
ENABLED_MODS.clear();
|
||||
for (Module m : ALL_MODS) {
|
||||
if (m.isEnabled()) {
|
||||
ENABLED_MODS.add(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Module[] getAllMods() {
|
||||
return ALL_MODS;
|
||||
}
|
||||
|
||||
public static void refreshEnabledModules() {
|
||||
updateEnabledModules();
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.saturnclient.modules;
|
||||
|
||||
import org.saturnclient.saturnclient.config.ConfigManager;
|
||||
import org.saturnclient.saturnclient.config.NamedProperty;
|
||||
import org.saturnclient.ui2.resources.Textures;
|
||||
import org.saturnclient.ui2.RenderScope;
|
||||
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public abstract class Module {
|
||||
private final ModuleDetails details;
|
||||
private final ConfigManager configManager;
|
||||
|
||||
public Module(ModuleDetails details, NamedProperty<?>... props) {
|
||||
this.details = details;
|
||||
configManager = new ConfigManager(details.name);
|
||||
for (NamedProperty<?> prop : props) {
|
||||
configManager.property(prop.name, prop.prop);
|
||||
}
|
||||
}
|
||||
|
||||
public void render(RenderScope scope) {}
|
||||
public void tick() {}
|
||||
|
||||
public abstract boolean isEnabled();
|
||||
|
||||
public abstract void setEnabled(boolean e);
|
||||
|
||||
public final String getName() {
|
||||
return details.name;
|
||||
}
|
||||
|
||||
public Identifier getIconTexture() {
|
||||
return Textures.getModIcon(details.namespace);
|
||||
}
|
||||
|
||||
public ConfigManager getConfig() {
|
||||
return configManager;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return details.description;
|
||||
}
|
||||
|
||||
public String[] getTags() {
|
||||
return details.tags;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return details.version;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package org.saturnclient.modules;
|
||||
|
||||
public class ModuleDetails {
|
||||
public String name;
|
||||
public String namespace;
|
||||
public String description;
|
||||
public String version;
|
||||
public String[] tags = {};
|
||||
|
||||
public ModuleDetails(String name, String namespace) {
|
||||
this.name = name;
|
||||
this.namespace = namespace;
|
||||
}
|
||||
|
||||
public ModuleDetails name(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ModuleDetails namespace(String namespace) {
|
||||
this.namespace = namespace;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ModuleDetails description(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ModuleDetails version(String version) {
|
||||
this.version = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ModuleDetails tags(String... tags) {
|
||||
this.tags = tags;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
# Credits
|
||||
### Freelook
|
||||
- https://github.com/Celibistrial/freelook
|
||||
- https://modrinth.com/mod/freelook
|
||||
|
||||
**The other mods are coded by us, if you have a concern or we missed something feel free to submit a issue with the credits tag**
|
||||
@@ -1,102 +0,0 @@
|
||||
package org.saturnclient.modules.mods;
|
||||
|
||||
import org.saturnclient.modules.HudMod;
|
||||
import org.saturnclient.modules.ModDimensions;
|
||||
import org.saturnclient.modules.Module;
|
||||
import org.saturnclient.modules.ModuleDetails;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.saturnclient.config.Property;
|
||||
import org.saturnclient.ui2.RenderScope;
|
||||
|
||||
import net.minecraft.entity.EquipmentSlot;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.item.Items;
|
||||
|
||||
public class ArmorDisplay extends Module implements HudMod {
|
||||
private static Property<Boolean> enabled = Property.bool(false);
|
||||
private static ModDimensions dimensions = new ModDimensions(40, 75);
|
||||
private static Property<Boolean> useMainHand = Property.bool(false);
|
||||
|
||||
public ArmorDisplay() {
|
||||
super(new ModuleDetails("Armor Display", "armor")
|
||||
.description("Displays armor durability")
|
||||
.version("v0.1.0")
|
||||
.tags("Utility"),
|
||||
enabled.named("Enabled"),
|
||||
useMainHand.named("Use Main Hand"),
|
||||
dimensions.prop());
|
||||
}
|
||||
|
||||
public void renderArmor(RenderScope scope, ItemStack mainHand, ItemStack helmet, ItemStack chestplate,
|
||||
ItemStack leggings, ItemStack boots) {
|
||||
int row = 0;
|
||||
|
||||
if (useMainHand.value && !mainHand.isEmpty()) {
|
||||
scope.drawItem(mainHand, 0, 15 * row);
|
||||
renderHealth(scope, row, mainHand.getMaxDamage(), mainHand.getDamage());
|
||||
row++;
|
||||
}
|
||||
|
||||
if (!helmet.isEmpty()) {
|
||||
scope.drawItem(helmet, 0, 15 * row);
|
||||
renderHealth(scope, row, helmet.getMaxDamage(), helmet.getDamage());
|
||||
row++;
|
||||
}
|
||||
|
||||
if (!chestplate.isEmpty()) {
|
||||
scope.drawItem(chestplate, 0, 15 * row);
|
||||
renderHealth(scope, row, chestplate.getMaxDamage(), chestplate.getDamage());
|
||||
row++;
|
||||
}
|
||||
if (!leggings.isEmpty()) {
|
||||
scope.drawItem(leggings, 0, 15 * row);
|
||||
renderHealth(scope, row, leggings.getMaxDamage(), leggings.getDamage());
|
||||
row++;
|
||||
}
|
||||
|
||||
if (!boots.isEmpty()) {
|
||||
scope.drawItem(boots, 0, 15 * row);
|
||||
renderHealth(scope, row, boots.getMaxDamage(), boots.getDamage());
|
||||
row++;
|
||||
}
|
||||
}
|
||||
|
||||
public void renderHealth(RenderScope scope, int i, int max_damage, int damage) {
|
||||
if (max_damage > 0) {
|
||||
scope.drawText(0.5f, "" + (max_damage - damage), 17, (15 * i) + 3, dimensions.font.value,
|
||||
dimensions.fgColor.value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderHud(RenderScope scope) {
|
||||
renderArmor(scope,
|
||||
SaturnClient.client.player.getEquippedStack(EquipmentSlot.MAINHAND),
|
||||
SaturnClient.client.player.getEquippedStack(EquipmentSlot.HEAD),
|
||||
SaturnClient.client.player.getEquippedStack(EquipmentSlot.CHEST),
|
||||
SaturnClient.client.player.getEquippedStack(EquipmentSlot.LEGS),
|
||||
SaturnClient.client.player.getEquippedStack(EquipmentSlot.FEET));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderDummy(RenderScope scope) {
|
||||
renderArmor(scope, new ItemStack(Items.DIAMOND_SWORD), new ItemStack(Items.DIAMOND_HELMET),
|
||||
new ItemStack(Items.DIAMOND_CHESTPLATE),
|
||||
new ItemStack(Items.DIAMOND_LEGGINGS), new ItemStack(Items.DIAMOND_BOOTS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModDimensions getDimensions() {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean e) {
|
||||
enabled.value = e;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package org.saturnclient.modules.mods;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.saturnclient.modules.Module;
|
||||
import org.saturnclient.modules.ModuleDetails;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.saturnclient.config.Property;
|
||||
|
||||
public class AutoSprint extends Module {
|
||||
private static Property<Boolean> enabled = Property.bool(false);
|
||||
|
||||
public AutoSprint() {
|
||||
super(
|
||||
new ModuleDetails("Auto Sprint", "sprint")
|
||||
.description("Makes the player always sprint")
|
||||
.tags("Movement")
|
||||
.version("v0.1.0"),
|
||||
enabled.named("Enabled"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
if (SaturnClient.client.player == null || SaturnClient.client.getNetworkHandler() == null) {
|
||||
return;
|
||||
}
|
||||
if (SaturnClient.client.options.forwardKey.isPressed() && !SaturnClient.client.options.backKey.isPressed() && !SaturnClient.client.player.isSneaking() &&
|
||||
!SaturnClient.client.player.horizontalCollision) {
|
||||
Objects.requireNonNull(SaturnClient.client.player).setSprinting(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean e) {
|
||||
enabled.value = e;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package org.saturnclient.modules.mods;
|
||||
|
||||
import org.saturnclient.modules.HudMod;
|
||||
import org.saturnclient.modules.ModDimensions;
|
||||
import org.saturnclient.modules.Module;
|
||||
import org.saturnclient.modules.ModuleDetails;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.saturnclient.config.Property;
|
||||
import org.saturnclient.ui2.RenderScope;
|
||||
import org.saturnclient.ui2.resources.Fonts;
|
||||
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
|
||||
public class Coordinates extends Module implements HudMod {
|
||||
private static Property<Boolean> enabled = Property.bool(false);
|
||||
private static Property<Integer> displayMethod = Property.select(0, "Flat", "Flat annotated", "Newline", "Newline annotated");
|
||||
private static ModDimensions dimensions = new ModDimensions(40, 18);
|
||||
|
||||
public Coordinates() {
|
||||
super(new ModuleDetails("Coordinates", "coords")
|
||||
.description("Displays your current coordinates")
|
||||
.version("v0.1.0")
|
||||
.tags("Utility"),
|
||||
enabled.named("Enabled"),
|
||||
displayMethod.named("Display Method"),
|
||||
dimensions.prop()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderDummy(RenderScope scope) {
|
||||
renderCoords(532, 69, 253, scope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderHud(RenderScope scope) {
|
||||
Vec3d pos = SaturnClient.client.player.getPos();
|
||||
renderCoords((int) pos.x, (int) pos.y, (int) pos.z, scope);
|
||||
}
|
||||
|
||||
public void renderCoords(int x, int y, int z, RenderScope scope) {
|
||||
String f = "%d %d %d"; // Flat
|
||||
switch (displayMethod.value) {
|
||||
case 1: // Flat annotated
|
||||
f = "X: %d Y: %d Z: %d";
|
||||
break;
|
||||
case 2: // Newline
|
||||
f = "%d\n%d\n%d";
|
||||
break;
|
||||
case 3: // Newline annotated
|
||||
f = "X: %d\nY: %d\nZ: %d";
|
||||
break;
|
||||
}
|
||||
String text = String.format(f, x, y, z);
|
||||
scope.drawText(text,
|
||||
0, 0, dimensions.font.value, dimensions.fgColor.value);
|
||||
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
|
||||
dimensions.height = 18 * text.split("\n").length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModDimensions getDimensions() {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean e) {
|
||||
enabled.value = e;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package org.saturnclient.modules.mods;
|
||||
|
||||
import org.saturnclient.modules.Module;
|
||||
import org.saturnclient.modules.ModuleDetails;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.saturnclient.config.Property;
|
||||
import org.saturnclient.ui2.RenderScope;
|
||||
import org.saturnclient.ui2.resources.Textures;
|
||||
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
|
||||
public class Crosshair extends Module {
|
||||
public static Property<Boolean> enabled = Property.bool(false);
|
||||
|
||||
public Crosshair() {
|
||||
super(
|
||||
new ModuleDetails("Crosshair", "crosshair")
|
||||
.description("Changes the crosshair if you're aiming at a entity")
|
||||
.tags("Visuals", "Utility")
|
||||
.version("v0.1.0"),
|
||||
enabled.named("Enabled"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope scope) {
|
||||
if (enabled.value
|
||||
&& SaturnClient.client.targetedEntity != null
|
||||
&& SaturnClient.client.targetedEntity.isAlive()) {
|
||||
int scaledWidth = 15;
|
||||
int scaledHeight = 15;
|
||||
|
||||
scope.setRenderLayer(RenderLayer::getCrosshair);
|
||||
scope.drawTexture(Textures.CROSSHAIR_RANGE,
|
||||
(scope.getScaledWindowWidth() - scaledWidth) / 2,
|
||||
(scope.getScaledWindowHeight() - scaledHeight) / 2, 0.0F, 0.0F, scaledWidth, scaledHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean e) {
|
||||
enabled.value = e;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package org.saturnclient.modules.mods;
|
||||
|
||||
import org.saturnclient.modules.HudMod;
|
||||
import org.saturnclient.modules.ModDimensions;
|
||||
import org.saturnclient.modules.Module;
|
||||
import org.saturnclient.modules.ModuleDetails;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.saturnclient.config.Property;
|
||||
import org.saturnclient.ui2.RenderScope;
|
||||
import org.saturnclient.ui2.resources.Fonts;
|
||||
|
||||
public class Fps extends Module implements HudMod {
|
||||
private static Property<Boolean> enabled = Property.bool(false);
|
||||
private static ModDimensions dimensions = new ModDimensions(60, Fonts.getHeight());
|
||||
|
||||
public Fps() {
|
||||
super(new ModuleDetails("FPS Display", "fps")
|
||||
.description("Displays current FPS")
|
||||
.version("v0.1.0")
|
||||
.tags("Utility"),
|
||||
enabled.named("Enabled"),
|
||||
dimensions.prop());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderHud(RenderScope scope) {
|
||||
String text = String.valueOf(SaturnClient.client.getCurrentFps()) + " FPS";
|
||||
scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
|
||||
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderDummy(RenderScope scope) {
|
||||
String text = "369 FPS";
|
||||
scope.drawText(text, 0, 0, dimensions.font.value, dimensions.fgColor.value);
|
||||
dimensions.width = Fonts.getWidth(text, dimensions.font.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean e) {
|
||||
enabled.value = e;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModDimensions getDimensions() {
|
||||
return dimensions;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package org.saturnclient.modules.mods;
|
||||
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.saturnclient.modules.Module;
|
||||
import org.saturnclient.modules.ModuleDetails;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.saturnclient.config.Property;
|
||||
|
||||
import net.minecraft.client.option.Perspective;
|
||||
|
||||
public class Freelook extends Module {
|
||||
private static Property<Boolean> enabled = Property.bool(false);
|
||||
private static Property<Boolean> toggle = Property.bool(true);
|
||||
public static Property<Integer> freelookKey = Property.keybinding(GLFW.GLFW_KEY_H);
|
||||
public static boolean isFreeLooking = false;
|
||||
private static boolean was1stPerson = false;
|
||||
|
||||
public Freelook() {
|
||||
super(
|
||||
new ModuleDetails("Freelook", "freelook")
|
||||
.description("Allows you to look around freely without moving your character")
|
||||
.tags("Movement", "Camera")
|
||||
.version("0.1.0"),
|
||||
enabled.named("Enabled"),
|
||||
toggle.named("Toggle freelook"),
|
||||
freelookKey.named("Freelook Keybinding")
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
if (toggle.value) {
|
||||
if (freelookKey.wasKeyPressed()) {
|
||||
if (isFreeLooking) {
|
||||
stopFreelook();
|
||||
} else {
|
||||
startFreelook();
|
||||
}
|
||||
}
|
||||
} else if (freelookKey.isKeyPressed()) {
|
||||
if (!isFreeLooking) {
|
||||
startFreelook();
|
||||
}
|
||||
} else if (isFreeLooking) {
|
||||
stopFreelook();
|
||||
}
|
||||
}
|
||||
|
||||
private void startFreelook() {
|
||||
isFreeLooking = true;
|
||||
was1stPerson = SaturnClient.client.options.getPerspective() == Perspective.FIRST_PERSON;
|
||||
SaturnClient.client.options.setPerspective(Perspective.THIRD_PERSON_BACK);
|
||||
}
|
||||
|
||||
private void stopFreelook() {
|
||||
if (was1stPerson) {
|
||||
SaturnClient.client.options.setPerspective(Perspective.FIRST_PERSON);
|
||||
}
|
||||
isFreeLooking = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean e) {
|
||||
enabled.value = e;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled.value;
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package org.saturnclient.modules.mods;
|
||||
|
||||
import org.saturnclient.modules.Module;
|
||||
import org.saturnclient.modules.HudMod;
|
||||
import org.saturnclient.modules.ModDimensions;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.saturnclient.config.Property;
|
||||
import org.saturnclient.ui2.RenderScope;
|
||||
import org.saturnclient.modules.ModuleDetails;
|
||||
|
||||
public class Keystrokes extends Module implements HudMod {
|
||||
public static Property<Boolean> enabled = Property.bool(false);
|
||||
public static Property<Boolean> showMouse = Property.bool(true);
|
||||
public static Property<Boolean> showSpace = Property.bool(false);
|
||||
public static ModDimensions dimensions = new ModDimensions(78, 46);
|
||||
|
||||
// Cache key states to avoid repeated calls
|
||||
private boolean w = false;
|
||||
private boolean a = false;
|
||||
private boolean s = false;
|
||||
private boolean d = false;
|
||||
private boolean lmb = false;
|
||||
private boolean rmb = false;
|
||||
private boolean space = false;
|
||||
|
||||
// Cache dimensions to avoid recalculation
|
||||
private int cachedHeight = 54;
|
||||
private boolean dimensionsDirty = true;
|
||||
|
||||
private static Property<Integer> clickBg = Property.color(0xFFCCCCCC);
|
||||
private static Property<Integer> clickFg = Property.color(0xFFFFFFFF);
|
||||
|
||||
public Keystrokes() {
|
||||
super(new ModuleDetails("Keystrokes", "keystrokes")
|
||||
.description("Displays the keystrokes for movement")
|
||||
.version("v0.1.0")
|
||||
.tags("Utility"),
|
||||
|
||||
enabled.named("Enabled"),
|
||||
showMouse.named("Show mouse clicks"),
|
||||
showSpace.named("Show space clicks"),
|
||||
dimensions.prop(),
|
||||
clickFg.named("Clicked fg"),
|
||||
clickBg.named("Clicked bg"));
|
||||
|
||||
dimensions.renderBackground = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
// Cache key states to reduce method calls
|
||||
w = SaturnClient.client.options.forwardKey.isPressed();
|
||||
a = SaturnClient.client.options.leftKey.isPressed();
|
||||
s = SaturnClient.client.options.backKey.isPressed();
|
||||
d = SaturnClient.client.options.rightKey.isPressed();
|
||||
lmb = SaturnClient.client.options.attackKey.isPressed();
|
||||
rmb = SaturnClient.client.options.useKey.isPressed();
|
||||
space = SaturnClient.client.options.jumpKey.isPressed();
|
||||
|
||||
// Mark dimensions as dirty if mouse or space visibility changed
|
||||
if (dimensionsDirty) {
|
||||
updateDimensions();
|
||||
dimensionsDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDimensions() {
|
||||
cachedHeight = 54; // base height with padding considered
|
||||
|
||||
if (showMouse.value) {
|
||||
cachedHeight += 27;
|
||||
}
|
||||
|
||||
if (showSpace.value) {
|
||||
cachedHeight += 22;
|
||||
}
|
||||
|
||||
dimensions.height = cachedHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderDummy(RenderScope scope) {
|
||||
// Use cached dimensions
|
||||
dimensions.height = cachedHeight;
|
||||
|
||||
// WASD keys (3px padding between each key)
|
||||
renderKey(scope, false, 'W', 27, 0); // Top center
|
||||
renderKey(scope, false, 'A', 0, 27); // Bottom left
|
||||
renderKey(scope, false, 'S', 27, 27); // Bottom center
|
||||
renderKey(scope, false, 'D', 54, 27); // Bottom right
|
||||
|
||||
if (showMouse.value) {
|
||||
renderKeyM(scope, false, "LMB", 0, 54); // Below A/S/D
|
||||
renderKeyM(scope, false, "RMB", 40, 54); // With 3px between
|
||||
}
|
||||
|
||||
if (showSpace.value) {
|
||||
renderKeySpace(scope, false, 0, cachedHeight - (showSpace.value ? 22 : 0));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderHud(RenderScope scope) {
|
||||
// Use cached dimensions
|
||||
dimensions.height = cachedHeight;
|
||||
|
||||
// WASD keys (3px padding between each key)
|
||||
renderKey(scope, w, 'W', 27, 0); // Top center
|
||||
renderKey(scope, a, 'A', 0, 27); // Bottom left
|
||||
renderKey(scope, s, 'S', 27, 27); // Bottom center
|
||||
renderKey(scope, d, 'D', 54, 27); // Bottom right
|
||||
|
||||
if (showMouse.value) {
|
||||
renderKeyM(scope, lmb, "LMB", 0, 54); // Below A/S/D
|
||||
renderKeyM(scope, rmb, "RMB", 40, 54); // With 3px between
|
||||
}
|
||||
|
||||
if (showSpace.value) {
|
||||
renderKeySpace(scope, space, 0, cachedHeight - (showSpace.value ? 22 : 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Consistent 3px padding applied below:
|
||||
|
||||
private void renderKeySpace(RenderScope scope, boolean isPressed, int x, int y) {
|
||||
int height = 19;
|
||||
int bgColor = isPressed ? clickBg.value : dimensions.bgColor.value;
|
||||
int fgColor = isPressed ? clickFg.value : dimensions.fgColor.value;
|
||||
|
||||
scope.drawRoundedRectangle(x, y, dimensions.width, height, dimensions.radius.value, bgColor);
|
||||
|
||||
// A 30-pixel line centered vertically with 3px top padding
|
||||
int lineY = y + (height - 1) / 2;
|
||||
scope.drawRect(x + (dimensions.width - 30) / 2, lineY, 30, 1, fgColor);
|
||||
}
|
||||
|
||||
private void renderKeyM(RenderScope scope, boolean isPressed, String c, int x, int y) {
|
||||
int width = 38, height = 24;
|
||||
int bgColor = isPressed ? clickBg.value : dimensions.bgColor.value;
|
||||
int fgColor = isPressed ? clickFg.value : dimensions.fgColor.value;
|
||||
|
||||
scope.drawRoundedRectangle(x, y, width, height, dimensions.radius.value, bgColor);
|
||||
|
||||
// Apply padding: 3px top and left -> add slight offset to x/y
|
||||
scope.drawText(0.6f, c, x + 9, y + 7, dimensions.font.value, fgColor);
|
||||
}
|
||||
|
||||
private void renderKey(RenderScope scope, boolean isPressed, char c, int x, int y) {
|
||||
int size = 24;
|
||||
int bgColor = isPressed ? clickBg.value : dimensions.bgColor.value;
|
||||
int fgColor = isPressed ? clickFg.value : dimensions.fgColor.value;
|
||||
|
||||
scope.drawRoundedRectangle(x, y, size, size, dimensions.radius.value, bgColor);
|
||||
|
||||
// Center the character with 3px top padding
|
||||
scope.drawText(0.6f, String.valueOf(c), x + 9, y + 7, dimensions.font.value, fgColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModDimensions getDimensions() {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean e) {
|
||||
enabled.value = e;
|
||||
dimensionsDirty = true;
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package org.saturnclient.modules.mods;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.saturnclient.modules.HudMod;
|
||||
import org.saturnclient.modules.ModDimensions;
|
||||
import org.saturnclient.modules.Module;
|
||||
import org.saturnclient.modules.ModuleDetails;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.saturnclient.config.Property;
|
||||
import org.saturnclient.ui2.RenderScope;
|
||||
|
||||
import net.minecraft.client.resource.language.I18n;
|
||||
import net.minecraft.client.texture.Sprite;
|
||||
import net.minecraft.client.texture.StatusEffectSpriteManager;
|
||||
import net.minecraft.entity.effect.StatusEffect;
|
||||
import net.minecraft.entity.effect.StatusEffectInstance;
|
||||
import net.minecraft.registry.Registries;
|
||||
import net.minecraft.registry.entry.RegistryEntry;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
|
||||
public class PotionDisplay extends Module implements HudMod {
|
||||
public static Property<Boolean> enabled = Property.bool(false);
|
||||
private static ModDimensions dimensions = new ModDimensions(60, 0);
|
||||
|
||||
private final StatusEffectInstance[] dummyEffects = {
|
||||
new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("speed")).get(), 12000, 2),
|
||||
new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("strength")).get(), 12000, 2),
|
||||
new StatusEffectInstance(Registries.STATUS_EFFECT.getEntry(Identifier.ofVanilla("fire_resistance")).get(), 12000, 2),
|
||||
};
|
||||
|
||||
public PotionDisplay() {
|
||||
super(new ModuleDetails("Potion Display", "potion")
|
||||
.description("Displays potion effects")
|
||||
.version("v0.1.0")
|
||||
.tags("Utility"),
|
||||
|
||||
enabled.named("Enabled"),
|
||||
dimensions.prop());
|
||||
}
|
||||
|
||||
private void renderEffects(RenderScope scope, Iterable<StatusEffectInstance> effects) {
|
||||
int row = 0;
|
||||
|
||||
StatusEffectSpriteManager statusEffectSpriteManager = SaturnClient.client.getStatusEffectSpriteManager();
|
||||
|
||||
for (StatusEffectInstance effect : effects) {
|
||||
if (effect.shouldShowIcon()) {
|
||||
RegistryEntry<StatusEffect> registryEntry = effect.getEffectType();
|
||||
Sprite sprite = statusEffectSpriteManager.getSprite(registryEntry);
|
||||
scope.drawSpriteStretched(sprite, 0, 18 * row, 16, 16);
|
||||
scope.drawText(0.5f, getDurationAsString(effect), 18, 18 * row + 3, dimensions.font.value, -1);
|
||||
row++;
|
||||
}
|
||||
}
|
||||
|
||||
dimensions.height = 18 * row;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderHud(RenderScope scope) {
|
||||
renderEffects(scope, SaturnClient.client.player.getStatusEffects());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderDummy(RenderScope scope) {
|
||||
renderEffects(scope, Arrays.asList(dummyEffects));
|
||||
}
|
||||
|
||||
// This code was from: https://github.com/magicus/statuseffecttimer/blob/master/src/main/java/se/icus/mag/statuseffecttimer/mixin/StatusEffectTimerMixin.java
|
||||
private String getDurationAsString(StatusEffectInstance statusEffectInstance) {
|
||||
if (statusEffectInstance.isInfinite()) {
|
||||
return I18n.translate("effect.duration.infinite");
|
||||
}
|
||||
|
||||
int ticks = MathHelper.floor((float) statusEffectInstance.getDuration());
|
||||
int totalSeconds = ticks / 20;
|
||||
|
||||
if (totalSeconds >= 3600) {
|
||||
return totalSeconds / 3600 + "h";
|
||||
} else if (totalSeconds >= 60) {
|
||||
int minutes = totalSeconds / 60;
|
||||
int seconds = totalSeconds % 60;
|
||||
return String.format("%d:%02d", minutes, seconds);
|
||||
} else {
|
||||
return String.valueOf(totalSeconds) + "s";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModDimensions getDimensions() {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean e) {
|
||||
enabled.value = e;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package org.saturnclient.modules.mods.mixins;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.network.ClientPlayerEntity;
|
||||
import net.minecraft.client.render.Camera;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.world.BlockView;
|
||||
|
||||
import org.saturnclient.modules.mods.Freelook;
|
||||
import org.saturnclient.modules.mods.utils.CameraOverriddenEntity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(Camera.class)
|
||||
public abstract class CameraMixin {
|
||||
@Unique
|
||||
boolean firstTime = true;
|
||||
|
||||
@Shadow
|
||||
protected abstract void setRotation(float yaw, float pitch);
|
||||
|
||||
@Inject(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/Camera;setRotation(FF)V", ordinal = 1, shift = At.Shift.AFTER))
|
||||
public void lockRotation(BlockView focusedBlock, Entity cameraEntity, boolean isThirdPerson, boolean isFrontFacing, float tickDelta, CallbackInfo ci) {
|
||||
if (Freelook.isFreeLooking && cameraEntity instanceof ClientPlayerEntity) {
|
||||
CameraOverriddenEntity cameraOverriddenEntity = (CameraOverriddenEntity) cameraEntity;
|
||||
|
||||
if (firstTime && MinecraftClient.getInstance().player != null) {
|
||||
cameraOverriddenEntity.freelook$setCameraPitch(MinecraftClient.getInstance().player.getPitch());
|
||||
cameraOverriddenEntity.freelook$setCameraYaw(MinecraftClient.getInstance().player.getYaw());
|
||||
firstTime = false;
|
||||
}
|
||||
this.setRotation(cameraOverriddenEntity.freelook$getCameraYaw(), cameraOverriddenEntity.freelook$getCameraPitch());
|
||||
|
||||
}
|
||||
if (!Freelook.isFreeLooking && cameraEntity instanceof ClientPlayerEntity) {
|
||||
firstTime = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package org.saturnclient.modules.mods.mixins;
|
||||
|
||||
|
||||
import net.minecraft.client.network.ClientPlayerEntity;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
|
||||
import org.saturnclient.modules.mods.Freelook;
|
||||
import org.saturnclient.modules.mods.utils.CameraOverriddenEntity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(Entity.class)
|
||||
public class EntityMixin implements CameraOverriddenEntity {
|
||||
@Unique
|
||||
private float cameraPitch;
|
||||
|
||||
@Unique
|
||||
private float cameraYaw;
|
||||
|
||||
@Inject(method = "changeLookDirection", at = @At("HEAD"), cancellable = true)
|
||||
public void changeCameraLookDirection(double xDelta, double yDelta, CallbackInfo ci) {
|
||||
//noinspection ConstantValue// IntelliJ is incorrect here, this code block is reachable
|
||||
if (Freelook.isFreeLooking && (Object) this instanceof ClientPlayerEntity) {
|
||||
double pitchDelta = (yDelta * 0.15);
|
||||
double yawDelta = (xDelta * 0.15);
|
||||
|
||||
this.cameraPitch = MathHelper.clamp(this.cameraPitch + (float) pitchDelta, -90.0f, 90.0f);
|
||||
this.cameraYaw += (float) yawDelta;
|
||||
|
||||
ci.cancel();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Unique
|
||||
public float freelook$getCameraPitch() {
|
||||
return this.cameraPitch;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Unique
|
||||
public float freelook$getCameraYaw() {
|
||||
return this.cameraYaw;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Unique
|
||||
public void freelook$setCameraPitch(float pitch) {
|
||||
this.cameraPitch = pitch;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Unique
|
||||
public void freelook$setCameraYaw(float yaw) {
|
||||
this.cameraYaw = yaw;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package org.saturnclient.modules.mods.mixins;
|
||||
|
||||
import org.saturnclient.modules.HudMod;
|
||||
import org.saturnclient.modules.ModDimensions;
|
||||
import org.saturnclient.modules.ModManager;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import org.saturnclient.saturnclient.menus.HudEditor;
|
||||
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
|
||||
import org.saturnclient.ui2.RenderScope;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import net.minecraft.client.font.TextRenderer;
|
||||
import net.minecraft.client.gui.DrawContext;
|
||||
import net.minecraft.client.gui.hud.InGameHud;
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
import net.minecraft.client.render.RenderTickCounter;
|
||||
|
||||
@Mixin(InGameHud.class)
|
||||
public class RenderMixin {
|
||||
@Inject(method = "renderChat", at = @At("TAIL"))
|
||||
public void renderMods(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) {
|
||||
TextRenderer textRenderer = SaturnClient.client.textRenderer;
|
||||
|
||||
if (textRenderer != null &&
|
||||
!(SaturnClient.client.currentScreen instanceof HudEditor)) {
|
||||
RenderScope renderScope = new RenderScope(context.getMatrices(),
|
||||
((DrawContextAccessor) context).getVertexConsumers());
|
||||
|
||||
for (org.saturnclient.modules.Module m : ModManager.ENABLED_MODS) {
|
||||
if (m instanceof HudMod && m.isEnabled()) {
|
||||
ModDimensions dim = ((HudMod) m).getDimensions();
|
||||
|
||||
renderScope.matrices.push();
|
||||
|
||||
renderScope.matrices.translate(dim.x.value, (float) dim.y.value, 0);
|
||||
|
||||
renderScope.matrices.scale(dim.scale.value, dim.scale.value, 1.0f);
|
||||
|
||||
if (dim.renderBackground) {
|
||||
renderScope.drawRoundedRectangle(0, 0, dim.width, dim.height, dim.radius.value,
|
||||
dim.bgColor.value);
|
||||
}
|
||||
|
||||
renderScope.setRenderLayer(RenderLayer::getGuiTextured);
|
||||
|
||||
((HudMod) m).renderHud(renderScope);
|
||||
|
||||
renderScope.matrices.pop();
|
||||
} else {
|
||||
m.render(renderScope);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package org.saturnclient.modules.mods.utils;
|
||||
|
||||
public interface CameraOverriddenEntity {
|
||||
float freelook$getCameraPitch();
|
||||
float freelook$getCameraYaw();
|
||||
|
||||
void freelook$setCameraPitch(float pitch);
|
||||
void freelook$setCameraYaw(float yaw);
|
||||
}
|
||||
@@ -2,28 +2,26 @@ package org.saturnclient.saturnclient;
|
||||
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
|
||||
import org.saturnclient.modules.ModManager;
|
||||
import org.saturnclient.saturnclient.auth.Auth;
|
||||
import org.saturnclient.saturnclient.config.ConfigManager;
|
||||
import org.saturnclient.saturnclient.cosmetics.Emotes;
|
||||
import org.saturnclient.saturnclient.cosmetics.Hats;
|
||||
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
|
||||
import org.saturnclient.client.ServiceClient;
|
||||
import org.saturnclient.cosmetics.Emotes;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.impl.provider.GLFWProviderImpl;
|
||||
import org.saturnclient.impl.provider.FeatureProviderFabric;
|
||||
import org.saturnclient.impl.provider.RefConstructorImpl;
|
||||
import org.saturnclient.impl.provider.SaturnProviderImpl;
|
||||
import org.saturnclient.impl.ui.EntityDrawerImpl;
|
||||
import org.saturnclient.impl.ui.SaturnScreenFabric;
|
||||
import org.saturnclient.mod.ModManager;
|
||||
import org.saturnclient.saturnclient.event.KeyInputHandler;
|
||||
import org.saturnclient.saturnclient.mixin.DrawContextAccessor;
|
||||
import org.saturnclient.ui2.RenderScope;
|
||||
import org.saturnclient.ui2.SaturnScreen;
|
||||
import org.saturnclient.ui2.elements.ImageTexture;
|
||||
import org.saturnclient.ui2.resources.Textures;
|
||||
import org.saturnclient.ui2.screens.TitleMenu;
|
||||
import org.saturnclient.config.Config;
|
||||
import org.saturnclient.config.ConfigManager;
|
||||
import org.saturnclient.ui.components.SkinPreview;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import dev.felnull.specialmodelloader.api.event.SpecialModelLoaderEvents;
|
||||
|
||||
/**
|
||||
* Main class for the Saturn Client mod.
|
||||
*/
|
||||
@@ -37,50 +35,30 @@ public class SaturnClient implements ModInitializer {
|
||||
public void onInitialize() {
|
||||
LOGGER.info("Initializing " + MOD_ID);
|
||||
client = MinecraftClient.getInstance();
|
||||
SaturnClientConfig.init();
|
||||
|
||||
Providers.saturn = new SaturnProviderImpl();
|
||||
Providers.refConstructor = new RefConstructorImpl();
|
||||
Providers.GLFW = new GLFWProviderImpl();
|
||||
Providers.feature = new FeatureProviderFabric(client);
|
||||
SkinPreview.DRAWER = new EntityDrawerImpl();
|
||||
|
||||
Config.init();
|
||||
ModManager.init();
|
||||
|
||||
client.execute(() -> {
|
||||
SaturnScreenFabric.preload(client);
|
||||
});
|
||||
|
||||
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> ConfigManager.save());
|
||||
|
||||
SpecialModelLoaderEvents.LOAD_SCOPE.register(() -> {
|
||||
return (resourceManager, location) -> MOD_ID.equals(location.getNamespace());
|
||||
});
|
||||
|
||||
ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
|
||||
if (screen != null && !(screen instanceof TitleMenu)) {
|
||||
if (screen instanceof SaturnScreen) {
|
||||
((SaturnScreen) screen).draw(new ImageTexture(Textures.LOGO_TEXT_BIG).dimensions(180, 18)
|
||||
.position(screen.width - 180 - 20, screen.height - 18 - 20));
|
||||
} else {
|
||||
ScreenEvents.afterRender(screen).register((screen1, context, mouseX, mouseY, tickDelta) -> {
|
||||
RenderScope renderScope = new RenderScope(context.getMatrices(),
|
||||
((DrawContextAccessor) context).getVertexConsumers());
|
||||
|
||||
int width = context.getScaledWindowWidth();
|
||||
int height = context.getScaledWindowHeight();
|
||||
|
||||
float scaleX = (float) screen1.width / width;
|
||||
float scaleY = (float) screen1.height / height;
|
||||
|
||||
MatrixStack matrixStack = context.getMatrices();
|
||||
matrixStack.push();
|
||||
matrixStack.translate(width - 95, height - 14, 0);
|
||||
matrixStack.scale(0.5f / scaleX, 0.5f / scaleY, 1.0f);
|
||||
renderScope.drawTexture(Textures.LOGO_TEXT_BIG, 0, 0, 0, 0, 180, 18);
|
||||
matrixStack.pop();
|
||||
});
|
||||
}
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
for (org.saturnclient.mod.Mod m : ModManager.ENABLED_MODS) {
|
||||
m.tick();
|
||||
}
|
||||
});
|
||||
|
||||
KeyInputHandler.register();
|
||||
Emotes.initialize();
|
||||
if (Auth.authenticate()) {
|
||||
Cloaks.initialize();
|
||||
Hats.initialize();
|
||||
LOGGER.info(MOD_ID + " initialization complete");
|
||||
} else {
|
||||
LOGGER.error("Failed to authenticate with the server");
|
||||
}
|
||||
ServiceClient.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package org.saturnclient.saturnclient;
|
||||
|
||||
import org.saturnclient.saturnclient.config.Property;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.saturnclient.saturnclient.config.ConfigManager;
|
||||
import org.saturnclient.ui2.resources.Textures;
|
||||
|
||||
import net.minecraft.util.Formatting;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public class SaturnClientConfig {
|
||||
public static ConfigManager config;
|
||||
|
||||
public static Property<Boolean> realisticLogo = Property.bool(false);
|
||||
public static Property<Boolean> saturnTitleScreen = Property.bool(true);
|
||||
public static Property<Boolean> bendyCloaks = Property.bool(true);
|
||||
public static Property<Integer> openEmoteWheel = Property.keybinding(GLFW.GLFW_KEY_B);
|
||||
|
||||
public static Identifier getLogo() {
|
||||
return realisticLogo.value ? Textures.REALISTIC_LOGO : Textures.LOGO;
|
||||
}
|
||||
|
||||
public static String getSaturnIndicator() {
|
||||
return realisticLogo.value ? "" : "";
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets the icon color of a specific individual, here are the different colors
|
||||
*
|
||||
* - Owner: Dark Red
|
||||
* - Admin: Red
|
||||
* - Partners: Gold
|
||||
* - Contributor: Aqua
|
||||
* - Other/player: White
|
||||
*/
|
||||
public static Formatting getIconColor(String uuid) {
|
||||
if (uuid.equals("d362a04228bd49e1a807ae74dbe8aba9")) {
|
||||
return Formatting.DARK_RED; // Owner
|
||||
}
|
||||
|
||||
return Formatting.WHITE;
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
config = new ConfigManager("Saturn Client");
|
||||
config.property("Realistic logo", realisticLogo);
|
||||
config.property("Saturn client title screen", saturnTitleScreen);
|
||||
config.property("Open Emote Wheel", openEmoteWheel);
|
||||
config.property("Bendy Cloaks", bendyCloaks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.saturnclient.saturnclient;
|
||||
|
||||
//? if >=1.21.11
|
||||
import org.jspecify.annotations.NonNull;
|
||||
//? if >=1.21.8 {
|
||||
import org.saturnclient.client.player.SaturnPlayer;
|
||||
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.RenderStateDataKey;
|
||||
//?}
|
||||
|
||||
/**
|
||||
* The key PlayerEntityRendererMixin stores each player's SaturnPlayer under in their render state.
|
||||
* Fabric API's render state data only exists from 1.21.8, so older versions look players up by name.
|
||||
*/
|
||||
public class SaturnRenderState {
|
||||
//? if >=1.21.11 {
|
||||
@NonNull
|
||||
public static RenderStateDataKey<SaturnPlayer> saturnDataKey = RenderStateDataKey
|
||||
.create(() -> "saturn-client-player");
|
||||
//?} else if >=1.21.8 {
|
||||
/*public static RenderStateDataKey<SaturnPlayer> saturnDataKey = RenderStateDataKey
|
||||
.create(() -> "saturn-client-player");
|
||||
*///?}
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
package org.saturnclient.saturnclient.auth;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
|
||||
import net.minecraft.client.network.AbstractClientPlayerEntity;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
import org.saturnclient.saturnclient.cosmetics.Hats;
|
||||
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
|
||||
import org.saturnclient.ui2.Utils;
|
||||
import org.saturnclient.ui2.elements.Notification.NotificationKind;
|
||||
|
||||
import dev.kosmx.playerAnim.api.layered.AnimationStack;
|
||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
|
||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
|
||||
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
|
||||
public class Auth {
|
||||
public static Map<String, SaturnPlayer> players = new HashMap<>();
|
||||
public static Map<String, String> playerNames = new HashMap<>();
|
||||
public static String uuid;
|
||||
private static Thread listenerThread;
|
||||
private static Thread pingThread;
|
||||
private static volatile boolean running = false;
|
||||
|
||||
public static boolean authenticate() {
|
||||
// Register shutdown hook ONCE
|
||||
ClientLifecycleEvents.CLIENT_STOPPING.register(_o -> close());
|
||||
|
||||
try {
|
||||
if (SaturnClient.client.getSession() == null) {
|
||||
SaturnClient.LOGGER.error("No active Minecraft session found");
|
||||
return false;
|
||||
}
|
||||
|
||||
String accessToken = SaturnClient.client.getSession().getAccessToken();
|
||||
SaturnClient.LOGGER.info("Authenticating");
|
||||
|
||||
Network.init();
|
||||
|
||||
Network.write(accessToken);
|
||||
|
||||
String response = Network.read();
|
||||
|
||||
SaturnParser parser = new SaturnParser(response);
|
||||
|
||||
uuid = parser.getString("uuid");
|
||||
String cloak = parser.getString("cloak");
|
||||
String hat = parser.getString("hat");
|
||||
|
||||
for (String availableCloak : parser.getArray("cloaks")) {
|
||||
Cloaks.availableCloaks.add(availableCloak);
|
||||
}
|
||||
|
||||
for (String availableHat : parser.getArray("hats")) {
|
||||
Hats.availableHats.add(availableHat);
|
||||
}
|
||||
|
||||
players.put(uuid, new SaturnPlayer(cloak, hat));
|
||||
|
||||
Cloaks.loadCloak(uuid);
|
||||
|
||||
playerNames.put(SaturnClient.client.getSession().getUsername(), uuid);
|
||||
|
||||
afterAuth();
|
||||
|
||||
startListenerThread();
|
||||
startPingThread();
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
SaturnClient.LOGGER.error("Authentication failed", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void startListenerThread() {
|
||||
running = true;
|
||||
listenerThread = new Thread(() -> {
|
||||
try {
|
||||
while (running) {
|
||||
String message = Network.read();
|
||||
if (message == null) {
|
||||
SaturnClient.LOGGER.error("Connection closed");
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
|
||||
SaturnClient.LOGGER.info("Received: " + message);
|
||||
|
||||
SaturnParser parser = new SaturnParser(message);
|
||||
|
||||
switch (parser.method) {
|
||||
case "player":
|
||||
String uuid = parser.getString("uuid");
|
||||
if (parser.getBool("saturn")) {
|
||||
String cloak = parser.getString("cloak");
|
||||
String hat = parser.getString("hat");
|
||||
players.put(uuid, new SaturnPlayer(cloak, hat));
|
||||
Cloaks.loadCloak(uuid);
|
||||
} else {
|
||||
playerNames.entrySet().removeIf(entry -> entry.getValue().equals(uuid));
|
||||
}
|
||||
break;
|
||||
|
||||
case "successful_transaction":
|
||||
String cloak = parser.getStringOrNull("cloak");
|
||||
String hat = parser.getStringOrNull("hat");
|
||||
if (cloak != null) {
|
||||
Cloaks.availableCloaks.add(cloak);
|
||||
|
||||
}
|
||||
if (hat != null) {
|
||||
Hats.availableHats.add(hat);
|
||||
}
|
||||
|
||||
case "update_cloak":
|
||||
String targetUuid = parser.getStringOrNull("uuid");
|
||||
String targetCloak = parser.getStringOrNull("cloak");
|
||||
if (targetUuid != null && targetCloak != null) {
|
||||
SaturnPlayer player = players.get(targetUuid);
|
||||
if (player != null) {
|
||||
player.cloak = targetCloak;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "update_hat":
|
||||
targetUuid = parser.getStringOrNull("uuid");
|
||||
String targetHat = parser.getStringOrNull("hat");
|
||||
if (targetUuid != null && targetHat != null) {
|
||||
SaturnPlayer player = players.get(targetUuid);
|
||||
if (player != null) {
|
||||
player.hat = targetHat;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "emote":
|
||||
targetUuid = parser.getStringOrNull("uuid");
|
||||
String emote = parser.getStringOrNull("name");
|
||||
|
||||
for (AbstractClientPlayerEntity player : SaturnClient.client.world.getPlayers()) {
|
||||
if (player.getUuidAsString().replace("-", "").equals(targetUuid)) {
|
||||
AnimationStack animationStack = PlayerAnimationAccess.getPlayerAnimLayer(player);
|
||||
if (emote != null && !emote.isEmpty()) {
|
||||
if (animationStack.isActive() && animationStack.getPriority() == 1000) {
|
||||
animationStack.removeLayer(1000);
|
||||
}
|
||||
animationStack.addAnimLayer(1000,
|
||||
PlayerAnimationRegistry
|
||||
.getAnimation(Identifier.of("saturnclient", emote))
|
||||
.playAnimation());
|
||||
} else {
|
||||
animationStack.removeLayer(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (parser.error != null) {
|
||||
String[] a = parser.error.replace("!", "").split(": ");
|
||||
Utils.notify(NotificationKind.Error, a[0], a[1]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
SaturnClient.LOGGER.error("Error reading from server", e);
|
||||
} finally {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
listenerThread.setDaemon(true);
|
||||
listenerThread.start();
|
||||
}
|
||||
|
||||
private static void startPingThread() {
|
||||
pingThread = new Thread(() -> {
|
||||
try {
|
||||
while (running && !Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
Thread.sleep(25000); // 25 seconds
|
||||
if (running) {
|
||||
Network.write("ping");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt(); // Restore interrupt status
|
||||
break; // Exit the loop on interruption
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (!(e instanceof InterruptedException)) {
|
||||
SaturnClient.LOGGER.error("Error in ping thread", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
pingThread.setDaemon(true);
|
||||
pingThread.start();
|
||||
}
|
||||
|
||||
public static void close() {
|
||||
running = false;
|
||||
if (pingThread != null) {
|
||||
pingThread.interrupt();
|
||||
}
|
||||
if (listenerThread != null) {
|
||||
listenerThread.interrupt();
|
||||
}
|
||||
try {
|
||||
close();
|
||||
SaturnClient.LOGGER.info("SaturnApi connection closed.");
|
||||
} catch (Exception e) {
|
||||
SaturnClient.LOGGER.error("Failed to close resources", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void afterAuth() {
|
||||
PlayerTracker.initialize();
|
||||
}
|
||||
|
||||
public static void setCloak(String cloakName) {
|
||||
try {
|
||||
Network.write("set_cloak@cloak=" + cloakName + "@notify=" + stringifyPlayers());
|
||||
} catch (Exception e) {
|
||||
SaturnClient.LOGGER.error("Request failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setHat(String hatName) {
|
||||
try {
|
||||
Network.write("set_hat@hat=" + hatName + "@notify=" + stringifyPlayers());
|
||||
} catch (Exception e) {
|
||||
SaturnClient.LOGGER.error("Request failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void emote(String name) {
|
||||
try {
|
||||
Network.write("emote@name=" + name + "@notify=" + stringifyPlayers());
|
||||
} catch (Exception e) {
|
||||
SaturnClient.LOGGER.error("Request failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String stringifyPlayers() {
|
||||
return players.keySet().stream()
|
||||
.filter(key -> key != uuid)
|
||||
.collect(Collectors.joining("$"));
|
||||
}
|
||||
|
||||
public static void player(String name, String uuid) {
|
||||
try {
|
||||
playerNames.put(name, uuid.replaceAll("-", ""));
|
||||
Network.write("player@uuid=" + uuid.replaceAll("-", ""));
|
||||
} catch (Exception e) {
|
||||
SaturnClient.LOGGER.error("Request failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void buyCloak(String name) {
|
||||
try {
|
||||
Network.write("buy_cloak@cloak=" + name);
|
||||
} catch (Exception e) {
|
||||
SaturnClient.LOGGER.error("Request failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void buyHat(String name) {
|
||||
try {
|
||||
Network.write("buy_hat@hat=" + name);
|
||||
} catch (Exception e) {
|
||||
SaturnClient.LOGGER.error("Request failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package org.saturnclient.saturnclient.auth;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.security.*;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.*;
|
||||
import javax.crypto.spec.*;
|
||||
import java.security.interfaces.*;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
public class Network {
|
||||
static Socket socket;
|
||||
static PrintWriter out;
|
||||
static BufferedReader in;
|
||||
static byte[] key = new byte[32];
|
||||
|
||||
public static class AES {
|
||||
public static byte[] encryptAES256(String message, byte[] key) throws Exception {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
|
||||
byte[] encryptedMessage = cipher.doFinal(message.getBytes());
|
||||
return encryptedMessage;
|
||||
}
|
||||
|
||||
public static String decryptAES256(byte[] encryptedMessage, byte[] key) throws Exception {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
|
||||
byte[] decryptedMessage = cipher.doFinal(encryptedMessage);
|
||||
return new String(decryptedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public static void init() throws Exception {
|
||||
// Generate AES key
|
||||
new SecureRandom().nextBytes(key);
|
||||
|
||||
socket = new Socket("77.247.92.168", 8080);
|
||||
// out = new DataOutputStream(socket.getOutputStream());
|
||||
out = new PrintWriter(socket.getOutputStream(), true);
|
||||
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
|
||||
StringBuilder publicKeyBuilder = new StringBuilder();
|
||||
String line;
|
||||
while (!(line = in.readLine()).contains("-----END PUBLIC KEY-----")) {
|
||||
publicKeyBuilder.append(line).append("\n");
|
||||
}
|
||||
publicKeyBuilder.append(line);
|
||||
|
||||
String publicKeyPem = publicKeyBuilder.toString();
|
||||
|
||||
String publicKeyBase64 = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "").replace("\n", "").replace("\r", "");
|
||||
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);
|
||||
|
||||
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
|
||||
|
||||
Cipher rsaCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
||||
rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
byte[] encryptedKey = rsaCipher.doFinal(key);
|
||||
|
||||
out.println(Base64.getEncoder().encodeToString(encryptedKey));
|
||||
}
|
||||
|
||||
public static void write(String message) {
|
||||
try {
|
||||
out.println(Base64.getEncoder().encodeToString(AES.encryptAES256(message, key)));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to write to server: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static String read() {
|
||||
try {
|
||||
String msg = in.readLine();
|
||||
if (msg != null) {
|
||||
return AES.decryptAES256(Base64.getDecoder().decode(msg), key);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static void close() {
|
||||
try {
|
||||
socket.close();
|
||||
out.close();
|
||||
in.close();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.saturnclient.saturnclient.auth;
|
||||
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import net.minecraft.client.network.PlayerListEntry;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
|
||||
public class PlayerTracker {
|
||||
private static Set<UUID> trackedPlayers = new HashSet<>();
|
||||
|
||||
public static void initialize() {
|
||||
// When client joins a server, reset tracked players
|
||||
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> {
|
||||
trackedPlayers.clear();
|
||||
client.execute(() -> {
|
||||
checkForNewPlayers(SaturnClient.client);
|
||||
});
|
||||
});
|
||||
|
||||
// Check for new players every tick
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> checkForNewPlayers(client));
|
||||
}
|
||||
|
||||
private static void checkForNewPlayers(MinecraftClient client) {
|
||||
if (client.getNetworkHandler() == null)
|
||||
return;
|
||||
|
||||
for (PlayerListEntry player : client.getNetworkHandler().getPlayerList()) {
|
||||
UUID playerUUID = player.getProfile().getId();
|
||||
|
||||
if (!trackedPlayers.contains(playerUUID)) {
|
||||
// A new player has joined
|
||||
onPlayerJoin(client, player);
|
||||
trackedPlayers.add(playerUUID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void onPlayerJoin(MinecraftClient client, PlayerListEntry player) {
|
||||
GameProfile profile = player.getProfile();
|
||||
String uuid = profile.getId().toString();
|
||||
if (uuid != Auth.uuid)
|
||||
Auth.player(profile.getName(), uuid);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package org.saturnclient.saturnclient.auth;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class SaturnParser {
|
||||
public String method = "";
|
||||
public String error;
|
||||
private Map<String, String> params = new HashMap<>();
|
||||
|
||||
public SaturnParser(String input) throws IOException {
|
||||
String[] parts = input.split("@");
|
||||
|
||||
if (parts.length == 0) {
|
||||
throw new IOException("Invalid input string");
|
||||
}
|
||||
|
||||
if (parts[0].startsWith("!")) {
|
||||
error = input;
|
||||
}
|
||||
|
||||
method = parts[0];
|
||||
|
||||
for (int i = 1; i < parts.length; i++) {
|
||||
String[] keyValue = parts[i].split("=");
|
||||
if (keyValue.length == 2) {
|
||||
params.put(keyValue[0].trim(), keyValue[1].trim());
|
||||
} else if (keyValue.length == 1) {
|
||||
params.put(keyValue[0].trim(), "");
|
||||
} else {
|
||||
throw new IOException("Invalid parameter format: " + parts[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getString(String i) throws IOException {
|
||||
String val = params.get(i);
|
||||
if (val != null) {
|
||||
return val;
|
||||
} else {
|
||||
throw new IOException("Parameter not found " + i);
|
||||
}
|
||||
}
|
||||
|
||||
public String getStringOrNull(String i) throws IOException {
|
||||
String val = params.get(i);
|
||||
return val;
|
||||
}
|
||||
|
||||
public boolean getBool(String i) throws IOException {
|
||||
String val = params.get(i);
|
||||
if (val != null) {
|
||||
return val.equals("true");
|
||||
} else {
|
||||
throw new IOException("Parameter not found " + i);
|
||||
}
|
||||
}
|
||||
|
||||
public String[] getArray(String i) throws IOException {
|
||||
String val = params.get(i);
|
||||
if (val != null) {
|
||||
if (val.isEmpty()) {
|
||||
return new String[] {};
|
||||
}
|
||||
return val.split("\\$");
|
||||
} else {
|
||||
throw new IOException("Parameter not found " + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.saturnclient.saturnclient.auth;
|
||||
|
||||
public class SaturnPlayer {
|
||||
public String cloak = "";
|
||||
public String hat = "";
|
||||
|
||||
public SaturnPlayer(String cloak, String hat) {
|
||||
this.cloak = cloak;
|
||||
this.hat = hat;
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package org.saturnclient.saturnclient.config;
|
||||
|
||||
import com.google.gson.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.*;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
|
||||
public class ConfigManager {
|
||||
private static final File configFile = new File(SaturnClient.client.runDirectory, "saturn.json");
|
||||
private static final Map<String, Map<String, Property<?>>> properties = new HashMap<>();
|
||||
private static JsonObject cachedThemeJson = null;
|
||||
|
||||
private final Map<String, Property<?>> currentMap;
|
||||
private final String namespace;
|
||||
|
||||
public ConfigManager(String namespace) {
|
||||
this.namespace = namespace;
|
||||
this.currentMap = new LinkedHashMap<>();
|
||||
properties.put(namespace, currentMap);
|
||||
System.out.println("Created namespace: " + namespace);
|
||||
}
|
||||
|
||||
public ConfigManager(ConfigManager parent, String namespace) {
|
||||
this.namespace = namespace;
|
||||
this.currentMap = new LinkedHashMap<>();
|
||||
Property<Map<String, Property<?>>> nsProp = Property.namespace(currentMap);
|
||||
parent.property(namespace, nsProp);
|
||||
System.out.println("Created sub-namespace: " + namespace);
|
||||
}
|
||||
|
||||
public void sub(String name) {
|
||||
property(name, Property.namespace(new LinkedHashMap<>()));
|
||||
}
|
||||
|
||||
public <T> Property<T> property(String name, Property<T> value) {
|
||||
currentMap.put(name, value);
|
||||
loadProp(name, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private <T> void loadProp(String name, Property<T> prop) {
|
||||
JsonObject source = cachedThemeJson != null ? cachedThemeJson : loadAndCache();
|
||||
if (source == null)
|
||||
return;
|
||||
|
||||
JsonElement namespaceJson = source.get(namespace);
|
||||
if (namespaceJson == null || !namespaceJson.isJsonObject())
|
||||
return;
|
||||
|
||||
JsonElement value = namespaceJson.getAsJsonObject().get(name);
|
||||
if (value != null)
|
||||
prop.loadFromJson(value);
|
||||
}
|
||||
|
||||
private static JsonObject loadAndCache() {
|
||||
try {
|
||||
if (!configFile.exists()) {
|
||||
Files.writeString(configFile.toPath(), "{}");
|
||||
return new JsonObject();
|
||||
}
|
||||
String json = Files.readString(configFile.toPath());
|
||||
cachedThemeJson = JsonParser.parseString(json).getAsJsonObject();
|
||||
return cachedThemeJson;
|
||||
} catch (IOException e) {
|
||||
SaturnClient.LOGGER.error("Error reading config file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void load() {
|
||||
SaturnClient.LOGGER.info("Loading config...");
|
||||
JsonObject json = loadAndCache();
|
||||
if (json == null)
|
||||
return;
|
||||
|
||||
properties.forEach((namespace, map) -> {
|
||||
JsonElement nsElement = json.get(namespace);
|
||||
if (nsElement != null && nsElement.isJsonObject()) {
|
||||
loadProperties(nsElement.getAsJsonObject(), map);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void loadProperties(JsonObject json, Map<String, Property<?>> props) {
|
||||
props.forEach((name, prop) -> {
|
||||
JsonElement el = json.get(name);
|
||||
if (el != null)
|
||||
prop.loadFromJson(el);
|
||||
});
|
||||
}
|
||||
|
||||
public static void save() {
|
||||
SaturnClient.LOGGER.info("Saving config...");
|
||||
JsonObject root = new JsonObject();
|
||||
properties.forEach((namespace, map) -> {
|
||||
JsonObject nsJson = new JsonObject();
|
||||
saveProperties(nsJson, map);
|
||||
root.add(namespace, nsJson);
|
||||
});
|
||||
|
||||
try {
|
||||
String formatted = new GsonBuilder().setPrettyPrinting().create().toJson(root);
|
||||
Files.writeString(configFile.toPath(), formatted);
|
||||
SaturnClient.LOGGER.info("Config saved successfully.");
|
||||
} catch (IOException e) {
|
||||
SaturnClient.LOGGER.error("Error saving config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void saveProperties(JsonObject json, Map<String, Property<?>> props) {
|
||||
props.forEach((name, prop) -> json.add(name, prop.toJson()));
|
||||
}
|
||||
|
||||
public Map<String, Property<?>> getProperties() {
|
||||
return currentMap;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.saturnclient.saturnclient.config;
|
||||
|
||||
public class NamedProperty<T> {
|
||||
public String name;
|
||||
public Property<T> prop;
|
||||
|
||||
public NamedProperty(String name, Property<T> value) {
|
||||
this.name = name;
|
||||
this.prop = value;
|
||||
}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
package org.saturnclient.saturnclient.config;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
|
||||
public class Property<T> {
|
||||
|
||||
public enum PropertyType {
|
||||
BOOLEAN,
|
||||
INTEGER,
|
||||
FLOAT,
|
||||
STRING,
|
||||
HEX,
|
||||
NAMESPACE,
|
||||
SELECT,
|
||||
KEYBINDING,
|
||||
}
|
||||
|
||||
public T value;
|
||||
public boolean isReset = false;
|
||||
private T defaultValue;
|
||||
private String[] availableValues;
|
||||
private final PropertyType type;
|
||||
private boolean wasPressedLastTick = false;
|
||||
|
||||
private Property(T value, PropertyType type) {
|
||||
this.value = value;
|
||||
this.defaultValue = value;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
// ---------- Factory Methods ----------
|
||||
public static <T> Property<T> from(T value) {
|
||||
if (value instanceof Boolean)
|
||||
return new Property<>(value, PropertyType.BOOLEAN);
|
||||
if (value instanceof Integer)
|
||||
return new Property<>(value, PropertyType.INTEGER);
|
||||
if (value instanceof Float)
|
||||
return new Property<>(value, PropertyType.FLOAT);
|
||||
if (value instanceof String)
|
||||
return new Property<>(value, PropertyType.STRING);
|
||||
if (valueIsNamespace(value))
|
||||
return new Property<>(value, PropertyType.NAMESPACE);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Property<Integer> font(int value) {
|
||||
return select(value, "Default", "Inter", "Inter bold");
|
||||
}
|
||||
|
||||
public static Property<Integer> select(Integer value, String... availableValues) {
|
||||
Property<Integer> property = new Property<>(value, PropertyType.SELECT);
|
||||
property.availableValues = availableValues;
|
||||
return property;
|
||||
}
|
||||
|
||||
public static Property<Map<String, Property<?>>> namespace(Map<String, Property<?>> value) {
|
||||
return new Property<>(value, PropertyType.NAMESPACE);
|
||||
}
|
||||
|
||||
public static Property<Boolean> bool(boolean value) {
|
||||
return new Property<>(value, PropertyType.BOOLEAN);
|
||||
}
|
||||
|
||||
public static Property<Integer> integer(int value) {
|
||||
return new Property<>(value, PropertyType.INTEGER);
|
||||
}
|
||||
|
||||
public static Property<Float> floatProp(float value) {
|
||||
return new Property<>(value, PropertyType.FLOAT);
|
||||
}
|
||||
|
||||
public static Property<String> string(String value) {
|
||||
return new Property<>(value, PropertyType.STRING);
|
||||
}
|
||||
|
||||
public static Property<Integer> color(int value) {
|
||||
return new Property<>(value, PropertyType.HEX);
|
||||
}
|
||||
|
||||
public static Property<Integer> keybinding(int value) {
|
||||
return new Property<>(value, PropertyType.KEYBINDING);
|
||||
}
|
||||
|
||||
// ---------- Select Helpers ----------
|
||||
public void next() {
|
||||
if (type == PropertyType.SELECT) {
|
||||
int i = (Integer) value;
|
||||
setValue((i < availableValues.length - 1) ? i + 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void prev() {
|
||||
if (type == PropertyType.SELECT) {
|
||||
int i = (Integer) value;
|
||||
setValue((i > 0) ? i - 1 : availableValues.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelection(int selection) {
|
||||
if (type == PropertyType.SELECT && selection >= 0 && selection < availableValues.length) {
|
||||
setValue(selection);
|
||||
}
|
||||
}
|
||||
|
||||
public String getSelection() {
|
||||
return (type == PropertyType.SELECT) ? availableValues[(Integer) value] : null;
|
||||
}
|
||||
|
||||
// ---------- Lifecycle ----------
|
||||
public void reset() {
|
||||
value = defaultValue;
|
||||
isReset = true;
|
||||
}
|
||||
|
||||
public PropertyType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Property<?>> getNamespaceValue() {
|
||||
if (type == PropertyType.NAMESPACE && value instanceof Map) {
|
||||
return (Map<String, Property<?>>) value;
|
||||
}
|
||||
throw new IllegalStateException("Property is not a namespace");
|
||||
}
|
||||
|
||||
// ---------- JSON Serialization ----------
|
||||
/*
|
||||
* BOOLEAN,
|
||||
* INTEGER,
|
||||
* FLOAT,
|
||||
* STRING,
|
||||
* HEX,
|
||||
* NAMESPACE,
|
||||
* SELECT,
|
||||
* KEYBINDING,
|
||||
*/
|
||||
public JsonElement toJson() {
|
||||
switch (type) {
|
||||
case BOOLEAN:
|
||||
return new JsonPrimitive((Boolean) value);
|
||||
case INTEGER:
|
||||
return new JsonPrimitive((Integer) value);
|
||||
case FLOAT:
|
||||
return new JsonPrimitive((Float) value);
|
||||
case NAMESPACE:
|
||||
JsonObject nested = new JsonObject();
|
||||
getNamespaceValue().forEach((k, v) -> nested.add(k, v.toJson()));
|
||||
return nested;
|
||||
case HEX:
|
||||
return new JsonPrimitive(String.format("#%08X", (Integer) value));
|
||||
case KEYBINDING:
|
||||
return new JsonPrimitive((Integer) value);
|
||||
case SELECT:
|
||||
return new JsonPrimitive((Integer) value);
|
||||
default:
|
||||
return new JsonPrimitive(String.valueOf(value));
|
||||
}
|
||||
}
|
||||
|
||||
public void loadFromJson(JsonElement element) {
|
||||
if (element == null)
|
||||
return;
|
||||
|
||||
switch (type) {
|
||||
case BOOLEAN:
|
||||
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isBoolean())
|
||||
setValue(element.getAsBoolean());
|
||||
break;
|
||||
case INTEGER:
|
||||
if (element.isJsonPrimitive()) {
|
||||
JsonPrimitive p = element.getAsJsonPrimitive();
|
||||
if (p.isNumber())
|
||||
setValue(p.getAsInt());
|
||||
else if (p.isString())
|
||||
setValue(parseHexToInt(p.getAsString()));
|
||||
}
|
||||
break;
|
||||
case FLOAT:
|
||||
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
|
||||
setValue(element.getAsFloat());
|
||||
break;
|
||||
case STRING:
|
||||
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString())
|
||||
setValue(element.getAsString());
|
||||
break;
|
||||
case HEX:
|
||||
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isString())
|
||||
setValue(parseHexToInt(element.getAsString()));
|
||||
break;
|
||||
case NAMESPACE:
|
||||
if (element.isJsonObject()) {
|
||||
JsonObject obj = element.getAsJsonObject();
|
||||
getNamespaceValue().forEach((k, v) -> v.loadFromJson(obj.get(k)));
|
||||
}
|
||||
break;
|
||||
case SELECT:
|
||||
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
|
||||
setSelection(element.getAsInt());
|
||||
break;
|
||||
case KEYBINDING:
|
||||
if (element.isJsonPrimitive() && element.getAsJsonPrimitive().isNumber())
|
||||
setValue(element.getAsInt());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Utility ----------
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.valueOf(value);
|
||||
}
|
||||
|
||||
public static boolean valueIsNamespace(Object obj) {
|
||||
return obj instanceof Map;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setValue(Object value) {
|
||||
this.value = (T) value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setValue(Object value, Object defaultValue) {
|
||||
this.value = (T) value;
|
||||
this.defaultValue = (T) defaultValue;
|
||||
}
|
||||
|
||||
public NamedProperty<T> named(String name) {
|
||||
return new NamedProperty<>(name, this);
|
||||
}
|
||||
|
||||
// ---------- Keybindings ----------
|
||||
public boolean isKeyPressed() {
|
||||
return (Integer) value != -1 &&
|
||||
GLFW.glfwGetKey(SaturnClient.client.getWindow().getHandle(), (Integer) value) == GLFW.GLFW_PRESS &&
|
||||
SaturnClient.client.currentScreen == null;
|
||||
}
|
||||
|
||||
public boolean wasKeyPressed() {
|
||||
boolean pressed = isKeyPressed();
|
||||
boolean result = pressed && !wasPressedLastTick;
|
||||
wasPressedLastTick = pressed;
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------- HEX ----------
|
||||
public static int parseHexToInt(String hex) {
|
||||
hex = hex.replace("#", "");
|
||||
if (hex.length() == 6)
|
||||
hex = "FF" + hex;
|
||||
if (hex.length() != 8)
|
||||
throw new IllegalArgumentException("Hex must be 6 or 8 chars long, got \'" + hex + "\'");
|
||||
return (int) Long.parseLong(hex, 16);
|
||||
}
|
||||
|
||||
// Misc
|
||||
public Property<T> copy() {
|
||||
return new Property<>(this.value, this.type);
|
||||
}
|
||||
|
||||
public void load(String name, JsonElement element) {
|
||||
if (element != null && element.isJsonObject()) {
|
||||
load(name, element.getAsJsonObject());
|
||||
}
|
||||
}
|
||||
|
||||
public void load(String name, JsonObject element) {
|
||||
JsonElement value = element.get(name);
|
||||
if (value != null)
|
||||
this.loadFromJson(value);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package org.saturnclient.saturnclient.config;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
public class ThemeManager {
|
||||
private static File themeFile = new File(
|
||||
SaturnClient.client.runDirectory,
|
||||
"saturn.theme.json");
|
||||
private static List<ThemeManager> theme = new ArrayList<>();
|
||||
private static JsonObject cachedThemeJson = null;
|
||||
|
||||
private Map<String, Map<String, Object>> states;
|
||||
private Map<String, Property<?>> currentStyling;
|
||||
private String namespace;
|
||||
|
||||
public ThemeManager(String namespace, String... states) {
|
||||
this.namespace = namespace;
|
||||
this.states = new LinkedHashMap<>();
|
||||
this.currentStyling = new LinkedHashMap<>();
|
||||
this.states.put(null, new LinkedHashMap<>());
|
||||
for (String s : states) {
|
||||
this.states.put(s, new LinkedHashMap<>());
|
||||
}
|
||||
theme.add(this);
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
for (Map.Entry<String, Object> tProp : states.get(state).entrySet()) {
|
||||
currentStyling.get(tProp.getKey()).setValue(tProp.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public void applyState(String state) {
|
||||
for (Map.Entry<String, Object> tProp : states.get(state).entrySet()) {
|
||||
if (!states.get(null).get(tProp.getKey()).equals(tProp.getValue()))
|
||||
currentStyling.get(tProp.getKey()).setValue(tProp.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public <T> Property<T> property(String name, Property<T> prop) {
|
||||
for (Map.Entry<String, Map<String, Object>> state : this.states.entrySet()) {
|
||||
Property<?> property = prop.copy();
|
||||
if (cachedThemeJson == null) {
|
||||
load();
|
||||
}
|
||||
String stateName = state.getKey();
|
||||
property.load(name, cachedThemeJson.get(stateName != null ? namespace + "@" + stateName : namespace));
|
||||
state.getValue().put(name, property.value);
|
||||
}
|
||||
|
||||
currentStyling.put(name, prop);
|
||||
|
||||
return prop;
|
||||
}
|
||||
|
||||
public <T> void propertyStateDefault(String state, String name, T value) {
|
||||
Property<?> prop = currentStyling.get(name).copy();
|
||||
prop.setValue(value, value);
|
||||
if (cachedThemeJson == null) {
|
||||
load();
|
||||
}
|
||||
prop.load(name, cachedThemeJson.get(state != null ? namespace + "@" + state : namespace));
|
||||
states.get(state).put(name, prop.value);
|
||||
}
|
||||
|
||||
public static void load() {
|
||||
try {
|
||||
SaturnClient.LOGGER.info("Starting to load theme...");
|
||||
if (!themeFile.exists()) {
|
||||
themeFile.createNewFile();
|
||||
Files.write(themeFile.toPath(), "{}".getBytes());
|
||||
}
|
||||
|
||||
cachedThemeJson = JsonParser.parseString(
|
||||
new String(Files.readAllBytes(themeFile.toPath()))).getAsJsonObject();
|
||||
|
||||
SaturnClient.LOGGER.info("Theme loaded");
|
||||
} catch (IOException e) {
|
||||
SaturnClient.LOGGER.error("Error reading the theme file", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
package org.saturnclient.saturnclient.cosmetics;
|
||||
|
||||
import org.saturnclient.saturnclient.SaturnClientConfig;
|
||||
import org.saturnclient.saturnclient.auth.Auth;
|
||||
import org.saturnclient.saturnclient.cosmetics.cloaks.Cloaks;
|
||||
|
||||
import net.minecraft.client.render.OverlayTexture;
|
||||
import net.minecraft.client.render.RenderLayer;
|
||||
import net.minecraft.client.render.VertexConsumer;
|
||||
import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.render.entity.equipment.EquipmentModel;
|
||||
import net.minecraft.client.render.entity.equipment.EquipmentModelLoader;
|
||||
import net.minecraft.client.render.entity.equipment.EquipmentModel.LayerType;
|
||||
import net.minecraft.client.render.entity.feature.FeatureRenderer;
|
||||
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
|
||||
import net.minecraft.client.render.entity.model.PlayerEntityModel;
|
||||
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.component.DataComponentTypes;
|
||||
import net.minecraft.component.type.EquippableComponent;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.item.equipment.EquipmentAsset;
|
||||
import net.minecraft.registry.RegistryKey;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.math.RotationAxis;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
|
||||
/**
|
||||
* The cloak renderer
|
||||
* - **Efficient**: uses a image only consisting of the cloak texture, 16x8
|
||||
* whitespace.
|
||||
* - **Curving**: Curves when player velocity increases.
|
||||
* - **Wavey**: Makes the cloak feel like wind, when jump sprinting.
|
||||
*/
|
||||
public class CloakFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
|
||||
private final EquipmentModelLoader equipmentModelLoader;
|
||||
private static final int PARTS = 16;
|
||||
private float currentVelocity = 0.0f;
|
||||
|
||||
private static final float TEX_W = 176f;
|
||||
private static final float TEX_H = 138f;
|
||||
|
||||
private static final class PxRect {
|
||||
final int x, y, w, h;
|
||||
|
||||
PxRect(int x, int y, int w, int h) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.w = w;
|
||||
this.h = h;
|
||||
}
|
||||
|
||||
float u1() {
|
||||
return x / TEX_W;
|
||||
}
|
||||
|
||||
float v1() {
|
||||
return y / TEX_H;
|
||||
}
|
||||
|
||||
float u2() {
|
||||
return (x + w) / TEX_W;
|
||||
}
|
||||
|
||||
float v2() {
|
||||
return (y + h) / TEX_H;
|
||||
}
|
||||
}
|
||||
|
||||
private static final PxRect FRONT_RECT = new PxRect(8, 8, 80, 128);
|
||||
private static final PxRect BACK_RECT = new PxRect(96, 8, 80, 128);
|
||||
private static final PxRect LEFT_EDGE_RECT = new PxRect(0, 8, 8, 128);
|
||||
private static final PxRect RIGHT_EDGE_RECT = new PxRect(88, 8, 8, 128);
|
||||
private static final PxRect TOP_RECT = new PxRect(8, 0, 80, 8);
|
||||
private static final PxRect BOTTOM_RECT = new PxRect(88, 0, 80, 8);
|
||||
|
||||
public CloakFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context,
|
||||
EquipmentModelLoader equipmentModelLoader) {
|
||||
super(context);
|
||||
this.equipmentModelLoader = equipmentModelLoader;
|
||||
}
|
||||
|
||||
private boolean hasCustomModelForLayer(ItemStack stack, EquipmentModel.LayerType layerType) {
|
||||
EquippableComponent equippableComponent = (EquippableComponent) stack
|
||||
.get(DataComponentTypes.EQUIPPABLE);
|
||||
if (equippableComponent != null && !equippableComponent.assetId().isEmpty()) {
|
||||
EquipmentModel equipmentModel = this.equipmentModelLoader
|
||||
.get((RegistryKey<EquipmentAsset>) equippableComponent.assetId().get());
|
||||
return !equipmentModel.getLayers(layerType).isEmpty();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void renderCapeQuad(VertexConsumer vertexConsumer, MatrixStack matrixStack,
|
||||
Vec3d bottomLeft, Vec3d bottomRight, Vec3d topRight, Vec3d topLeft,
|
||||
float u1, float v1, float u2, float v2, int light, int overlay,
|
||||
float normalX, float normalY, float normalZ, boolean flipWinding) {
|
||||
MatrixStack.Entry entry = matrixStack.peek();
|
||||
|
||||
if (!flipWinding) {
|
||||
// CCW
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y,
|
||||
(float) bottomLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
|
||||
(float) bottomRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y,
|
||||
(float) topRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y,
|
||||
(float) topLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
} else {
|
||||
// CW
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topLeft.x, (float) topLeft.y,
|
||||
(float) topLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) topRight.x, (float) topRight.y,
|
||||
(float) topRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v1).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomRight.x, (float) bottomRight.y,
|
||||
(float) bottomRight.z)
|
||||
.color(255, 255, 255, 255).texture(u2, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
vertexConsumer.vertex(entry.getPositionMatrix(), (float) bottomLeft.x, (float) bottomLeft.y,
|
||||
(float) bottomLeft.z)
|
||||
.color(255, 255, 255, 255).texture(u1, v2).overlay(overlay).light(light)
|
||||
.normal(normalX, normalY, normalZ);
|
||||
}
|
||||
}
|
||||
|
||||
private float getCurve(int i, float curveMagnitude, PlayerEntityRenderState playerState) {
|
||||
float powCurve = (float) Math.pow((double) (PARTS - i) / PARTS, 2) * curveMagnitude;
|
||||
float waveFreq = 0.6f;
|
||||
float phase = (playerState.age + playerState.handSwingProgress) * 0.4f;
|
||||
float sineAmplitude = 0.02f;
|
||||
float sineCurve = (float) Math.sin(i * waveFreq + phase) * sineAmplitude;
|
||||
float t = Math.min(1.0f, Math.max(0.0f, (curveMagnitude - 0.3f) / 0.2f));
|
||||
t = t * t * (3 - 2 * t);
|
||||
return (1.0f - t) * powCurve + t * (powCurve + sineCurve);
|
||||
}
|
||||
|
||||
private void renderCape(MatrixStack matrixStack, VertexConsumer vertexConsumer,
|
||||
PlayerEntityRenderState playerState, int light, int overlay,
|
||||
float rotation, float curveMagnitude) {
|
||||
float capeWidth = 10.0f / 16.0f;
|
||||
float capeHeight = 16.0f / 16.0f;
|
||||
float capeDepth = 1.0f / 16.0f;
|
||||
float capePartHeight = capeHeight / PARTS;
|
||||
|
||||
matrixStack.push();
|
||||
matrixStack.translate(0.0f, 1.25, 0.125f);
|
||||
matrixStack.translate(0, -capeHeight, 0);
|
||||
matrixStack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(rotation));
|
||||
matrixStack.translate(0, capeHeight, 0);
|
||||
|
||||
float x1 = -capeWidth / 2.0f;
|
||||
float x2 = capeWidth / 2.0f;
|
||||
float y2 = -capeHeight;
|
||||
float z1 = 0.0f;
|
||||
float z2 = capeDepth;
|
||||
|
||||
final float frontU1 = FRONT_RECT.u1();
|
||||
final float frontU2 = FRONT_RECT.u2();
|
||||
final float frontV1 = FRONT_RECT.v1();
|
||||
final float frontV2 = FRONT_RECT.v2();
|
||||
final float frontPartV = (frontV2 - frontV1) / PARTS;
|
||||
|
||||
final float backU1 = BACK_RECT.u1();
|
||||
final float backU2 = BACK_RECT.u2();
|
||||
final float backV1 = BACK_RECT.v1();
|
||||
final float backV2 = BACK_RECT.v2();
|
||||
final float backPartV = (backV2 - backV1) / PARTS;
|
||||
|
||||
final float leftU1 = LEFT_EDGE_RECT.u1();
|
||||
final float leftU2 = LEFT_EDGE_RECT.u2();
|
||||
final float leftV1 = LEFT_EDGE_RECT.v1();
|
||||
final float leftV2 = LEFT_EDGE_RECT.v2();
|
||||
final float leftPartV = (leftV2 - leftV1) / PARTS;
|
||||
|
||||
final float rightU1 = RIGHT_EDGE_RECT.u1();
|
||||
final float rightU2 = RIGHT_EDGE_RECT.u2();
|
||||
final float rightV1 = RIGHT_EDGE_RECT.v1();
|
||||
final float rightV2 = RIGHT_EDGE_RECT.v2();
|
||||
final float rightPartV = (rightV2 - rightV1) / PARTS;
|
||||
|
||||
final float topU1 = TOP_RECT.u1();
|
||||
final float topU2 = TOP_RECT.u2();
|
||||
final float topV1 = TOP_RECT.v1();
|
||||
final float topV2 = TOP_RECT.v2();
|
||||
|
||||
final float bottomU1 = BOTTOM_RECT.u1();
|
||||
final float bottomU2 = BOTTOM_RECT.u2();
|
||||
final float bottomV1 = BOTTOM_RECT.v1();
|
||||
final float bottomV2 = BOTTOM_RECT.v2();
|
||||
|
||||
for (int i = 0; i < PARTS; i++) {
|
||||
float y3 = -(capePartHeight * i);
|
||||
float y4 = -(capePartHeight * (i + 1));
|
||||
|
||||
float curveOffsetTop = getCurve(i, curveMagnitude, playerState);
|
||||
float curveOffsetBottom = getCurve(i + 1, curveMagnitude, playerState);
|
||||
|
||||
// FRONT FACE (outside)
|
||||
this.renderCapeQuad(
|
||||
vertexConsumer, matrixStack,
|
||||
new Vec3d(x2, y4, z2 + curveOffsetBottom),
|
||||
new Vec3d(x1, y4, z2 + curveOffsetBottom),
|
||||
new Vec3d(x1, y3, z2 + curveOffsetTop), new Vec3d(x2, y3, z2 + curveOffsetTop),
|
||||
frontU1, frontV2 - (frontPartV * i),
|
||||
frontU2, frontV2 - (frontPartV * (i + 1)),
|
||||
light, overlay,
|
||||
0.0f, 0.0f, 1.0f, false);
|
||||
|
||||
// BACK FACE (inside)
|
||||
this.renderCapeQuad(
|
||||
vertexConsumer, matrixStack,
|
||||
new Vec3d(x1, y4, z1 + curveOffsetBottom),
|
||||
new Vec3d(x2, y4, z1 + curveOffsetBottom),
|
||||
new Vec3d(x2, y3, z1 + curveOffsetTop), new Vec3d(x1, y3, z1 + curveOffsetTop),
|
||||
backU1, backV2 - (backPartV * i),
|
||||
backU2, backV2 - (backPartV * (i + 1)),
|
||||
light, overlay,
|
||||
0.0f, 0.0f, 1.0f, false);
|
||||
|
||||
// LEFT FACE (thickness at x2)
|
||||
this.renderCapeQuad(
|
||||
vertexConsumer, matrixStack,
|
||||
new Vec3d(x2, y4, z1 + curveOffsetBottom),
|
||||
new Vec3d(x2, y4, z2 + curveOffsetBottom),
|
||||
new Vec3d(x2, y3, z2 + curveOffsetTop), new Vec3d(x2, y3, z1 + curveOffsetTop),
|
||||
leftU1, leftV2 - (leftPartV * i),
|
||||
leftU2, leftV2 - (leftPartV * (i + 1)),
|
||||
light, overlay,
|
||||
1.0f, 0.0f, 0.0f, false);
|
||||
|
||||
// RIGHT FACE (thickness at x1)
|
||||
this.renderCapeQuad(
|
||||
vertexConsumer, matrixStack,
|
||||
new Vec3d(x1, y4, z2 + curveOffsetBottom),
|
||||
new Vec3d(x1, y4, z1 + curveOffsetBottom),
|
||||
new Vec3d(x1, y3, z1 + curveOffsetTop), new Vec3d(x1, y3, z2 + curveOffsetTop),
|
||||
rightU1, rightV2 - (rightPartV * i),
|
||||
rightU2, rightV2 - (rightPartV * (i + 1)),
|
||||
light, overlay,
|
||||
-1.0f, 0.0f, 0.0f, false);
|
||||
}
|
||||
|
||||
// TOP FACE (shoulders edge) – use its dedicated strip
|
||||
matrixStack.push();
|
||||
matrixStack.translate(0.0f, y2 + capeDepth, 0.0f);
|
||||
matrixStack.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(90.0f));
|
||||
matrixStack.translate(0.0f, -y2, 0.0f);
|
||||
this.renderCapeQuad(
|
||||
vertexConsumer, matrixStack,
|
||||
new Vec3d(x2, y2 + capeDepth, z2), new Vec3d(x1, y2 + capeDepth, z2),
|
||||
new Vec3d(x1, y2, z2), new Vec3d(x2, y2, z2),
|
||||
topU1, topV1, topU2, topV2,
|
||||
light, overlay,
|
||||
0.0f, 1.0f, 0.0f, true);
|
||||
matrixStack.pop();
|
||||
|
||||
// BOTTOM FACE – use bottom strip
|
||||
float curveOffset = getCurve(0, curveMagnitude, playerState);
|
||||
this.renderCapeQuad(
|
||||
vertexConsumer, matrixStack,
|
||||
new Vec3d(x2, 0.0f, z2 + curveOffset), new Vec3d(x1, 0.0f, z2 + curveOffset),
|
||||
new Vec3d(x1, 0.0f, z1 + curveOffset), new Vec3d(x2, 0.0f, z1 + curveOffset),
|
||||
bottomU1, bottomV1, bottomU2, bottomV2,
|
||||
light, overlay,
|
||||
0.0f, 0.0f, 1.0f, false);
|
||||
|
||||
matrixStack.pop();
|
||||
}
|
||||
|
||||
public void render(MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int light,
|
||||
PlayerEntityRenderState playerEntityRenderState, float f, float g) {
|
||||
if (playerEntityRenderState.invisible || !playerEntityRenderState.capeVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (playerEntityRenderState.skinTextures.capeTexture() != null) {
|
||||
return;
|
||||
}
|
||||
String name = playerEntityRenderState.name;
|
||||
String uuid = Auth.playerNames.get(name);
|
||||
if (uuid == null || !Auth.players.containsKey(uuid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Identifier customCape = Cloaks.getCurrentCloakTexture(uuid);
|
||||
if (customCape == null || this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack,
|
||||
LayerType.WINGS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int minBrightness = 7;
|
||||
int blockLight = (light >> 4) & 0xF;
|
||||
int skyLight = (light >> 20) & 0xF;
|
||||
blockLight = Math.max(blockLight, minBrightness);
|
||||
skyLight = Math.max(skyLight, minBrightness);
|
||||
light = (skyLight << 20) | (blockLight << 4);
|
||||
|
||||
matrixStack.push();
|
||||
VertexConsumer vertexConsumer = vertexConsumerProvider
|
||||
.getBuffer(RenderLayer.getEntityAlpha(customCape));
|
||||
|
||||
float targetVelocity = ((6.0F + playerEntityRenderState.field_53537 / 2.0F
|
||||
+ playerEntityRenderState.field_53536) * 0.02f);
|
||||
targetVelocity = Math.max(0.0f, targetVelocity - 0.1f);
|
||||
|
||||
float accelerationRate = 0.02f;
|
||||
this.currentVelocity = this.currentVelocity
|
||||
+ (targetVelocity - this.currentVelocity) * accelerationRate;
|
||||
|
||||
float rotation = this.currentVelocity * 72.0f;
|
||||
float curve = this.currentVelocity * 0.4f;
|
||||
|
||||
if (playerEntityRenderState.sneaking && playerEntityRenderState.isInSneakingPose) {
|
||||
matrixStack.translate(0, 0.24f, 0);
|
||||
matrixStack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(27.0f));
|
||||
curve += 0.2f;
|
||||
}
|
||||
|
||||
matrixStack.translate(0.0f, -0.25f, 0.0f);
|
||||
if (this.hasCustomModelForLayer(playerEntityRenderState.equippedChestStack,
|
||||
LayerType.HUMANOID)) {
|
||||
matrixStack.translate(0.0F, -0.053125F, 0.06875F);
|
||||
}
|
||||
|
||||
if (playerEntityRenderState.isSwimming) {
|
||||
curve = 0.0f;
|
||||
rotation = 0.0f;
|
||||
}
|
||||
|
||||
if (!SaturnClientConfig.bendyCloaks.value) {
|
||||
curve = 0.0f;
|
||||
}
|
||||
|
||||
renderCape(matrixStack, vertexConsumer, playerEntityRenderState, light,
|
||||
OverlayTexture.DEFAULT_UV, Math.min(rotation, 60.0f), Math.min(curve, 0.5f));
|
||||
|
||||
matrixStack.pop();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package org.saturnclient.saturnclient.cosmetics;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Emotes {
|
||||
public static List<String> availableEmotes = new ArrayList<>();
|
||||
|
||||
public static void initialize() {
|
||||
availableEmotes.add("tpose");
|
||||
availableEmotes.add("back_flip");
|
||||
availableEmotes.add("facepalm");
|
||||
availableEmotes.add("cry");
|
||||
availableEmotes.add("giga_chad");
|
||||
availableEmotes.add("front_flip");
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package org.saturnclient.saturnclient.cosmetics;
|
||||
|
||||
import org.saturnclient.saturnclient.auth.SaturnPlayer;
|
||||
import org.saturnclient.saturnclient.auth.Auth;
|
||||
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
import net.minecraft.client.render.OverlayTexture;
|
||||
import net.minecraft.client.render.VertexConsumerProvider;
|
||||
import net.minecraft.client.render.entity.feature.FeatureRenderer;
|
||||
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
|
||||
import net.minecraft.client.render.entity.model.PlayerEntityModel;
|
||||
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
|
||||
import net.minecraft.client.render.item.ItemRenderer;
|
||||
import net.minecraft.client.util.math.MatrixStack;
|
||||
import net.minecraft.component.DataComponentTypes;
|
||||
import net.minecraft.component.type.EquippableComponent;
|
||||
import net.minecraft.entity.EquipmentSlot;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.item.Items;
|
||||
import net.minecraft.item.ModelTransformationMode;
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
public class HatFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
|
||||
public HatFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light,
|
||||
PlayerEntityRenderState state, float limbAngle, float limbDistance) {
|
||||
ItemStack headItem = state.equippedHeadStack;
|
||||
String uuid = Auth.playerNames.get(state.name);
|
||||
if (headItem.isEmpty() && uuid != null && !state.invisible) {
|
||||
SaturnPlayer player = Auth.players.get(uuid);
|
||||
if (player == null || player.hat == null || player.hat.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
matrices.push();
|
||||
|
||||
this.getContextModel().head.rotate(matrices);
|
||||
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_X.rotationDegrees(180.0f));
|
||||
matrices.multiply(net.minecraft.util.math.RotationAxis.POSITIVE_Y.rotationDegrees(180.0f));
|
||||
|
||||
ItemStack customHat = new ItemStack(Items.STICK);
|
||||
customHat.set(DataComponentTypes.ITEM_MODEL,
|
||||
Identifier.of("saturnclient:hat_" + player.hat));
|
||||
customHat.set(DataComponentTypes.EQUIPPABLE,
|
||||
EquippableComponent.builder(EquipmentSlot.HEAD).build());
|
||||
|
||||
ItemRenderer itemRenderer = SaturnClient.client.getItemRenderer();
|
||||
|
||||
itemRenderer.renderItem(
|
||||
customHat,
|
||||
ModelTransformationMode.THIRD_PERSON_RIGHT_HAND,
|
||||
light,
|
||||
OverlayTexture.DEFAULT_UV,
|
||||
matrices,
|
||||
vertexConsumers,
|
||||
null,
|
||||
0);
|
||||
|
||||
matrices.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package org.saturnclient.saturnclient.cosmetics;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.saturnclient.saturnclient.auth.SaturnPlayer;
|
||||
import org.saturnclient.saturnclient.auth.Auth;
|
||||
|
||||
public class Hats {
|
||||
public static final String[] ALL_HATS = { "horns_black", "horns_white", "halo", "halo_black" };
|
||||
public static List<String> availableHats = new ArrayList<>();
|
||||
|
||||
public static void initialize() {
|
||||
availableHats.add(0, "");
|
||||
}
|
||||
|
||||
public static void setHat(String uuid, String hatName) {
|
||||
setHatSilent(uuid, hatName);
|
||||
Auth.setHat(hatName);
|
||||
}
|
||||
|
||||
public static void setHatSilent(String uuid, String hatName) {
|
||||
SaturnPlayer player = Auth.players.get(uuid);
|
||||
if (player == null) {
|
||||
Auth.players.put(uuid, new SaturnPlayer("", hatName));
|
||||
} else {
|
||||
player.hat = hatName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
package org.saturnclient.saturnclient.cosmetics.cloaks;
|
||||
|
||||
import net.minecraft.util.Identifier;
|
||||
import org.saturnclient.saturnclient.auth.Auth;
|
||||
import org.saturnclient.saturnclient.cosmetics.cloaks.utils.AnimatedCloakData;
|
||||
import org.saturnclient.saturnclient.cosmetics.cloaks.utils.IdentifierUtils;
|
||||
import org.saturnclient.saturnclient.auth.SaturnPlayer;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.saturnclient.saturnclient.SaturnClient;
|
||||
|
||||
/**
|
||||
* Manages the cloak system for Saturn Client.
|
||||
* Handles loading and caching of player cloaks.
|
||||
* Originally created by IIpho3nix and modified for Saturn Client by leo.
|
||||
*/
|
||||
public class Cloaks {
|
||||
public static final String[] ALL_CLOAKS = { "glitch", "mercedes_flow", "crimson_mark", "bmw", "amg", "amg_petronas",
|
||||
"ferrari", "redbull", "black_hole_amethyst", "black_hole_flame", "black_hole_white" };
|
||||
private static final String[] ANIMATED_CLOAKS = { "glitch", "black_hole_amethyst", "black_hole_flame",
|
||||
"black_hole_white" };
|
||||
|
||||
private static final String CLOAKS_RESOURCE_PATH = "assets/saturnclient/textures/cloaks/";
|
||||
public static final List<String> availableCloaks = new ArrayList<>();
|
||||
public static Identifier cloakCacheIdentifier = null;
|
||||
public static final Map<String, List<AnimatedCloakData>> animatedCloaks = new ConcurrentHashMap<>();
|
||||
private static final Map<String, Long> lastFrameTime = new ConcurrentHashMap<>();
|
||||
|
||||
private static final ExecutorService CLOAK_LOADER_EXECUTOR = Executors.newFixedThreadPool(
|
||||
Math.min(4, Runtime.getRuntime().availableProcessors()),
|
||||
r -> {
|
||||
Thread t = new Thread(r, "cloak-loader");
|
||||
t.setDaemon(true);
|
||||
t.setPriority(Thread.NORM_PRIORITY - 1);
|
||||
return t;
|
||||
});
|
||||
|
||||
private static final ConcurrentHashMap<String, List<AnimatedCloakData>> CLOAK_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Initializes the cloak system.
|
||||
* Loads cloak textures from resources.
|
||||
*/
|
||||
public static void initialize() {
|
||||
availableCloaks.add(0, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles loading and caching of a new cloak texture.
|
||||
*
|
||||
* @param cloakName Name of the cloak file to load
|
||||
*/
|
||||
public static void setCloak(String uuid, String cloakName) {
|
||||
Auth.setCloak(cloakName);
|
||||
setCloakSilent(uuid, cloakName);
|
||||
SaturnClient.LOGGER.info("Cloak set to " + cloakName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets cloak of the current player
|
||||
*
|
||||
* @param cloakName Name of the cloak file to load
|
||||
*/
|
||||
public static void setCloak(String cloakName) {
|
||||
if (availableCloaks.contains(cloakName)) {
|
||||
setCloak(Auth.uuid, cloakName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles loading and caching of a new cloak texture, without connecting to the
|
||||
* server.
|
||||
*
|
||||
* @param cloakName Name of the cloak file to load
|
||||
*/
|
||||
public static void setCloakSilent(String uuid, String cloakName) {
|
||||
SaturnPlayer player = Auth.players.get(uuid);
|
||||
if (player == null) {
|
||||
Auth.players.put(uuid, new SaturnPlayer(cloakName, null));
|
||||
} else {
|
||||
player.cloak = cloakName;
|
||||
}
|
||||
|
||||
loadCloak(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles loading and caching of a new cloak texture, without connecting to the
|
||||
* server.
|
||||
*
|
||||
* @param cloakName Name of the cloak file to load
|
||||
*/
|
||||
public static void loadCloak(String uuid) {
|
||||
SaturnPlayer player = Auth.players.get(uuid);
|
||||
if (player != null && player.cloak != null) {
|
||||
if (!player.cloak.isEmpty()) {
|
||||
if (Arrays.asList(ANIMATED_CLOAKS).contains(player.cloak)) {
|
||||
loadAnimatedCloakAsync(uuid, player.cloak)
|
||||
.exceptionally(throwable -> {
|
||||
SaturnClient.LOGGER.error("Failed to load animated cloak for player: " + player.cloak,
|
||||
throwable);
|
||||
return null;
|
||||
});
|
||||
|
||||
} else {
|
||||
SaturnClient.client.execute(() -> loadStaticCloak(player.cloak + ".png"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and processes a static cloak from a PNG file in resources.
|
||||
*
|
||||
* @param fileName Name of the PNG file to load
|
||||
*/
|
||||
private static void loadStaticCloak(String fileName) {
|
||||
try {
|
||||
String resourcePath = CLOAKS_RESOURCE_PATH + fileName;
|
||||
InputStream inputStream = Cloaks.class.getClassLoader().getResourceAsStream(resourcePath);
|
||||
|
||||
if (inputStream != null) {
|
||||
BufferedImage image = ImageIO.read(inputStream);
|
||||
|
||||
// Add debug logging for static cloak
|
||||
SaturnClient.LOGGER.info("Static cloak dimensions: " + image.getWidth() + "x" + image.getHeight());
|
||||
|
||||
String safeFileName = fileName.toLowerCase(Locale.ROOT)
|
||||
.replace(' ', '_')
|
||||
.replaceAll("[^a-z0-9/._-]", "");
|
||||
|
||||
cloakCacheIdentifier = Identifier.of(SaturnClient.MOD_ID, "cloaks_" + safeFileName);
|
||||
IdentifierUtils.registerBufferedImageTexture(cloakCacheIdentifier, image);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
SaturnClient.LOGGER.error("Failed to load static cloak from resources: " + fileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static CompletableFuture<Void> loadAnimatedCloakAsync(String uuid, String cloakName) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
String fileName = cloakName + ".gif";
|
||||
|
||||
// Check cache first
|
||||
List<AnimatedCloakData> cached = CLOAK_CACHE.get(fileName);
|
||||
if (cached != null) {
|
||||
animatedCloaks.put(uuid, cached);
|
||||
lastFrameTime.put(uuid, System.currentTimeMillis());
|
||||
return;
|
||||
}
|
||||
|
||||
String resourcePath = CLOAKS_RESOURCE_PATH + fileName;
|
||||
|
||||
try (InputStream inputStream = Cloaks.class.getClassLoader().getResourceAsStream(resourcePath)) {
|
||||
if (inputStream == null) {
|
||||
SaturnClient.LOGGER.warn("Cloak resource not found: " + resourcePath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read all bytes at once - more efficient than multiple reads
|
||||
byte[] data = inputStream.readAllBytes();
|
||||
GifDecoder.GifImage gif = GifDecoder.read(data);
|
||||
|
||||
int frameCount = gif.getFrameCount();
|
||||
if (frameCount == 0) {
|
||||
SaturnClient.LOGGER.warn("No frames found in animated cloak: " + fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-allocate collections with known size
|
||||
List<AnimatedCloakData> animatedFrames = new ArrayList<>(frameCount);
|
||||
String baseFrameId = fileName.replace(".gif", "");
|
||||
|
||||
// Process frames in batch
|
||||
for (int i = 0; i < frameCount; i++) {
|
||||
BufferedImage frame = gif.getFrame(i);
|
||||
int delay = Math.max(gif.getDelay(i) * 10, 50); // Minimum 50ms delay
|
||||
|
||||
String frameId = baseFrameId + "_frame_" + i;
|
||||
Identifier frameIdentifier = Identifier.of(SaturnClient.MOD_ID, "cloaks_" + frameId);
|
||||
|
||||
try {
|
||||
IdentifierUtils.registerBufferedImageTextureFast(frameIdentifier, frame);
|
||||
// Register texture on main thread if required by the graphics system
|
||||
if (SaturnClient.client.isOnThread()) {
|
||||
// IdentifierUtils.registerBufferedImageTexture(frameIdentifier, frame);
|
||||
} else {
|
||||
// Queue for main thread execution
|
||||
// SaturnClient.client.execute(
|
||||
// () -> IdentifierUtils.registerBufferedImageTexture(frameIdentifier, frame));
|
||||
}
|
||||
|
||||
animatedFrames.add(new AnimatedCloakData(frameIdentifier, delay));
|
||||
} catch (Exception e) {
|
||||
SaturnClient.LOGGER.error("Failed to register frame " + i + " for cloak: " + fileName, e);
|
||||
// Continue with other frames instead of failing completely
|
||||
}
|
||||
}
|
||||
|
||||
if (!animatedFrames.isEmpty()) {
|
||||
// Cache the result for future use
|
||||
CLOAK_CACHE.put(fileName, animatedFrames);
|
||||
animatedCloaks.put(uuid, animatedFrames);
|
||||
lastFrameTime.put(uuid, System.currentTimeMillis());
|
||||
|
||||
SaturnClient.LOGGER.info("Loaded {} frames for animated cloak: {} (cached)",
|
||||
animatedFrames.size(), fileName);
|
||||
} else {
|
||||
SaturnClient.LOGGER.error("No valid frames could be loaded for cloak: " + fileName);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
SaturnClient.LOGGER.error("Failed to load animated cloak from resources: " + fileName, e);
|
||||
} catch (Exception e) {
|
||||
SaturnClient.LOGGER.error("Unexpected error loading animated cloak: " + fileName, e);
|
||||
}
|
||||
}, CLOAK_LOADER_EXECUTOR);
|
||||
}
|
||||
|
||||
public static Identifier getCurrentCloakTexture(String uuid) {
|
||||
if (!Auth.players.containsKey(uuid)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String cloakName = Auth.players.get(uuid).cloak;
|
||||
|
||||
if (cloakName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Arrays.asList(ANIMATED_CLOAKS).contains(cloakName)) {
|
||||
List<AnimatedCloakData> frames = animatedCloaks.get(uuid);
|
||||
if (frames == null || frames.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long lastTime = lastFrameTime.getOrDefault(uuid, currentTime);
|
||||
int currentFrame = 0;
|
||||
|
||||
long elapsedTime = currentTime - lastTime;
|
||||
long totalTime = 0;
|
||||
for (int i = 0; i < frames.size(); i++) {
|
||||
totalTime += frames.get(i).getDelayMs();
|
||||
if (elapsedTime < totalTime) {
|
||||
currentFrame = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (elapsedTime >= totalTime) {
|
||||
lastFrameTime.put(uuid, currentTime);
|
||||
currentFrame = 0;
|
||||
}
|
||||
|
||||
Identifier identifier = frames.get(currentFrame).getTextureId();
|
||||
|
||||
if (identifier == null) {
|
||||
return Identifier.of(SaturnClient.MOD_ID, "textures/cloaks/" + cloakName + ".png");
|
||||
}
|
||||
|
||||
return identifier;
|
||||
} else {
|
||||
return Identifier.of(SaturnClient.MOD_ID, "textures/cloaks/" + cloakName + ".png");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,791 +0,0 @@
|
||||
package org.saturnclient.saturnclient.cosmetics.cloaks;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
/*
|
||||
* Copyright 2014 Dhyan Blum
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A thread-safe decoder capable of processing a GIF data stream to render the graphics
|
||||
* contained in it. This implementation follows the official
|
||||
* <A HREF="http://www.w3.org/Graphics/GIF/spec-gif89a.txt">GIF
|
||||
* specification</A>.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Example usage:
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
*
|
||||
* <pre>
|
||||
* final GifImage gifImage = GifDecoder.read(int[] data);
|
||||
* final int width = gifImage.getWidth();
|
||||
* final int height = gifImage.getHeight();
|
||||
* final int frameCount = gifImage.getFrameCount();
|
||||
* for (int i = 0; i < frameCount; i++) {
|
||||
* final BufferedImage image = gifImage.getFrame(i);
|
||||
* final int delay = gif.getDelay(i);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* </p>
|
||||
*
|
||||
* @author Dhyan Blum
|
||||
* @version 1.09 November 2017 (Thread-safe version)
|
||||
*/
|
||||
public final class GifDecoder {
|
||||
|
||||
// Thread-local storage for reusable objects to avoid synchronization overhead
|
||||
private static final ThreadLocal<DecodingContext> DECODING_CONTEXT =
|
||||
ThreadLocal.withInitial(DecodingContext::new);
|
||||
|
||||
/**
|
||||
* Thread-local context containing reusable objects for decoding operations.
|
||||
* This eliminates the need for synchronization while maintaining performance benefits
|
||||
* of object reuse within each thread.
|
||||
*/
|
||||
static final class DecodingContext {
|
||||
final BitReader bitReader = new BitReader();
|
||||
final CodeTable codeTable = new CodeTable();
|
||||
int[] pixelBuffer;
|
||||
int[] prevPixelBuffer;
|
||||
int[] tempBuffer;
|
||||
|
||||
// Get or create a pixel buffer of the specified size
|
||||
int[] getPixelBuffer(int size) {
|
||||
if (pixelBuffer == null || pixelBuffer.length < size) {
|
||||
pixelBuffer = new int[Math.max(size, 1024)]; // Minimum size to reduce allocations
|
||||
}
|
||||
return pixelBuffer;
|
||||
}
|
||||
|
||||
// Get or create a previous pixel buffer of the specified size
|
||||
int[] getPrevPixelBuffer(int size) {
|
||||
if (prevPixelBuffer == null || prevPixelBuffer.length < size) {
|
||||
prevPixelBuffer = new int[Math.max(size, 1024)];
|
||||
}
|
||||
return prevPixelBuffer;
|
||||
}
|
||||
|
||||
// Get or create a temporary buffer of the specified size
|
||||
int[] getTempBuffer(int size) {
|
||||
if (tempBuffer == null || tempBuffer.length < size) {
|
||||
tempBuffer = new int[Math.max(size, 1024)];
|
||||
}
|
||||
return tempBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
static final class BitReader {
|
||||
private int nextBitToRead;
|
||||
private int numberOfBitsToRead;
|
||||
private int bitMask; // Used to kill unwanted higher bits
|
||||
private byte[] bytes; // Data array
|
||||
|
||||
// To avoid costly bounds checks, 'in' needs 2 more 0-bytes at the end
|
||||
private void init(final byte[] bytes) {
|
||||
this.bytes = bytes;
|
||||
nextBitToRead = 0;
|
||||
}
|
||||
|
||||
private int read() {
|
||||
// Byte indices: (bitPos / 8), (bitPos / 8) + 1, (bitPos / 8) + 2
|
||||
final int byteIndex = nextBitToRead >>> 3; // Byte = bit / 8
|
||||
final int bitsToShiftRight = nextBitToRead & 7; // & 7 is the same as MODULO 8
|
||||
|
||||
// Use int operations to avoid repeated array access
|
||||
final int byte0 = bytes[byteIndex] & 0xFF;
|
||||
final int byte1 = bytes[byteIndex + 1] & 0xFF;
|
||||
final int byte2 = bytes[byteIndex + 2] & 0xFF;
|
||||
|
||||
// Optimized bit manipulation
|
||||
final int buffer = ((byte2 << 16) | (byte1 << 8) | byte0) >>> bitsToShiftRight;
|
||||
nextBitToRead += numberOfBitsToRead;
|
||||
return buffer & bitMask; // Kill the unwanted higher bits
|
||||
}
|
||||
|
||||
private void setNumberOfBitsToRead(final int numberOfBitsToRead) {
|
||||
this.numberOfBitsToRead = numberOfBitsToRead;
|
||||
bitMask = (1 << numberOfBitsToRead) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
static final class CodeTable {
|
||||
private final int[][] table; // Maps codes to lists of colors
|
||||
private int initTableSize; // Number of colors +2 for CLEAR + EOI
|
||||
private int initCodeSize; // Initial code size
|
||||
private int initCodeLimit; // First code limit
|
||||
private int codeSize; // Current code size, maximum is 12 bits
|
||||
private int nextCode; // Next available code for a new entry
|
||||
private int nextCodeLimit; // Increase codeSize when nextCode == limit
|
||||
private BitReader bitReader; // Notify when code sizes increases
|
||||
|
||||
public CodeTable() {
|
||||
table = new int[4096][];
|
||||
}
|
||||
|
||||
private int add(final int[] indices) {
|
||||
if (nextCode < 4096) {
|
||||
if (nextCode == nextCodeLimit && codeSize < 12) {
|
||||
codeSize++; // Max code size is 12
|
||||
bitReader.setNumberOfBitsToRead(codeSize);
|
||||
nextCodeLimit = (1 << codeSize) - 1; // 2^codeSize - 1
|
||||
}
|
||||
table[nextCode++] = indices;
|
||||
}
|
||||
return codeSize;
|
||||
}
|
||||
|
||||
private int clear() {
|
||||
codeSize = initCodeSize;
|
||||
bitReader.setNumberOfBitsToRead(codeSize);
|
||||
nextCodeLimit = initCodeLimit;
|
||||
nextCode = initTableSize; // Don't recreate table, reset pointer
|
||||
return codeSize;
|
||||
}
|
||||
|
||||
private void init(final GifFrame fr, final int[] activeColTbl, final BitReader br) {
|
||||
this.bitReader = br;
|
||||
final int numColors = activeColTbl.length;
|
||||
initCodeSize = fr.firstCodeSize;
|
||||
initCodeLimit = (1 << initCodeSize) - 1; // 2^initCodeSize - 1
|
||||
initTableSize = fr.endOfInfoCode + 1;
|
||||
nextCode = initTableSize;
|
||||
|
||||
// Initialize single color entries
|
||||
for (int c = 0; c < numColors; c++) {
|
||||
table[c] = new int[]{activeColTbl[c]}; // Single element array
|
||||
}
|
||||
|
||||
table[fr.clearCode] = new int[] { fr.clearCode }; // CLEAR
|
||||
table[fr.endOfInfoCode] = new int[] { fr.endOfInfoCode }; // EOI
|
||||
|
||||
// Locate transparent color in code table and set to 0
|
||||
if (fr.transpColFlag && fr.transpColIndex < numColors) {
|
||||
table[fr.transpColIndex] = new int[]{0};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class GifFrame {
|
||||
// Graphic control extension (optional)
|
||||
// Disposal: 0=NO_ACTION, 1=NO_DISPOSAL, 2=RESTORE_BG, 3=RESTORE_PREV
|
||||
private volatile int disposalMethod; // 0-3 as above, 4-7 undefined
|
||||
private volatile boolean transpColFlag; // 1 Bit
|
||||
private volatile int delay; // Unsigned, LSByte first, n * 1/100 * s
|
||||
private volatile int transpColIndex; // 1 Byte
|
||||
// Image descriptor
|
||||
private volatile int x; // Position on the canvas from the left
|
||||
private volatile int y; // Position on the canvas from the top
|
||||
private volatile int w; // May be smaller than the base image
|
||||
private volatile int h; // May be smaller than the base image
|
||||
private volatile int wh; // width * height
|
||||
private volatile boolean hasLocColTbl; // Has local color table? 1 Bit
|
||||
private volatile boolean interlaceFlag; // Is an interlace image? 1 Bit
|
||||
@SuppressWarnings("unused")
|
||||
private volatile boolean sortFlag; // True if local colors are sorted, 1 Bit
|
||||
private volatile int sizeOfLocColTbl; // Size of the local color table, 3 Bits
|
||||
private volatile int[] localColTbl; // Local color table (optional)
|
||||
// Image data
|
||||
private volatile int firstCodeSize; // LZW minimum code size + 1 for CLEAR & EOI
|
||||
private volatile int clearCode;
|
||||
private volatile int endOfInfoCode;
|
||||
private volatile byte[] data; // Holds LZW encoded data
|
||||
private volatile BufferedImage img; // Full drawn image, not just the frame area
|
||||
|
||||
// Synchronization for frame image creation
|
||||
private final Object imageLock = new Object();
|
||||
}
|
||||
|
||||
public final class GifImage {
|
||||
public volatile String header; // Bytes 0-5, GIF87a or GIF89a
|
||||
private volatile int w; // Unsigned 16 Bit, the least significant byte first
|
||||
private volatile int h; // Unsigned 16 Bit, the least significant byte first
|
||||
private volatile int wh; // Image width * image height
|
||||
public volatile boolean hasGlobColTbl; // 1 Bit
|
||||
public volatile int colorResolution; // 3 Bits
|
||||
public volatile boolean sortFlag; // True if global colors are sorted, 1 Bit
|
||||
public volatile int sizeOfGlobColTbl; // 2^(val(3 Bits) + 1), see spec
|
||||
public volatile int bgColIndex; // Background color index, 1 Byte
|
||||
public volatile int pxAspectRatio; // Pixel aspect ratio, 1 Byte
|
||||
public volatile int[] globalColTbl; // Global color table
|
||||
private final List<GifFrame> frames = new ArrayList<>(64);
|
||||
public volatile String appId = ""; // 8 Bytes at in[i+3], usually "NETSCAPE"
|
||||
public volatile String appAuthCode = ""; // 3 Bytes at in[i+11], usually "2.0"
|
||||
public volatile int repetitions = 0; // 0: infinite loop, N: number of loops
|
||||
|
||||
// Thread-safe frame rendering state
|
||||
private volatile BufferedImage img = null; // Currently, drawn frame
|
||||
private volatile Graphics2D g;
|
||||
private final ReadWriteLock renderLock = new ReentrantReadWriteLock();
|
||||
|
||||
// Synchronization objects
|
||||
private final Object initLock = new Object();
|
||||
|
||||
private int[] decode(final GifFrame fr, final int[] activeColTbl) {
|
||||
final DecodingContext ctx = DECODING_CONTEXT.get();
|
||||
ctx.codeTable.init(fr, activeColTbl, ctx.bitReader);
|
||||
ctx.bitReader.init(fr.data); // Incoming codes
|
||||
final int clearCode = fr.clearCode, endCode = fr.endOfInfoCode;
|
||||
|
||||
// Use thread-local pixel buffer
|
||||
final int[] out = ctx.getPixelBuffer(wh);
|
||||
|
||||
final int[][] tbl = ctx.codeTable.table; // Code table
|
||||
int outPos = 0; // Next pixel position in the output image array
|
||||
ctx.codeTable.clear(); // Init code table
|
||||
ctx.bitReader.read(); // Skip leading clear code
|
||||
int code = ctx.bitReader.read(); // Read first code
|
||||
int[] pixels = tbl[code]; // Output pixel for first code
|
||||
|
||||
// Use System.arraycopy for better performance
|
||||
System.arraycopy(pixels, 0, out, outPos, pixels.length);
|
||||
outPos += pixels.length;
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
final int prevCode = code;
|
||||
code = ctx.bitReader.read(); // Get next code in stream
|
||||
if (code == clearCode) { // After a CLEAR table, there is
|
||||
ctx.codeTable.clear(); // no previous code, we need to read
|
||||
code = ctx.bitReader.read(); // a new one
|
||||
pixels = tbl[code]; // Output pixels
|
||||
System.arraycopy(pixels, 0, out, outPos, pixels.length);
|
||||
outPos += pixels.length;
|
||||
continue; // Back to the loop with a valid previous code
|
||||
} else if (code == endCode) {
|
||||
break;
|
||||
}
|
||||
final int[] prevVals = tbl[prevCode];
|
||||
final int[] prevValsAndK = new int[prevVals.length + 1];
|
||||
System.arraycopy(prevVals, 0, prevValsAndK, 0, prevVals.length);
|
||||
if (code < ctx.codeTable.nextCode) { // Code table contains code
|
||||
pixels = tbl[code]; // Output pixels
|
||||
System.arraycopy(pixels, 0, out, outPos, pixels.length);
|
||||
outPos += pixels.length;
|
||||
prevValsAndK[prevVals.length] = tbl[code][0]; // K
|
||||
} else {
|
||||
prevValsAndK[prevVals.length] = prevVals[0]; // K
|
||||
System.arraycopy(prevValsAndK, 0, out, outPos, prevValsAndK.length);
|
||||
outPos += prevValsAndK.length;
|
||||
}
|
||||
ctx.codeTable.add(prevValsAndK); // Previous indices + K
|
||||
}
|
||||
} catch (final ArrayIndexOutOfBoundsException ignored) {
|
||||
}
|
||||
|
||||
// Return a properly sized array
|
||||
return Arrays.copyOf(out, outPos);
|
||||
}
|
||||
|
||||
private int[] deinterlace(final int[] src, final GifFrame fr) {
|
||||
final int w = fr.w, h = fr.h, wh = fr.wh;
|
||||
final DecodingContext ctx = DECODING_CONTEXT.get();
|
||||
final int[] dest = ctx.getTempBuffer(src.length);
|
||||
|
||||
// Interlaced images are organized in 4 sets of pixel lines
|
||||
final int set2Y = (h + 7) >>> 3; // Line no. = ceil(h/8.0)
|
||||
final int set3Y = set2Y + ((h + 3) >>> 3); // ceil(h-4/8.0)
|
||||
final int set4Y = set3Y + ((h + 1) >>> 2); // ceil(h-2/4.0)
|
||||
|
||||
// Sets' start indices in source array
|
||||
final int set2 = w * set2Y, set3 = w * set3Y, set4 = w * set4Y;
|
||||
|
||||
// Line skips in destination array - use bit shifts for multiplication
|
||||
final int w2 = w << 1, w4 = w2 << 1, w8 = w4 << 1;
|
||||
|
||||
// Group 1 contains every 8th line starting from 0
|
||||
int from = 0, to = 0;
|
||||
for (; from < set2; from += w, to += w8) {
|
||||
System.arraycopy(src, from, dest, to, w);
|
||||
}
|
||||
// Group 2 contains every 8th line starting from 4
|
||||
for (to = w4; from < set3; from += w, to += w8) {
|
||||
System.arraycopy(src, from, dest, to, w);
|
||||
}
|
||||
// Group 3 contains every 4th line starting from 2
|
||||
for (to = w2; from < set4; from += w, to += w4) {
|
||||
System.arraycopy(src, from, dest, to, w);
|
||||
}
|
||||
// Group 4 contains every 2nd line starting from 1 (biggest group)
|
||||
for (to = w; from < wh; from += w, to += w2) {
|
||||
System.arraycopy(src, from, dest, to, w);
|
||||
}
|
||||
return Arrays.copyOf(dest, src.length); // Return a copy, not the shared buffer
|
||||
}
|
||||
|
||||
private void drawFrame(final GifFrame fr) {
|
||||
renderLock.writeLock().lock();
|
||||
try {
|
||||
// Determine the color table that will be active for this frame
|
||||
final int[] activeColTbl = fr.hasLocColTbl ? fr.localColTbl : globalColTbl;
|
||||
|
||||
// Get pixels from data stream
|
||||
int[] pixels = decode(fr, activeColTbl);
|
||||
if (fr.interlaceFlag) {
|
||||
pixels = deinterlace(pixels, fr); // Rearrange pixel lines
|
||||
}
|
||||
|
||||
// Create image of type TYPE_INT_ARGB for frame area
|
||||
final BufferedImage frame = new BufferedImage(fr.w, fr.h, BufferedImage.TYPE_INT_ARGB);
|
||||
final int[] frameData = ((DataBufferInt) frame.getRaster().getDataBuffer()).getData();
|
||||
System.arraycopy(pixels, 0, frameData, 0, Math.min(pixels.length, fr.wh));
|
||||
|
||||
// Draw frame area on top of working image
|
||||
g.drawImage(frame, fr.x, fr.y, null);
|
||||
|
||||
// Keep a copy of the previous frame's pixels
|
||||
final DecodingContext ctx = DECODING_CONTEXT.get();
|
||||
final int[] prevPixelBuffer = ctx.getPrevPixelBuffer(wh);
|
||||
final int[] imgData = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
|
||||
System.arraycopy(imgData, 0, prevPixelBuffer, 0, wh);
|
||||
|
||||
// Create another copy for the end user to not expose internal state
|
||||
synchronized (fr.imageLock) {
|
||||
if (fr.img == null) { // Double-checked locking
|
||||
fr.img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
|
||||
final int[] frameImgData = ((DataBufferInt) fr.img.getRaster().getDataBuffer()).getData();
|
||||
System.arraycopy(prevPixelBuffer, 0, frameImgData, 0, wh);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle disposal of current frame
|
||||
if (fr.disposalMethod == 2) {
|
||||
// Restore to background color (clear frame area only)
|
||||
g.clearRect(fr.x, fr.y, fr.w, fr.h);
|
||||
} else if (fr.disposalMethod == 3) {
|
||||
// Restore previous frame
|
||||
System.arraycopy(prevPixelBuffer, 0, imgData, 0, wh);
|
||||
}
|
||||
} finally {
|
||||
renderLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the background color of the first frame in this GIF image. If
|
||||
* the frame has a local color table, the returned color will be from
|
||||
* that table. If not, the color will be from the global color table.
|
||||
* Returns 0 if there is neither a local nor a global color table.
|
||||
*
|
||||
* @return 32 bit ARGB color in the form 0xAARRGGBB
|
||||
*/
|
||||
public final int getBackgroundColor() {
|
||||
renderLock.readLock().lock();
|
||||
try {
|
||||
final GifFrame frame = frames.get(0);
|
||||
if (frame.hasLocColTbl) {
|
||||
return frame.localColTbl[bgColIndex];
|
||||
} else if (hasGlobColTbl) {
|
||||
return globalColTbl[bgColIndex];
|
||||
}
|
||||
return 0;
|
||||
} finally {
|
||||
renderLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If not 0, the delay specifies how many hundredths (1/100) of a second
|
||||
* to wait before displaying the frame <i>after</i> the current frame.
|
||||
*
|
||||
* @param index Index of the current frame, 0 to N-1
|
||||
* @return Delay as number of hundredths (1/100) of a second
|
||||
*/
|
||||
public final int getDelay(final int index) {
|
||||
renderLock.readLock().lock();
|
||||
try {
|
||||
return frames.get(index).delay;
|
||||
} finally {
|
||||
renderLock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index Index of the frame to return as image, starting from 0.
|
||||
* For incremental calls such as [0, 1, 2, ...] the method's
|
||||
* run time is O(1) as only one frame is drawn per call. For
|
||||
* random access calls such as [7, 12, ...] the run time is
|
||||
* O(N+1) with N being the number of previous frames that
|
||||
* need to be drawn before N+1 can be drawn on top. Once a
|
||||
* frame has been drawn it is being cached and the run time
|
||||
* is more or less O(0) to retrieve it from the list.
|
||||
* @return A BufferedImage for the specified frame.
|
||||
*/
|
||||
public BufferedImage getFrame(final int index) {
|
||||
// Initialize rendering context if needed
|
||||
if (img == null) {
|
||||
synchronized (initLock) {
|
||||
if (img == null) { // Double-checked locking
|
||||
img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
|
||||
g = img.createGraphics();
|
||||
g.setBackground(new Color(0, true)); // Transparent color
|
||||
// Set rendering hints for better performance
|
||||
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
|
||||
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GifFrame fr = frames.get(index);
|
||||
|
||||
// Check if frame is already rendered using double-checked locking
|
||||
if (fr.img == null) {
|
||||
synchronized (fr.imageLock) {
|
||||
if (fr.img == null) {
|
||||
// Draw all frames until and including the requested frame
|
||||
for (int i = 0; i <= index; i++) {
|
||||
fr = frames.get(i);
|
||||
if (fr.img == null) {
|
||||
drawFrame(fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fr.img;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The number of frames contained in this GIF image
|
||||
*/
|
||||
public final int getFrameCount() {
|
||||
return frames.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The height of the GIF image
|
||||
*/
|
||||
public final int getHeight() {
|
||||
return h;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The width of the GIF image
|
||||
*/
|
||||
public final int getWidth() {
|
||||
return w;
|
||||
}
|
||||
}
|
||||
|
||||
static final boolean DEBUG_MODE = false;
|
||||
|
||||
/**
|
||||
* @param in Raw image data as a byte[] array
|
||||
* @return A GifImage object exposing the properties of the GIF image.
|
||||
* @throws IOException If the image violates the GIF specification or is
|
||||
* truncated.
|
||||
*/
|
||||
public static GifImage read(final byte[] in) throws IOException {
|
||||
final GifDecoder decoder = new GifDecoder();
|
||||
final GifImage img = decoder.new GifImage();
|
||||
GifFrame frame = null; // Currently open frame
|
||||
int pos = readHeader(in, img); // Read header, get next byte position
|
||||
pos = readLogicalScreenDescriptor(img, in, pos);
|
||||
if (img.hasGlobColTbl) {
|
||||
img.globalColTbl = new int[img.sizeOfGlobColTbl];
|
||||
pos = readColTbl(in, img.globalColTbl, pos);
|
||||
}
|
||||
|
||||
// Main parsing loop with bounds checking optimization
|
||||
while (pos < in.length) {
|
||||
final int block = in[pos] & 0xFF;
|
||||
switch (block) {
|
||||
case 0x21: // Extension introducer
|
||||
if (pos + 1 >= in.length) {
|
||||
throw new IOException("Unexpected end of file.");
|
||||
}
|
||||
switch (in[pos + 1] & 0xFF) {
|
||||
case 0xFE: // Comment extension
|
||||
pos = readTextExtension(in, pos);
|
||||
break;
|
||||
case 0xFF: // Application extension
|
||||
pos = readAppExt(img, in, pos);
|
||||
break;
|
||||
case 0x01: // Plain text extension
|
||||
frame = null; // End of current frame
|
||||
pos = readTextExtension(in, pos);
|
||||
break;
|
||||
case 0xF9: // Graphic control extension
|
||||
if (frame == null) {
|
||||
frame = decoder.new GifFrame();
|
||||
img.frames.add(frame);
|
||||
}
|
||||
pos = readGraphicControlExt(frame, in, pos);
|
||||
break;
|
||||
default:
|
||||
throw new IOException("Unknown extension at " + pos);
|
||||
}
|
||||
break;
|
||||
case 0x2C: // Image descriptor
|
||||
if (frame == null) {
|
||||
frame = decoder.new GifFrame();
|
||||
img.frames.add(frame);
|
||||
}
|
||||
pos = readImgDescr(frame, in, pos);
|
||||
if (frame.hasLocColTbl) {
|
||||
frame.localColTbl = new int[frame.sizeOfLocColTbl];
|
||||
pos = readColTbl(in, frame.localColTbl, pos);
|
||||
}
|
||||
pos = readImgData(frame, in, pos);
|
||||
frame = null; // End of current frame
|
||||
break;
|
||||
case 0x3B: // GIF Trailer
|
||||
return img; // Found trailer, finished reading.
|
||||
default:
|
||||
// Unknown block. The image is corrupted. Strategies: a) Skip
|
||||
// and wait for a valid block. Experience: It'll get worse. b)
|
||||
// Throw exception. c) Return gracefully if we are almost done
|
||||
// processing. The frames we have so far should be error-free.
|
||||
final double progress = (double) pos / in.length;
|
||||
if (progress < 0.9) {
|
||||
throw new IOException("Unknown block at: " + pos);
|
||||
}
|
||||
pos = in.length; // Exit loop
|
||||
}
|
||||
}
|
||||
return img;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param is Image data as input stream. This method will read from the
|
||||
* input stream's current position. It will not reset the
|
||||
* position before reading and won't reset or close the stream
|
||||
* afterwards. Call these methods before and after calling this
|
||||
* method as needed.
|
||||
* @return A GifImage object exposing the properties of the GIF image.
|
||||
* @throws IOException If an I/O error occurs, the image violates the GIF
|
||||
* specification or the GIF is truncated.
|
||||
*/
|
||||
public static GifImage read(final InputStream is) throws IOException {
|
||||
final byte[] data = new byte[is.available()];
|
||||
is.read(data, 0, data.length);
|
||||
return read(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param img GIF image
|
||||
* @param in Raw data
|
||||
* @param i Index of the first byte of the application extension
|
||||
* @return Index of the first byte after this extension
|
||||
*/
|
||||
static int readAppExt(final GifImage img, final byte[] in, int i) {
|
||||
// Use StandardCharsets for better performance if available, otherwise fallback
|
||||
img.appId = new String(in, i + 3, 8); // should be "NETSCAPE"
|
||||
img.appAuthCode = new String(in, i + 11, 3); // should be "2.0"
|
||||
i += 14; // Go to sub-block size, it's value should be 3
|
||||
final int subBlockSize = in[i] & 0xFF;
|
||||
// The only app extension widely used is NETSCAPE, it's got 3 data bytes
|
||||
if (subBlockSize == 3) {
|
||||
// in[i+1] should have value 01, in[i+5] should be block terminator
|
||||
img.repetitions = (in[i + 2] & 0xFF) | ((in[i + 3] & 0xFF) << 8); // Short
|
||||
return i + 5;
|
||||
} // Skip unknown application extensions
|
||||
while ((in[i] & 0xFF) != 0) { // While sub-block size != 0
|
||||
i += (in[i] & 0xFF) + 1; // Skip to next sub-block
|
||||
}
|
||||
return i + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param in Raw data
|
||||
* @param colors Pre-initialized target array to store ARGB colors
|
||||
* @param i Index of the color table's first byte
|
||||
* @return Index of the first byte after the color table
|
||||
*/
|
||||
static int readColTbl(final byte[] in, final int[] colors, int i) {
|
||||
final int numColors = colors.length;
|
||||
for (int c = 0; c < numColors; c++) {
|
||||
final int r = in[i++] & 0xFF; // 1st byte is red
|
||||
final int g = in[i++] & 0xFF; // 2nd byte is green
|
||||
final int b = in[i++] & 0xFF; // 3rd byte is blue
|
||||
// Optimized color packing - alpha is always 0xFF for opaque
|
||||
colors[c] = 0xFF000000 | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fr GIF frame
|
||||
* @param in Raw data
|
||||
* @param i Index of the extension introducer
|
||||
* @return Index of the first byte after this block
|
||||
*/
|
||||
static int readGraphicControlExt(final GifFrame fr, final byte[] in, final int i) {
|
||||
final int packed = in[i + 3] & 0xFF;
|
||||
fr.disposalMethod = (packed & 0b00011100) >>> 2; // Bits 4-2
|
||||
fr.transpColFlag = (packed & 1) == 1; // Bit 0
|
||||
fr.delay = (in[i + 4] & 0xFF) | ((in[i + 5] & 0xFF) << 8); // 16 bit LSB
|
||||
fr.transpColIndex = in[i + 6] & 0xFF; // Byte 6
|
||||
return i + 8; // Skipped byte 7 (blockTerminator), as it's always 0x00
|
||||
}
|
||||
|
||||
/**
|
||||
* @param in Raw data
|
||||
* @param img The GifImage object that is currently read
|
||||
* @return Index of the first byte after this block
|
||||
* @throws IOException If the GIF header/trailer is missing, incomplete or
|
||||
* unknown
|
||||
*/
|
||||
static int readHeader(final byte[] in, final GifImage img) throws IOException {
|
||||
if (in.length < 6) { // Check first 6 bytes
|
||||
throw new IOException("Image is truncated.");
|
||||
}
|
||||
// Use ASCII encoding for header - faster than default charset
|
||||
img.header = new String(in, 0, 6);
|
||||
if (!img.header.equals("GIF87a") && !img.header.equals("GIF89a")) {
|
||||
throw new IOException("Invalid GIF header.");
|
||||
}
|
||||
return 6;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fr The GIF frame to whom this image descriptor belongs
|
||||
* @param in Raw data
|
||||
* @param i Index of the first byte of this block, i.e. the minCodeSize
|
||||
* @return Byte index
|
||||
*/
|
||||
static int readImgData(final GifFrame fr, final byte[] in, int i) {
|
||||
final int fileSize = in.length;
|
||||
final int minCodeSize = in[i++] & 0xFF; // Read code size, go to block
|
||||
final int clearCode = 1 << minCodeSize; // CLEAR = 2^minCodeSize
|
||||
fr.firstCodeSize = minCodeSize + 1; // Add 1 bit for CLEAR and EOI
|
||||
fr.clearCode = clearCode;
|
||||
fr.endOfInfoCode = clearCode + 1;
|
||||
|
||||
// Pre-calculate image data size for better memory allocation
|
||||
final int imgDataSize = readImgDataSize(in, i);
|
||||
final byte[] imgData = new byte[imgDataSize + 2];
|
||||
int imgDataPos = 0;
|
||||
int subBlockSize = in[i] & 0xFF;
|
||||
|
||||
while (subBlockSize > 0) { // While block has data
|
||||
try { // Next line may throw exception if sub-block size is fake
|
||||
final int nextSubBlockSizePos = i + subBlockSize + 1;
|
||||
final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF;
|
||||
System.arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
|
||||
imgDataPos += subBlockSize; // Move output data position
|
||||
i = nextSubBlockSizePos; // Move to next sub-block size
|
||||
subBlockSize = nextSubBlockSize;
|
||||
} catch (final Exception e) {
|
||||
// Sub-block exceeds file end, only use remaining bytes
|
||||
subBlockSize = fileSize - i - 1; // Remaining bytes
|
||||
if (subBlockSize > 0) {
|
||||
System.arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
|
||||
imgDataPos += subBlockSize; // Move output data position
|
||||
}
|
||||
i += subBlockSize + 1; // Move to next sub-block size
|
||||
break;
|
||||
}
|
||||
}
|
||||
fr.data = imgData; // Holds LZW encoded data
|
||||
i++; // Skip last sub-block size, should be 0
|
||||
return i;
|
||||
}
|
||||
|
||||
static int readImgDataSize(final byte[] in, int i) {
|
||||
final int fileSize = in.length;
|
||||
int imgDataPos = 0;
|
||||
int subBlockSize = in[i] & 0xFF;
|
||||
while (subBlockSize > 0) { // While block has data
|
||||
try { // Next line may throw exception if sub-block size is fake
|
||||
final int nextSubBlockSizePos = i + subBlockSize + 1;
|
||||
final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF;
|
||||
imgDataPos += subBlockSize; // Move output data position
|
||||
i = nextSubBlockSizePos; // Move to next sub-block size
|
||||
subBlockSize = nextSubBlockSize;
|
||||
} catch (final Exception e) {
|
||||
// Sub-block exceeds file end, only use remaining bytes
|
||||
subBlockSize = fileSize - i - 1; // Remaining bytes
|
||||
if (subBlockSize > 0) {
|
||||
imgDataPos += subBlockSize; // Move output data position
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return imgDataPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fr The GIF frame to whom this image descriptor belongs
|
||||
* @param in Raw data
|
||||
* @param i Index of the image separator, i.e. the first block byte
|
||||
* @return Index of the first byte after this block
|
||||
*/
|
||||
static int readImgDescr(final GifFrame fr, final byte[] in, int i) {
|
||||
fr.x = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 1-2: left
|
||||
fr.y = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 3-4: top
|
||||
fr.w = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 5-6: width
|
||||
fr.h = (in[++i] & 0xFF) | ((in[++i] & 0xFF) << 8); // Byte 7-8: height
|
||||
fr.wh = fr.w * fr.h;
|
||||
final int packed = in[++i] & 0xFF; // Byte 9 is a packed byte
|
||||
fr.hasLocColTbl = (packed & 0b10000000) != 0; // Bit 7
|
||||
fr.interlaceFlag = (packed & 0b01000000) != 0; // Bit 6
|
||||
fr.sortFlag = (packed & 0b00100000) != 0; // Bit 5
|
||||
final int colTblSizePower = (packed & 7) + 1; // Bits 2-0
|
||||
fr.sizeOfLocColTbl = 1 << colTblSizePower; // 2^(N+1), As per the spec
|
||||
return ++i;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param img GIF image
|
||||
* @param i Start index of this block.
|
||||
* @return Index of the first byte after this block.
|
||||
*/
|
||||
static int readLogicalScreenDescriptor(final GifImage img, final byte[] in, final int i) {
|
||||
img.w = (in[i] & 0xFF) | ((in[i + 1] & 0xFF) << 8); // 16 bit, LSB 1st
|
||||
img.h = (in[i + 2] & 0xFF) | ((in[i + 3] & 0xFF) << 8); // 16 bit
|
||||
img.wh = img.w * img.h;
|
||||
final int packed = in[i + 4] & 0xFF; // Byte 4 is a packed byte
|
||||
img.hasGlobColTbl = (packed & 0b10000000) != 0; // Bit 7
|
||||
final int colResPower = ((packed & 0b01110000) >>> 4) + 1; // Bits 6-4
|
||||
img.colorResolution = 1 << colResPower; // 2^(N+1), As per the spec
|
||||
img.sortFlag = (packed & 0b00001000) != 0; // Bit 3
|
||||
final int globColTblSizePower = (packed & 7) + 1; // Bits 0-2
|
||||
img.sizeOfGlobColTbl = 1 << globColTblSizePower; // 2^(N+1), see spec
|
||||
img.bgColIndex = in[i + 5] & 0xFF; // 1 Byte
|
||||
img.pxAspectRatio = in[i + 6] & 0xFF; // 1 Byte
|
||||
return i + 7;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param in Raw data
|
||||
* @param pos Index of the extension introducer
|
||||
* @return Index of the first byte after this block
|
||||
*/
|
||||
static int readTextExtension(final byte[] in, final int pos) {
|
||||
int i = pos + 2; // Skip extension introducer and label
|
||||
int subBlockSize = in[i++] & 0xFF;
|
||||
while (subBlockSize != 0 && i < in.length) {
|
||||
i += subBlockSize;
|
||||
if (i >= in.length) break; // Safety check
|
||||
subBlockSize = in[i++] & 0xFF;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
package org.saturnclient.saturnclient.cosmetics.cloaks.utils;
|
||||
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
/**
|
||||
* Represents a single frame of an animated cloak.
|
||||
*/
|
||||
public class AnimatedCloakData {
|
||||
private final Identifier textureId;
|
||||
private final int delayMs;
|
||||
|
||||
/**
|
||||
* Creates a new animated cloak frame.
|
||||
*
|
||||
* @param textureId The texture identifier for this frame
|
||||
* @param delayMs The delay in milliseconds before the next frame
|
||||
*/
|
||||
public AnimatedCloakData(Identifier textureId, int delayMs) {
|
||||
this.textureId = textureId;
|
||||
this.delayMs = delayMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The texture identifier for this frame
|
||||
*/
|
||||
public Identifier getTextureId() {
|
||||
return textureId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The delay in milliseconds before the next frame
|
||||
*/
|
||||
public int getDelayMs() {
|
||||
return delayMs;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user