mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 17:21:49 -10:00
Compare commits
701
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13be1a9e4f | ||
|
|
6a978d3bef | ||
|
|
a8055893a2 | ||
|
|
ef78519011 | ||
|
|
c30775263f | ||
|
|
04176c2939 | ||
|
|
96505f501d | ||
|
|
c932d4bec1 | ||
|
|
c87e44bf63 | ||
|
|
d8d0d5e832 | ||
|
|
79d2b975f5 | ||
|
|
0b9487e6e9 | ||
|
|
e32b03ea27 | ||
|
|
c4d8c79e2b | ||
|
|
8cc05f114c | ||
|
|
b80e816a76 | ||
|
|
9b8b755e4c | ||
|
|
5692713279 | ||
|
|
ff31439c92 | ||
|
|
02641599e8 | ||
|
|
12030689f8 | ||
|
|
ce68709f1f | ||
|
|
3e5b2ef194 | ||
|
|
14bb1416d1 | ||
|
|
1f666cfec4 | ||
|
|
8b578746ea | ||
|
|
af55aad580 | ||
|
|
b11e8a54b6 | ||
|
|
0ef93c96b9 | ||
|
|
5fca014076 | ||
|
|
c1c985ce5b | ||
|
|
af24ac05bb | ||
|
|
1a92fe7767 | ||
|
|
079ed1a010 | ||
|
|
e55180e7dd | ||
|
|
da4fb17df7 | ||
|
|
114e9920ac | ||
|
|
ad2c0d2ecd | ||
|
|
976afe97e2 | ||
|
|
7c58203441 | ||
|
|
cb201c59e4 | ||
|
|
d22ecb0b52 | ||
|
|
62075a5b32 | ||
|
|
aea2bdadb1 | ||
|
|
f59035948c | ||
|
|
d05f889fe1 | ||
|
|
c96af77eb6 | ||
|
|
d4df5dbf59 | ||
|
|
66e6b23fb8 | ||
|
|
918a4c8434 | ||
|
|
a3c540e8c0 | ||
|
|
e243cbac06 | ||
|
|
4c5f37de8d | ||
|
|
ba7cdd4d8c | ||
|
|
b61394f396 | ||
|
|
a13866fff3 | ||
|
|
f9302c0736 | ||
|
|
44e4124142 | ||
|
|
dc040a9dd7 | ||
|
|
d54d736546 | ||
|
|
e4928503ac | ||
|
|
2a83a524fc | ||
|
|
cfd68c41cf | ||
|
|
2c149ab9e6 | ||
|
|
aebabfb5c6 | ||
|
|
90a09f5b25 | ||
|
|
ea2986d57b | ||
|
|
7093191256 | ||
|
|
d774839f1f | ||
|
|
6cfb046532 | ||
|
|
fb0866ef3f | ||
|
|
eec2eb3b32 | ||
|
|
4e8ed32e62 | ||
|
|
8f55a6c245 | ||
|
|
13664f3799 | ||
|
|
1fb80a5d79 | ||
|
|
3d8521b429 | ||
|
|
6181c89ed6 | ||
|
|
2018af984c | ||
|
|
f31d16a2c6 | ||
|
|
c7d5881fdc | ||
|
|
c815a0f81c | ||
|
|
08c3b12345 | ||
|
|
0925060e6f | ||
|
|
08f1c4f6c6 | ||
|
|
838375d4c2 | ||
|
|
8bb2525e31 | ||
|
|
ef6be13a1c | ||
|
|
014ad04d1c | ||
|
|
f2ec4e9c01 | ||
|
|
0d840d54e3 | ||
|
|
43528d198c | ||
|
|
3d92e52a83 | ||
|
|
0ef3df34d6 | ||
|
|
e4872aeba7 | ||
|
|
d08f9ce9d5 | ||
|
|
162dd10147 | ||
|
|
5919e28e0a | ||
|
|
8985543344 | ||
|
|
29f5c3504f | ||
|
|
06c3ba2173 | ||
|
|
57dcec2e2d | ||
|
|
a3b0891fc2 | ||
|
|
68d5e2ec6c | ||
|
|
909a8123d0 | ||
|
|
da3a5aff7e | ||
|
|
e1d2186ee1 | ||
|
|
e500989785 | ||
|
|
57ad8718c3 | ||
|
|
c12fe63d33 | ||
|
|
71e18e8c0a | ||
|
|
880b690d6c | ||
|
|
d55b7f8594 | ||
|
|
b849bc3dab | ||
|
|
85d5ce37f0 | ||
|
|
5395de1ab4 | ||
|
|
24459551e6 | ||
|
|
848ffed94d | ||
|
|
66ee01b737 | ||
|
|
7227485ca2 | ||
|
|
d914334acc | ||
|
|
07b8f97138 | ||
|
|
5e8d557b17 | ||
|
|
07a162cb30 | ||
|
|
8146a586da | ||
|
|
dc8aba8bee | ||
|
|
f1f352044f | ||
|
|
83feefe860 | ||
|
|
2132e6126f | ||
|
|
97b36f8e1b | ||
|
|
521c26d6ca | ||
|
|
218624479d | ||
|
|
4dc1282f65 | ||
|
|
b351455eae | ||
|
|
217e87e8f3 | ||
|
|
e3356386d0 | ||
|
|
bf17581664 | ||
|
|
02e62fbedf | ||
|
|
e0f9934254 | ||
|
|
76bda62793 | ||
|
|
b12c89bf04 | ||
|
|
345e6efdc9 | ||
|
|
77ec563892 | ||
|
|
5bd038ed99 | ||
|
|
06e2041d43 | ||
|
|
2643b0e246 | ||
|
|
f235e95518 | ||
|
|
1e68c9db3d | ||
|
|
ee88e8c870 | ||
|
|
18eb428ffb | ||
|
|
cd99e67d1c | ||
|
|
71622aabb3 | ||
|
|
9884fc3af1 | ||
|
|
1e137ff7f3 | ||
|
|
bae2b17a0d | ||
|
|
6f5be3bbf8 | ||
|
|
627617ee6b | ||
|
|
9343b3c357 | ||
|
|
b85a07d423 | ||
|
|
9b788418c3 | ||
|
|
19eff5eb79 | ||
|
|
af5731d305 | ||
|
|
53c13c0cd9 | ||
|
|
33cd3992bf | ||
|
|
a419cc37c7 | ||
|
|
ec534edc6c | ||
|
|
8c5025d3d9 | ||
|
|
d6113ce7e5 | ||
|
|
1c389c3758 | ||
|
|
8d5e529b46 | ||
|
|
6f82bf74b1 | ||
|
|
e7c082ee6f | ||
|
|
5efccc59f0 | ||
|
|
cdf61801bb | ||
|
|
f7190cff67 | ||
|
|
02896862e8 | ||
|
|
87761cd5b0 | ||
|
|
ff80bb806c | ||
|
|
d41641f85d | ||
|
|
9bc728f303 | ||
|
|
58fc786742 | ||
|
|
3d2be9b1bd | ||
|
|
258dee9c8d | ||
|
|
24036af2fb | ||
|
|
65ee18174a | ||
|
|
10e24b4f32 | ||
|
|
5303bf09af | ||
|
|
e524b6a114 | ||
|
|
533dd084ae | ||
|
|
f1fac7cc6f | ||
|
|
7b3be8d0b9 | ||
|
|
408f38d62a | ||
|
|
e68a0653a0 | ||
|
|
a8201731a1 | ||
|
|
ff4fad66c9 | ||
|
|
a13fa8c13c | ||
|
|
5c86d98a9b | ||
|
|
9004c6c8d6 | ||
|
|
5fe1090401 | ||
|
|
0ab59f64d4 | ||
|
|
b0d94ed2c0 | ||
|
|
a4c8dc296d | ||
|
|
1d800a7655 | ||
|
|
776bb4f4d5 | ||
|
|
9fb818013c | ||
|
|
efc682f297 | ||
|
|
0bd8f4c89c | ||
|
|
e173055d5d | ||
|
|
5b44880bfb | ||
|
|
b2a6c25a6e | ||
|
|
942e212ca7 | ||
|
|
e380e88edb | ||
|
|
5201202a30 | ||
|
|
f5c1c04245 | ||
|
|
fcb378d2dd | ||
|
|
4062a09766 | ||
|
|
48791d642e | ||
|
|
7f87be741b | ||
|
|
5a44d6dd7f | ||
|
|
54c4a07e61 | ||
|
|
1fd49b9082 | ||
|
|
892f24b15d | ||
|
|
dde8c502ca | ||
|
|
b31e63adc8 | ||
|
|
e4bb7bf7ae | ||
|
|
f7b204a231 | ||
|
|
6635e7a46a | ||
|
|
b127089760 | ||
|
|
e575a22cb2 | ||
|
|
4e6b887535 | ||
|
|
4d3f9944eb | ||
|
|
4e36ae1e4e | ||
|
|
253a694871 | ||
|
|
bfdff7afb6 | ||
|
|
ff416a1889 | ||
|
|
89dc5664de | ||
|
|
a98ba02730 | ||
|
|
36232d8820 | ||
|
|
85b299df9d | ||
|
|
c968fb0d7d | ||
|
|
1bb6912a4b | ||
|
|
7a46ba1f27 | ||
|
|
f3d842d846 | ||
|
|
8f73091827 | ||
|
|
03d9b39fc4 | ||
|
|
a5fa6f0d09 | ||
|
|
04448803c8 | ||
|
|
8f39c8afd2 | ||
|
|
0f992292b7 | ||
|
|
21d66471d4 | ||
|
|
6e302fab48 | ||
|
|
b55e8616c1 | ||
|
|
f7af3f420f | ||
|
|
ec74bd066b | ||
|
|
3de12c0c7c | ||
|
|
01954d87c5 | ||
|
|
41f822b659 | ||
|
|
d6fd737e31 | ||
|
|
52766de1c4 | ||
|
|
ce25432200 | ||
|
|
15405e4ccf | ||
|
|
bf4657da26 | ||
|
|
3c6c890993 | ||
|
|
0efe1c2c13 | ||
|
|
a2090d0b0f | ||
|
|
5b12729b5f | ||
|
|
cf20ccc4f9 | ||
|
|
58a5690ec6 | ||
|
|
d2b61ade91 | ||
|
|
cdd4ffe95d | ||
|
|
f217a34593 | ||
|
|
f15541c0fe | ||
|
|
965c9d9aed | ||
|
|
97f85f4530 | ||
|
|
4ce74ca81c | ||
|
|
2dcb671e13 | ||
|
|
964d919d96 | ||
|
|
fd61fa626e | ||
|
|
db25d1d092 | ||
|
|
398da21689 | ||
|
|
297a718908 | ||
|
|
8a78ad3315 | ||
|
|
82dc682cbc | ||
|
|
09160dc1e2 | ||
|
|
f1ef5cb53a | ||
|
|
28a389b7a3 | ||
|
|
8ead30efe3 | ||
|
|
052d9ebf20 | ||
|
|
d1ce5ddfd9 | ||
|
|
25ce3945c8 | ||
|
|
40b1fc9100 | ||
|
|
a61d6fa54f | ||
|
|
38d23bc13c | ||
|
|
c4c0e6fc6b | ||
|
|
d2d046b1be | ||
|
|
ae2e74af51 | ||
|
|
62028656e0 | ||
|
|
6f2b33ff95 | ||
|
|
5fd9d40d4c | ||
|
|
d0a80d6eb8 | ||
|
|
3a0d345f06 | ||
|
|
09ac779466 | ||
|
|
cd7ce3024a | ||
|
|
8eb1e7cf4f | ||
|
|
4e94325ba5 | ||
|
|
225bcde8b9 | ||
|
|
1dd1969640 | ||
|
|
7e13949bcf | ||
|
|
a1ff75a4a5 | ||
|
|
bedc6fbeda | ||
|
|
ad6e268383 | ||
|
|
445ba1168e | ||
|
|
31310a69f6 | ||
|
|
e517343add | ||
|
|
d49f1a0444 | ||
|
|
d2fbe234ba | ||
|
|
ba4d3d471f | ||
|
|
a6a93455e8 | ||
|
|
c545bd4cb1 | ||
|
|
a0bb275b62 | ||
|
|
3eda4b1b14 | ||
|
|
8ccf1404e7 | ||
|
|
1de40d7cd3 | ||
|
|
e3ca63226c | ||
|
|
77f00c606d | ||
|
|
5751bbd65f | ||
|
|
1c0fabcd7e | ||
|
|
10ec872cc1 | ||
|
|
a2b395d91a | ||
|
|
ae31791c33 | ||
|
|
1fa86c919f | ||
|
|
2710305be4 | ||
|
|
f4dcf8a2fb | ||
|
|
c87e5a6893 | ||
|
|
0b2f2fda73 | ||
|
|
ef64ea9d3e | ||
|
|
54cdfe00fe | ||
|
|
03ded99473 | ||
|
|
0873f24d20 | ||
|
|
823cf263ff | ||
|
|
e5d96bad14 | ||
|
|
296ac7fbf6 | ||
|
|
a6661aab7e | ||
|
|
2c92825807 | ||
|
|
ed739050ae | ||
|
|
1caa6c8931 | ||
|
|
ddb365e6b4 | ||
|
|
64f691f9f7 | ||
|
|
0167f90c29 | ||
|
|
28d84f2078 | ||
|
|
5b16680739 | ||
|
|
086633c199 | ||
|
|
fb75cde631 | ||
|
|
869d1aa812 | ||
|
|
2af85dcc84 | ||
|
|
6285cca37c | ||
|
|
44a12076d5 | ||
|
|
079d82f968 | ||
|
|
f4146eea7c | ||
|
|
503727b594 | ||
|
|
9f4b5e0a80 | ||
|
|
41eec6978b | ||
|
|
88cddcec82 | ||
|
|
bc6b668820 | ||
|
|
aad7542283 | ||
|
|
2a931e3e63 | ||
|
|
be4ce23d67 | ||
|
|
cfdca1dd6e | ||
|
|
e096a156a1 | ||
|
|
4a20680924 | ||
|
|
85a5195411 | ||
|
|
3aaa70563e | ||
|
|
361766f621 | ||
|
|
835f062e8e | ||
|
|
1f00e33453 | ||
|
|
d3f799f5d5 | ||
|
|
918767ac08 | ||
|
|
089a7c1242 | ||
|
|
6cfb512e1e | ||
|
|
6f6042da38 | ||
|
|
345a66c710 | ||
|
|
2cc12be695 | ||
|
|
8126b95fb6 | ||
|
|
a36ce4ef76 | ||
|
|
d53d7bf02a | ||
|
|
e42f12db85 | ||
|
|
988f7d1ade | ||
|
|
8e3cade985 | ||
|
|
8ca4e1d985 | ||
|
|
b5c12d0f76 | ||
|
|
2251cfb716 | ||
|
|
e5b2d2ea71 | ||
|
|
c1a7b0d35e | ||
|
|
b502fe41ac | ||
|
|
a8c5f833e4 | ||
|
|
8ca4e05005 | ||
|
|
adcc59b920 | ||
|
|
f42833e146 | ||
|
|
1bd640668a | ||
|
|
cc5f7ff239 | ||
|
|
f7a5a3809f | ||
|
|
1faeccc439 | ||
|
|
57512281ba | ||
|
|
ebafaf775a | ||
|
|
2f890f7df9 | ||
|
|
62a7855892 | ||
|
|
cc3714e6ad | ||
|
|
d08bf63777 | ||
|
|
92dbf8406e | ||
|
|
77ac54a216 | ||
|
|
8815f97746 | ||
|
|
7e74759055 | ||
|
|
6c0d2781e6 | ||
|
|
45a678e309 | ||
|
|
efaa2a0e3f | ||
|
|
ed5ac7df1f | ||
|
|
3e92f5e6fd | ||
|
|
a0e3c383a2 | ||
|
|
1c9379c29e | ||
|
|
46e88c47b1 | ||
|
|
8671d460bf | ||
|
|
9f282e42ea | ||
|
|
7b0e4fee81 | ||
|
|
c76e42d88d | ||
|
|
fe8b0c622f | ||
|
|
c8137fbec9 | ||
|
|
a4d6ba2781 | ||
|
|
84cc6b5d64 | ||
|
|
5258de5549 | ||
|
|
c939630a79 | ||
|
|
932d351e11 | ||
|
|
bd0c8cb4fd | ||
|
|
8504efabb8 | ||
|
|
63db515796 | ||
|
|
d629a4f6f6 | ||
|
|
77f2a09212 | ||
|
|
aa1a7c0c42 | ||
|
|
36e6c2dc3f | ||
|
|
aca06a277b | ||
|
|
fedc1c7313 | ||
|
|
df6139520e | ||
|
|
1a57f3660d | ||
|
|
1e030fd397 | ||
|
|
4c1c81539f | ||
|
|
bf6bb5d470 | ||
|
|
f4cc8528ae | ||
|
|
c12ad4d2cf | ||
|
|
b8dccdaa55 | ||
|
|
50ac3abcda | ||
|
|
f4eb3234a8 | ||
|
|
3728c4b7d1 | ||
|
|
899d87e22a | ||
|
|
1fd348869c | ||
|
|
ec6b1817a5 | ||
|
|
07c3ef02d0 | ||
|
|
a02fdabe6f | ||
|
|
d16b78e715 | ||
|
|
e5cc020034 | ||
|
|
799fcccba4 | ||
|
|
54feca80d1 | ||
|
|
c1f8fee513 | ||
|
|
8ca476e489 | ||
|
|
efabb913d0 | ||
|
|
bed239a2cd | ||
|
|
63ddd76a15 | ||
|
|
208f1c010f | ||
|
|
935e3927ab | ||
|
|
412594e68d | ||
|
|
63d25cbd63 | ||
|
|
43aba8dbe5 | ||
|
|
9ee9bb4d0e | ||
|
|
4d5f0a965f | ||
|
|
585e28d1fb | ||
|
|
966a63278e | ||
|
|
aff26bf9f9 | ||
|
|
cf8a5a51b4 | ||
|
|
22462182e3 | ||
|
|
c8c4a29367 | ||
|
|
0c47273286 | ||
|
|
f24cc569db | ||
|
|
c3140e1bae | ||
|
|
017caac28c | ||
|
|
061648308e | ||
|
|
ce90330576 | ||
|
|
7c411b3a58 | ||
|
|
b6e947c9a4 | ||
|
|
db1cde61a8 | ||
|
|
89bac30424 | ||
|
|
cd5b93a54a | ||
|
|
35bb6ede00 | ||
|
|
da06786848 | ||
|
|
8b33877963 | ||
|
|
3b56859e04 | ||
|
|
376be8985d | ||
|
|
a4586852b1 | ||
|
|
f2a63474b9 | ||
|
|
24224f4728 | ||
|
|
4e580b2732 | ||
|
|
0a14bac24c | ||
|
|
4eb7a27e58 | ||
|
|
8be5c59fce | ||
|
|
3aaff8da93 | ||
|
|
a8107ee025 | ||
|
|
625f6ca1a5 | ||
|
|
d12a260a53 | ||
|
|
0d70c87082 | ||
|
|
81cf3ef358 | ||
|
|
5c1d4d53f6 | ||
|
|
b32520088e | ||
|
|
460ecfce99 | ||
|
|
3e4828d791 | ||
|
|
e925e25781 | ||
|
|
58d5f3b519 | ||
|
|
d3678c9112 | ||
|
|
08393ef6f7 | ||
|
|
ebe3818eb7 | ||
|
|
11dda135a8 | ||
|
|
7cc9057fa6 | ||
|
|
8a65d28c0d | ||
|
|
8dd47b5ef2 | ||
|
|
643d699d51 | ||
|
|
7d5d34ae32 | ||
|
|
97d414cc4d | ||
|
|
f0de57ae23 | ||
|
|
2f075689f5 | ||
|
|
3f11bff706 | ||
|
|
27b110085e | ||
|
|
5bb149a282 | ||
|
|
4b3116c54f | ||
|
|
7b2ce7a1bc | ||
|
|
9e90050c60 | ||
|
|
131d5d73e2 | ||
|
|
f488baa0ab | ||
|
|
1d13a300fa | ||
|
|
54642a5e80 | ||
|
|
5905a4977f | ||
|
|
98d37c0887 | ||
|
|
be24d83d97 | ||
|
|
561f797c04 | ||
|
|
dcfa4b07f7 | ||
|
|
00b9953741 | ||
|
|
623aa1d3ea | ||
|
|
82e2c96a20 | ||
|
|
ee50f8ae1a | ||
|
|
bfc40195eb | ||
|
|
cb65d4f80d | ||
|
|
04116c33c6 | ||
|
|
1da773a501 | ||
|
|
a02c7fcd4b | ||
|
|
19f87e8059 | ||
|
|
83e2920c5e | ||
|
|
9734a8a255 | ||
|
|
cd7d6a3dcc | ||
|
|
d5479156d7 | ||
|
|
b73ef3faa3 | ||
|
|
b7841886cf | ||
|
|
868e494b0c | ||
|
|
576c5942f1 | ||
|
|
575fadbeeb | ||
|
|
6df80522aa | ||
|
|
f16012e26c | ||
|
|
5a92cfb6b1 | ||
|
|
22807a3f3a | ||
|
|
b27965b4ca | ||
|
|
d95d0be4e4 | ||
|
|
8f4888a2ef | ||
|
|
d1eef82997 | ||
|
|
9c5982d8e0 | ||
|
|
1665d177c6 | ||
|
|
36dac313c8 | ||
|
|
25bec21906 | ||
|
|
9fb4fde63b | ||
|
|
d2cd07f1ac | ||
|
|
dec4b523f3 | ||
|
|
3236b57773 | ||
|
|
c572bb7f88 | ||
|
|
9329e154cb | ||
|
|
72a1b13eae | ||
|
|
2ea59c309b | ||
|
|
cf92894885 | ||
|
|
0e05b77d95 | ||
|
|
4fea4cfd63 | ||
|
|
61e96ef269 | ||
|
|
b5f50bc41d | ||
|
|
9ee63c0510 | ||
|
|
cf6a402c41 | ||
|
|
4ebdcbf7b2 | ||
|
|
0fdc892ef9 | ||
|
|
9794dd7ca9 | ||
|
|
5d76ff0225 | ||
|
|
caa5c31dd0 | ||
|
|
4465421885 | ||
|
|
1d38cdeba7 | ||
|
|
249341e7cc | ||
|
|
85c277b5e2 | ||
|
|
309ddb48ed | ||
|
|
87a0ccc90c | ||
|
|
1e36a5da1f | ||
|
|
6ad03dd306 | ||
|
|
837ec11a1a | ||
|
|
a26ffbb007 | ||
|
|
8089757807 | ||
|
|
eb6d139e34 | ||
|
|
fc6f53a337 | ||
|
|
16d11c6bb2 | ||
|
|
b248489d15 | ||
|
|
6165c33ebe | ||
|
|
313f65e049 | ||
|
|
4b8abba21c | ||
|
|
e932cade0c | ||
|
|
404cf600c1 | ||
|
|
b66525f609 | ||
|
|
1b8b51978d | ||
|
|
dcffd6cae9 | ||
|
|
694d953496 | ||
|
|
3f782b89ee | ||
|
|
0e33837c78 | ||
|
|
53aad00102 | ||
|
|
3c794c2825 | ||
|
|
29662d963c | ||
|
|
c69d03f96e | ||
|
|
de4029c1df | ||
|
|
1bf04aa84f | ||
|
|
8a2619be29 | ||
|
|
8ec7d24add | ||
|
|
62a47df275 | ||
|
|
232b0e85aa | ||
|
|
02ba821bcf | ||
|
|
05bc471106 | ||
|
|
934c4aaabf | ||
|
|
cc474268e7 | ||
|
|
45af2b23b2 | ||
|
|
de7d92e134 | ||
|
|
8c6933bda4 | ||
|
|
4ea8baade5 | ||
|
|
15047ffac4 | ||
|
|
3b71256fcd | ||
|
|
b20ab60d01 | ||
|
|
c0e2f62b35 | ||
|
|
817c37749f | ||
|
|
4f49676a37 | ||
|
|
24987d6660 | ||
|
|
391ce2975d | ||
|
|
ce92f7e45a | ||
|
|
a7f6ecbdfb | ||
|
|
31c1bd1f8b | ||
|
|
7e5063a4d5 | ||
|
|
185908fd38 | ||
|
|
dd0bffd667 | ||
|
|
c8f1559eb0 | ||
|
|
1f031f6bd3 | ||
|
|
2e7afbfe64 | ||
|
|
5a3dc52e1f | ||
|
|
96d60b9daf | ||
|
|
785a399740 | ||
|
|
bb4d24e909 | ||
|
|
02b2f7b9c0 | ||
|
|
513312eb8b | ||
|
|
6c44743c21 | ||
|
|
7cead764e8 | ||
|
|
f719bbef1c | ||
|
|
ef05a384d5 | ||
|
|
d4a2f07032 | ||
|
|
0801940eeb | ||
|
|
5cede4ff4e | ||
|
|
574b20a662 | ||
|
|
2a767017b9 | ||
|
|
a8ea55f39a | ||
|
|
0e80c10042 | ||
|
|
9fcd6133ed | ||
|
|
812702d809 | ||
|
|
bfc26f944a | ||
|
|
7658a24435 | ||
|
|
394db9011a | ||
|
|
7a7cf79d4b | ||
|
|
955558305a | ||
|
|
b1896906d1 | ||
|
|
debd86ea59 | ||
|
|
f9a93d080a | ||
|
|
caeaa41bdc | ||
|
|
15f071ff3b | ||
|
|
4985864055 | ||
|
|
9d9617fe29 | ||
|
|
3a298db1c0 | ||
|
|
6d59c2da8d | ||
|
|
8a3eafbd61 | ||
|
|
f732441c84 | ||
|
|
d9d4d19a1d | ||
|
|
068b87cc25 | ||
|
|
a5416ca075 | ||
|
|
2f9c963068 | ||
|
|
37ff7be994 | ||
|
|
476c083e82 | ||
|
|
055c47c98d | ||
|
|
31756ebfe2 | ||
|
|
0e6e28109f | ||
|
|
e76cb1f87a | ||
|
|
e770447686 | ||
|
|
6510fd2039 | ||
|
|
817722009c | ||
|
|
ea80bba0f7 |
No files matched your search
@@ -34,7 +34,9 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
|
||||
- Contributors first: `gh pr list --state open --search <area>` before you start; never duplicate theirs.
|
||||
- **No hardcoded user-facing strings.** Keep UI translation-ready from the start. In the launcher (`launcher/`), wrap visible text with `tr("...")` or `trf("... {arg} ...", &[("arg", &val)])` from `crate::i18n` instead of raw string literals or `format!`. In the game, use translation keys (`Text::tr`, `lang::get`), never hardcoded English strings.
|
||||
- **Claim before you start**: comment `/claim` on the issue (no issue yet: open one, then claim it). An assigned
|
||||
issue is taken: pick another. `/unclaim` when you stop; a claim with no open PR linking it is released after 5 days.
|
||||
issue is taken, and so is one labelled `in progress` (claimed on Discord): pick another. `/unclaim` when you stop;
|
||||
a claim with no open PR linking it is released after 5 days. The issues free to claim, without reading threads:
|
||||
`gh issue list --search 'is:open no:assignee -label:"in progress" -label:tracker -label:proposal -label:"not claimable" -linked:pr'`
|
||||
- **Controllers reach what registers.** Draw controls with a registering helper (`ui::button`, `settings_shell::foot_button`); `graphics_style::button` and `screen_draw::button` only paint, and registering one part of a screen ends its crosshair, so register all of it.
|
||||
|
||||
## Crates
|
||||
|
||||
@@ -1,77 +1,87 @@
|
||||
<p align="center">
|
||||
<img src="docs/media/banner.webp" alt="The Horizon website: the game you know, rebuilt in Rust" width="100%">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/horizon-rust/horizon/actions/workflows/ci.yml"><img src="https://github.com/horizon-rust/horizon/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
|
||||
<img src="https://img.shields.io/badge/Minecraft-Java%2026.3-5D9C3F" alt="Minecraft Java 26.3">
|
||||
<img src="https://img.shields.io/badge/Rust-stable-B7410E?logo=rust&logoColor=white" alt="Rust">
|
||||
<img src="https://img.shields.io/badge/vanilla%20parity-100%25-F0795A" alt="Vanilla parity 100%">
|
||||
<a href="#project-status"><img src="https://img.shields.io/badge/player--visible%20gaps-0%20open-F0795A" alt="Player-visible gaps open"></a>
|
||||
<img src="https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20Windows-2A2470" alt="Linux, macOS and Windows">
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3B2F7E" alt="MIT license"></a>
|
||||
<img src="docs/media/banner.webp" alt="Horizon: the game you know, rebuilt in Rust" width="100%">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://playhorizon.dev"><b>Website</b></a> ·
|
||||
<a href="#download"><b>Download</b></a> ·
|
||||
<a href="https://playhorizon.dev/docs/"><b>Documentation</b></a> ·
|
||||
<a href="https://github.com/horizon-rust/horizon/releases"><b>Release notes</b></a> ·
|
||||
<a href="https://playhorizon.dev/docs/"><b>Docs</b></a> ·
|
||||
<a href="https://discord.gg/playhorizon"><b>Discord</b></a> ·
|
||||
<a href="ROADMAP.md"><b>Roadmap</b></a> ·
|
||||
<a href="#project-status"><b>Project status</b></a> ·
|
||||
<a href="#contributing"><b>Contributing</b></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/vanilla%20parity-100%25-F0795A" alt="Vanilla parity 100%">
|
||||
<a href="#project-status"><img src="https://img.shields.io/badge/player--visible%20gaps-0%20open-F0795A" alt="Player-visible gaps open"></a>
|
||||
</p>
|
||||
|
||||
# Horizon
|
||||
|
||||
Minecraft: Java Edition 26.3, rewritten in Rust. Same worlds, same rules, same terrain, checked against the Java game by more than 300 differential harnesses. Free, open source and unofficial.
|
||||
**Minecraft: Java Edition 26.3, rewritten in Rust.** Your worlds, the same rules and the same terrain as the Java game, checked against it by hundreds of differential tests, in one native program with no Java. Free, open source (MIT) and unofficial: you play it with your own copy of Java Edition.
|
||||
|
||||
> Not an official Minecraft product, nor approved by or associated with Mojang or Microsoft. Horizon contains no Minecraft code, assets or data: you need your own copy of Java Edition 26.3.
|
||||
|
||||
## Download
|
||||
|
||||
Get **the Horizon launcher** from [playhorizon.dev](https://playhorizon.dev) or [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher&expanded=true) (Linux x86-64, macOS on Apple Silicon, macOS on Intel from the next Nightly, Windows x64). It installs and updates the game, prepares its files from your copy of Minecraft, and updates itself.
|
||||
|
||||
**You need** a Microsoft account that owns Java Edition ([why](docs/microsoft-account.md)) and a GPU with Vulkan or OpenGL 3.3 (Linux), Vulkan or DirectX 12 (Windows), or a Mac (Apple Silicon, or Intel from the next Nightly). An existing Minecraft install (official launcher, Prism, MultiMC...) is reused; without one, Play downloads 26.3 from Mojang for your account. Step by step: [Install and first launch](https://playhorizon.dev/docs/install).
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%"><img src="docs/media/launcher-play.webp" alt="The launcher's Home page: the selected instance ready to play, the version picker, Join a friend and the instance's tabs"></td>
|
||||
<td width="50%"><img src="docs/media/launcher-settings.webp" alt="The launcher's Settings page: Microsoft account, offline name, updates, appearance and accent colour"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<p align="center">
|
||||
<img src="docs/media/readme/valley.webp" alt="A river valley with a plains village, savanna hills and a cherry grove, seen from above with a long view distance" width="100%">
|
||||
</p>
|
||||
|
||||
## What works today
|
||||
|
||||
- **The game:** vanilla world generation, mobs, villagers, raids, redstone, every command, the HUD and the recipe book, on vanilla saves. Worlds from 1.8 to 26.2 are upgraded. [Status](#project-status).
|
||||
- **Extras:** new menus, graphics presets, distant terrain up to 1024 chunks, controllers, zoom, minimap and more, off by default where they change the vanilla look. [Settings](https://playhorizon.dev/docs/settings).
|
||||
- **Packs:** resource packs, and data packs with functions, predicates, item modifiers and world generation.
|
||||
- **Multiplayer:** invite friends with a code, Open to LAN, join servers (online mode included). [Multiplayer](https://playhorizon.dev/docs/multiplayer).
|
||||
- **Mods:** content mods (blocks, items, data) and native Rust mods, built by the launcher with nothing to install. Java mod jars do not run. [Mods](https://playhorizon.dev/docs/mods).
|
||||
Everything below is in the current release.
|
||||
|
||||
In the Rust client, with vanilla assets and no mods or shaders:
|
||||
- **The game.** Survival, creative, hardcore and spectator; vanilla world generation in all three dimensions, mobs, villagers, raids, redstone, every command, the HUD and the recipe book. Vanilla saves open as they are, and worlds from 1.8 to 26.2 are upgraded.
|
||||
- **Multiplayer.** Join vanilla and Horizon servers (online mode included), open a world to LAN, or invite friends with a code from the launcher's Home page.
|
||||
- **Graphics.** Distant terrain up to 1,024 chunks, graphics presets, HDR and controllers, all off by default where they would change the vanilla look ([settings](https://playhorizon.dev/docs/settings)).
|
||||
- **Packs.** Resource packs, data packs and shader packs ([below](#mods-and-shader-packs)).
|
||||
- **Mods.** Content mods and native Rust mods, installed from the launcher ([below](#mods-and-shader-packs)).
|
||||
- **Servers.** Host one from the launcher, or run `horizon-server` on a VPS ([below](#host-a-server)).
|
||||
- **The launcher.** It installs and updates the game, prepares its files from your copy of Minecraft, and updates itself.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td colspan="2"><img src="docs/media/sunset.webp" alt="Sunset over a savanna river valley and a plains village"></td>
|
||||
<td width="50%"><img src="docs/media/readme/sunset.webp" alt="Sunset behind a cherry grove on a hill, with a river below"></td>
|
||||
<td width="50%"><img src="docs/media/readme/nether.webp" alt="A crimson forest in the Nether, with a piglin among the fungi"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%"><img src="docs/media/survival.webp" alt="Survival at dusk: a zombie walks up, with the hotbar, health, hunger and experience"></td>
|
||||
<td width="50%"><img src="docs/media/settings.webp" alt="Vanilla's Video Settings screen with Horizon's own buttons at the top: Horizon Graphics and Advanced Graphics"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%"><img src="docs/media/village.webp" alt="A plains village among savanna hills, with farms, villagers and an iron golem"></td>
|
||||
<td width="50%"><img src="docs/media/nether.webp" alt="A crimson forest in the Nether with a zombified piglin"></td>
|
||||
<td width="50%"><img src="docs/media/readme/survival.webp" alt="Survival in a village square: two villagers, flowers, the hotbar, health and hunger"></td>
|
||||
<td width="50%"><img src="docs/media/readme/multiplayer.webp" alt="Two players in a LAN world: the other player's name tag and the player list"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<sub>Nightly 2026-11-05 on a Mac mini (Apple M4 Pro, Metal): vanilla's textures, no mods or shader packs, render distance 32 with distant terrain on.</sub>
|
||||
|
||||
## Download
|
||||
|
||||
Get **the Horizon launcher** from [playhorizon.dev](https://playhorizon.dev), or from [the launcher releases](https://github.com/horizon-rust/horizon/releases?q=launcher&expanded=true):
|
||||
|
||||
| Platform | File | Needs |
|
||||
| --- | --- | --- |
|
||||
| Windows x64 | `horizon-launcher-setup.exe` | Windows 10 or 11, a GPU with Vulkan or DirectX 12 |
|
||||
| Windows on Arm | `horizon-launcher-setup-arm64.exe` | Windows 11 on Arm |
|
||||
| macOS, Apple Silicon | `horizon-launcher-macos-arm64.dmg` | macOS 12 or later |
|
||||
| macOS, Intel | `horizon-launcher-macos-x64.dmg` | macOS 12 or later |
|
||||
| Linux x86-64 | `horizon-launcher-linux-x86_64.AppImage` or `.tar.gz` | a glibc distribution, X11 or Wayland, a GPU with Vulkan or OpenGL 3.3 |
|
||||
|
||||
**You also need a Microsoft account that owns Minecraft: Java Edition.** You sign in with it in the launcher, which plays only with an account that owns the game ([why](docs/microsoft-account.md)). Horizon contains no Minecraft code or assets: an existing install (official launcher, Prism, MultiMC...) is reused, and without one the launcher downloads 26.3 from Mojang for your account. Step by step: [Install and first launch](https://playhorizon.dev/docs/install).
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%"><img src="docs/media/readme/launcher-home.webp" alt="The launcher's Home page: the Horizon instance, Play, the version picker, the friend invite field, screenshots and the patch log"></td>
|
||||
<td width="50%"><img src="docs/media/readme/launcher-mods.webp" alt="The launcher's Mods page: Horizon's mod registry with Zoom selected, and tabs for modpacks, resource packs, shader packs and data packs"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Mods and shader packs
|
||||
|
||||
- **Content mods** (`.hmod`): blocks, items, recipes, loot, models and textures, with no code.
|
||||
- **Native mods** (`.hnmod`): Rust that the launcher compiles into the game for you, with nothing to install.
|
||||
- **Java mods** (Fabric, NeoForge, Forge) don't run: Horizon has no Java. The launcher carries over the data and assets inside their jars, and imports modpacks (`.mrpack`, CurseForge zips, Prism instances) the same way ([modpacks](https://playhorizon.dev/docs/modpacks)).
|
||||
- **Shader packs**: Iris/OptiFine-format packs (`shaders/` folders) and Horizon's own WGSL packs, under Video Settings → Shader Packs ([pack format](docs/shaders.md)).
|
||||
|
||||
Mods are off by default: a fresh install plays exactly like vanilla 26.3. The launcher's Mods page browses Horizon's registry and Modrinth. [Mods](https://playhorizon.dev/docs/mods) · [make a mod](docs/mods.md).
|
||||
|
||||
## Host a server
|
||||
|
||||
The launcher's **Servers** page creates, starts, backs up and updates a server, and invites friends over Tailscale. On a VPS, `horizon-server` installs with one command and needs no Java. [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
|
||||
|
||||
## Modpacks
|
||||
|
||||
The launcher imports Modrinth and CurseForge packs and plays the mods Horizon can carry over: today, content mods made of data and assets, not Java code. [Modpacks](https://playhorizon.dev/docs/modpacks).
|
||||
The launcher's **Servers** page creates, starts, backs up and updates a server, and invites friends. On a VPS, `horizon-server` installs with one command, needs no Java, and takes vanilla 26.3 and Horizon players alike. [Host a server](https://playhorizon.dev/docs/hosting) ([reference](docs/hosting.md)).
|
||||
|
||||
## Agent control
|
||||
|
||||
@@ -80,13 +90,11 @@ AI agents and scripts can play Horizon. `horizon-ctl`, shipped with the game, re
|
||||
```sh
|
||||
horizon-ctl launch --world Survival --seed 5 --wait # finds the version the launcher installed
|
||||
horizon-ctl state # position, health, food, target, screen...
|
||||
horizon-ctl find-block birch_log # the nearest logs
|
||||
horizon-ctl goto -0.5 65 -2.5 --near 2.5 # walks there along a path
|
||||
horizon-ctl break -1 65 -3 # mines it; replies with the drops
|
||||
horizon-ctl open-inventory && horizon-ctl click-slot 0 # menus: slots, buttons by label, text fields
|
||||
```
|
||||
|
||||
Commands block until done and reply with the result; `horizon-ctl help` lists them all. [Agent control](docs/native-client.md#agent-control-horizon-ctl-and-mcp).
|
||||
`horizon-ctl help` lists every command. [Agent control](docs/native-client.md#agent-control-horizon-ctl-and-mcp).
|
||||
|
||||
## Build from source
|
||||
|
||||
@@ -105,13 +113,9 @@ cargo run --release -p horizon-client
|
||||
|
||||
Prepared files land in the gitignored `.local/`: never commit or share them. Options, controls and architecture: [the native client guide](docs/native-client.md); all developer docs: [docs/README.md](docs/README.md).
|
||||
|
||||
## Roadmap
|
||||
|
||||
What is in progress, what comes next and where to help: [ROADMAP.md](ROADMAP.md), or [playhorizon.dev/hub](https://playhorizon.dev/hub) with the work board and what shipped.
|
||||
|
||||
## Contributing
|
||||
|
||||
Issues and pull requests are welcome: read [CONTRIBUTING.md](CONTRIBUTING.md) (vanilla first, small pull requests). Questions: [Discord](https://discord.gg/playhorizon). Before a pull request, run the self-check and paste its output:
|
||||
Issues and pull requests are welcome: read [CONTRIBUTING.md](CONTRIBUTING.md) (vanilla first, small pull requests), and see [ROADMAP.md](ROADMAP.md) or [playhorizon.dev/hub](https://playhorizon.dev/hub) for what's next. Questions: [Discord](https://discord.gg/playhorizon). Before a pull request, run the self-check and paste its output:
|
||||
|
||||
```sh
|
||||
git config core.hooksPath .githooks
|
||||
@@ -124,7 +128,7 @@ It runs rustfmt, clippy, the touched crates' tests, the Python tests, the public
|
||||
|
||||
**Player-visible gaps: 0 open** of the 833 the [port ledger](tools/port_ledger.py) found, reading every vanilla 26.3 method against Horizon's code: vanilla behaviour a player would notice that Horizon doesn't have yet. By area: Animals [0](https://github.com/horizon-rust/horizon/issues/3512); Monsters and bosses [0](https://github.com/horizon-rust/horizon/issues/3513); Entities, projectiles, vehicles and decorations [0](https://github.com/horizon-rust/horizon/issues/3514); Items, enchantments and inventories [0](https://github.com/horizon-rust/horizon/issues/3515); Blocks, redstone and fluids [0](https://github.com/horizon-rust/horizon/issues/3516); World, world generation and saves [0](https://github.com/horizon-rust/horizon/issues/3517); Screens, HUD and client packets [0](https://github.com/horizon-rust/horizon/issues/3518); Entity and block rendering, models [0](https://github.com/horizon-rust/horizon/issues/3519); Server and players on the server [0](https://github.com/horizon-rust/horizon/issues/3520); Mob AI, villagers and raids [0](https://github.com/horizon-rust/horizon/issues/3521). This number goes down as gaps close (`tools/parity_gaps.py`); the harness results below say that what is tested matches.
|
||||
|
||||
The table is the project's own audit: every subsystem matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not that nothing else differs.
|
||||
The table is the project's own audit: every subsystem matches vanilla in every harness that compares it. 100% means 100% of what the harnesses compare, not that nothing else differs. What players still run into: [known issues](docs/known-issues.md).
|
||||
|
||||
### Java parity: 100%
|
||||
|
||||
@@ -147,14 +151,8 @@ Scores: 0 (absent), 0.25 (started), 0.5 (playable, major features missing), 0.75
|
||||
| Audio | 1 | None: the sound engine, sound events, music, situational music, ambience and subtitles match vanilla |
|
||||
| Block behaviours and ticking | 1 | None: block updates, random ticks, redstone, dispensers (6,000 cases), crafters (4,000), container spills (4,000), copper golems and the sculk family (435 scenarios) match |
|
||||
|
||||
### Known issues
|
||||
|
||||
Skins served over https show the default skin unless already cached; pack icons show the unknown-pack texture in game; item tooltips lack attribute lines.
|
||||
|
||||
## Licence
|
||||
|
||||
[MIT](LICENSE), for the project's original work; dependencies keep their own licences ([notices](THIRD_PARTY_NOTICES.md)). Minecraft is a trademark of Mojang Synergies AB; its code and assets belong to Mojang and Microsoft under the [Minecraft EULA](https://www.minecraft.net/en-us/eula).
|
||||
[MIT](LICENSE), for the project's original work; dependencies keep their own licences ([notices](THIRD_PARTY_NOTICES.md)). Horizon is made by [Ludovic Marie](https://lu4ovic.com) ([@ludovic111](https://github.com/ludovic111)) and [its contributors](https://github.com/horizon-rust/horizon/graphs/contributors); support it on [GitHub Sponsors](https://github.com/sponsors/ludovic111).
|
||||
|
||||
## Author
|
||||
|
||||
Made by [Ludovic Marie](https://lu4ovic.com) ([@ludovic111](https://github.com/ludovic111)).
|
||||
NOT AN OFFICIAL MINECRAFT PRODUCT. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT. Minecraft is a trademark of Mojang Synergies AB; its code and assets belong to Mojang and Microsoft under the [Minecraft EULA](https://www.minecraft.net/en-us/eula).
|
||||
+40
-2
@@ -266,7 +266,7 @@ Shader packs were removed and are being rebuilt from scratch. They come back, of
|
||||
|
||||
A clean-room rewrite in `horizon-shaderpack`, from the packs' public format documentation only: reading packs, the preprocessor and `.properties` files are merged ([#1584](https://github.com/horizon-rust/horizon/issues/1584), [#1585](https://github.com/horizon-rust/horizon/issues/1585), [#1586](https://github.com/horizon-rust/horizon/issues/1586)); translating programs to WGSL through naga, buffers, uniforms and the terrain, entity, sky, weather and hand stages are in review ([#1698](https://github.com/horizon-rust/horizon/issues/1698) to [#1801](https://github.com/horizon-rust/horizon/issues/1801)).
|
||||
|
||||
More: [#1561](https://github.com/horizon-rust/horizon/issues/1561) · [#1771](https://github.com/horizon-rust/horizon/issues/1771) · [#1786](https://github.com/horizon-rust/horizon/issues/1786)
|
||||
More: [#1561](https://github.com/horizon-rust/horizon/issues/1561) · [#1771](https://github.com/horizon-rust/horizon/issues/1771) · [#1786](https://github.com/horizon-rust/horizon/issues/1786) · [#9045](https://github.com/horizon-rust/horizon/issues/9045) · [#6233](https://github.com/horizon-rust/horizon/issues/6233) · [#6485](https://github.com/horizon-rust/horizon/issues/6485) · [#3734](https://github.com/horizon-rust/horizon/issues/3734) · [#3125](https://github.com/horizon-rust/horizon/issues/3125)
|
||||
|
||||
</details>
|
||||
|
||||
@@ -298,6 +298,8 @@ Far terrain drawn from the world you really built and explored, caves and builds
|
||||
|
||||
Keep a 3D mip store on disk built from real chunks, drawn by the GPU beside the seed preview for areas nobody has visited. Written from scratch and measured before it is kept. Builds on Far Detail and the far-terrain levels of detail already shipped; the open work on them is [#718](https://github.com/horizon-rust/horizon/issues/718).
|
||||
|
||||
More: [#4678](https://github.com/horizon-rust/horizon/issues/4678)
|
||||
|
||||
</details>
|
||||
|
||||
#### A batch of targeted optimisations
|
||||
@@ -308,7 +310,7 @@ Several smaller speed-ups, each measured: idle block entities that sleep, cheape
|
||||
|
||||
Block entities and hoppers that skip work when idle (the hopper item scan is already skipped when no item is near); a particle frustum test first (also a parity gap), then particle quads built on the GPU; jigsaw shortcuts that skip blocked connectors and pool elements known not to fit, with layouts unchanged (trial chambers and ancient cities first); batched raw mouse input on Windows for 8000 Hz mice; a better atlas packer for huge resource packs (a 64x pack builds a 704 MiB atlas for 452 MiB of textures); sign-heavy scenes ([#1461](https://github.com/horizon-rust/horizon/issues/1461)). Profile first, keep what pays, show before and after numbers.
|
||||
|
||||
More: [docs/profiling.md](docs/profiling.md)
|
||||
More: [docs/profiling.md](docs/profiling.md) · [#7752](https://github.com/horizon-rust/horizon/issues/7752) · [#8251](https://github.com/horizon-rust/horizon/issues/8251) · [#10229](https://github.com/horizon-rust/horizon/issues/10229)
|
||||
|
||||
</details>
|
||||
|
||||
@@ -322,6 +324,8 @@ Recognise mods built for versions before 26.1 in the mod browser, so they can be
|
||||
|
||||
A mapping table from intermediary and SRG names so pre-26.1 jars can be classified, and a rename for mod ids with dashes (`fabric-api`); the port step then treats them like newer jars ([docs/mod-browser.md](docs/mod-browser.md)).
|
||||
|
||||
More: [#10262](https://github.com/horizon-rust/horizon/issues/10262)
|
||||
|
||||
</details>
|
||||
|
||||
#### A modpack's own resource and shader packs
|
||||
@@ -536,6 +540,8 @@ Contraptions and physics like the Create and Aeronautics mods, as a mod for Hori
|
||||
|
||||
Issue [#805](https://github.com/horizon-rust/horizon/issues/805) (porting QOL mods) closed with this answer: it would come as a native Horizon mod.
|
||||
|
||||
More: [#6302](https://github.com/horizon-rust/horizon/issues/6302) · [#5313](https://github.com/horizon-rust/horizon/issues/5313) · [#5556](https://github.com/horizon-rust/horizon/issues/5556)
|
||||
|
||||
</details>
|
||||
|
||||
#### Proximity voice chat as a native mod
|
||||
@@ -558,6 +564,8 @@ Every past and future Minecraft version prepared for Horizon automatically. When
|
||||
|
||||
The port pipeline ([docs/version-ports.md](docs/version-ports.md), `tools/version_port.py`) detects a release, prepares it, diffs its classes against the pinned 26.3, regenerates the data-driven tables, runs every parity harness on it and writes a port report that becomes issues. The goal is as little human work as possible for any version, past or future. Detection and the first slices are merged; the scheduled agent that runs it every day isn't installed yet. Two versions in one build are out of scope.
|
||||
|
||||
More: [#4758](https://github.com/horizon-rust/horizon/issues/4758)
|
||||
|
||||
</details>
|
||||
|
||||
#### Controllers: tested on real pads, and in the launcher
|
||||
@@ -568,6 +576,8 @@ Make controller support solid: tested with real PlayStation and Xbox pads and a
|
||||
|
||||
The game side is merged ([docs/controllers.md](docs/controllers.md)): pads read with gilrs, menus, rumble, glyphs, remapping and DualSense adaptive triggers, lightbar and haptics, but not yet confirmed on real pads or a Steam Deck (DualSense haptics stay behind a switch until then). Left: a test pass on real hardware, and the same focus navigation in the GPUI launcher.
|
||||
|
||||
More: [#10298](https://github.com/horizon-rust/horizon/issues/10298) · [#8678](https://github.com/horizon-rust/horizon/issues/8678)
|
||||
|
||||
</details>
|
||||
|
||||
#### The full debug screen
|
||||
@@ -668,6 +678,16 @@ A shared resource store keyed by the preparer's digest, no prepare intermediates
|
||||
|
||||
</details>
|
||||
|
||||
#### A launcher screen readers can read
|
||||
|
||||
Screen readers read the launcher's buttons, lists and settings, and every part of it works from the keyboard.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Issue [#4537](https://github.com/horizon-rust/horizon/issues/4537): the launcher is GPUI (not Electron), so it exposes nothing to UI Automation, NSAccessibility or AT-SPI yet. Needs an accessibility tree from GPUI (an AccessKit bridge) and a role and label on each widget.
|
||||
|
||||
</details>
|
||||
|
||||
#### A visible beta label
|
||||
|
||||
A clear 'beta' label in the launcher and the game, so nobody is surprised that Horizon is still being built.
|
||||
@@ -690,6 +710,18 @@ Issue [#1709](https://github.com/horizon-rust/horizon/issues/1709): aarch64 buil
|
||||
|
||||
</details>
|
||||
|
||||
#### The launcher builds Horizon on other systems
|
||||
|
||||
On a system Horizon has no download for, such as FreeBSD, the launcher offers to build the game on your computer, as it already does for native mods.
|
||||
|
||||
<details><summary>Technical</summary>
|
||||
|
||||
Issue [#4354](https://github.com/horizon-rust/horizon/issues/4354): when no release asset matches the platform, the launcher fetches the tagged source and runs the same pinned-toolchain `cargo build --locked` its native-mod builds use, with a warning about the time it takes. The build is unsigned, so the launcher marks the version as built locally.
|
||||
|
||||
More: [docs/native-mods.md](docs/native-mods.md)
|
||||
|
||||
</details>
|
||||
|
||||
#### Horizon on Windows ARM64
|
||||
|
||||
A native Horizon for Windows on ARM computers, such as Snapdragon laptops.
|
||||
@@ -710,6 +742,8 @@ Versions of Horizon for Android, iOS and web browsers. Nothing official yet; con
|
||||
|
||||
No official build. Android was first closed as not planned, with touch controls, a mobile interface, packaging and loading the player's own assets all missing; the roadmap now includes it, iOS and a build for the browser as long-term goals. First pieces from contributors: `horizon-dualsense` compiling on iOS ([#1813](https://github.com/horizon-rust/horizon/issues/1813)) and helper-thread limits on Android ([#1751](https://github.com/horizon-rust/horizon/issues/1751)). Preparing without Java ([#1750](https://github.com/horizon-rust/horizon/issues/1750)) matters here too. Horizon ships none of Mojang's files on any platform.
|
||||
|
||||
More: [#9134](https://github.com/horizon-rust/horizon/issues/9134) · [#8069](https://github.com/horizon-rust/horizon/issues/8069) · [#4153](https://github.com/horizon-rust/horizon/issues/4153)
|
||||
|
||||
</details>
|
||||
|
||||
#### Horizon on Intel Macs
|
||||
@@ -752,6 +786,8 @@ Record, replay and edit videos without leaving the game: record in a keypress, r
|
||||
|
||||
Three stages. Recording: capture gameplay in a keypress with an encoder thread and little frame-time cost. Replay: a replay mode for recorded sessions. Editing: cuts, camera paths, titles and export. None of it exists yet; the agent-control socket ([docs/agent-control.md](docs/agent-control.md)) already lets tools drive the game for scripted shots.
|
||||
|
||||
More: [#10025](https://github.com/horizon-rust/horizon/issues/10025)
|
||||
|
||||
</details>
|
||||
|
||||
#### Game menus in the launcher's style
|
||||
@@ -842,6 +878,8 @@ Every download Horizon makes is checked against the project's signature, includi
|
||||
|
||||
Releases carry signed checksums, and the launcher and `horizon-server update` refuse anything unsigned ([#1011](https://github.com/horizon-rust/horizon/issues/1011), [#1014](https://github.com/horizon-rust/horizon/issues/1014)). Left: the first install through `install.sh` and `install.ps1` still trusts only the checksum GitHub publishes, because verifying Ed25519 signatures needs OpenSSL 3, which macOS doesn't ship ([docs/security.md](docs/security.md)).
|
||||
|
||||
More: [#10354](https://github.com/horizon-rust/horizon/issues/10354)
|
||||
|
||||
</details>
|
||||
|
||||
## Help wanted
|
||||
|
||||
@@ -108,6 +108,16 @@ impl<T> CompiledChoices<T> {
|
||||
multipart: self.multipart.iter().map(|c| c.map(&mut f)).collect(),
|
||||
}
|
||||
}
|
||||
/// A variant that needs no random selection and produces exactly one model.
|
||||
pub fn single_variant(&self, selected: &SelectedModels) -> Option<&T> {
|
||||
let SelectedModels::Variant(index) = selected else {
|
||||
return None;
|
||||
};
|
||||
match self.variants.get(*index)? {
|
||||
Choice::Single(value) => Some(value),
|
||||
Choice::Weighted { .. } => None,
|
||||
}
|
||||
}
|
||||
/// The variant `particleMaterial` comes from: the first entry of a weighted
|
||||
/// list (`WeightedVariants`) and, for a multipart root, of its first
|
||||
/// selector whether or not that selector applies (`MultiPartModel`).
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
//! NBT path parser output and byte cursor for the pinned Java oracle.
|
||||
use horizon_client_core::nbt_path::NbtPath;
|
||||
use std::io::{BufRead, Write};
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let input = std::io::BufReader::new(std::fs::File::open(args.next().unwrap())?);
|
||||
let mut output = std::io::BufWriter::new(std::fs::File::create(args.next().unwrap())?);
|
||||
for line in input.lines() {
|
||||
let text: String = serde_json::from_str(&line?)?;
|
||||
match NbtPath::parse_prefix(&text) {
|
||||
Ok((path, cursor)) => {
|
||||
write!(output, "OK {cursor} ")?;
|
||||
for value in path.as_str().as_bytes() {
|
||||
write!(output, "{value:02x}")?;
|
||||
}
|
||||
writeln!(output)?;
|
||||
}
|
||||
Err(error) => writeln!(output, "ERR {} {}", error.cursor, error.key)?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -111,7 +111,15 @@ impl Parser<'_> {
|
||||
map.insert(key, self.value()?);
|
||||
self.ws();
|
||||
match self.peek() {
|
||||
Some(b',') => self.i += 1,
|
||||
// SnbtGrammar: an entry may be followed by a comma before the brace.
|
||||
Some(b',') => {
|
||||
self.i += 1;
|
||||
self.ws();
|
||||
if self.peek() == Some(b'}') {
|
||||
self.i += 1;
|
||||
return Ok(Value::Object(map));
|
||||
}
|
||||
}
|
||||
Some(b'}') => {
|
||||
self.i += 1;
|
||||
return Ok(Value::Object(map));
|
||||
@@ -140,7 +148,15 @@ impl Parser<'_> {
|
||||
list.push(self.value()?);
|
||||
self.ws();
|
||||
match self.peek() {
|
||||
Some(b',') => self.i += 1,
|
||||
// SnbtGrammar: a list or array may end with a comma.
|
||||
Some(b',') => {
|
||||
self.i += 1;
|
||||
self.ws();
|
||||
if self.peek() == Some(b']') {
|
||||
self.i += 1;
|
||||
return Ok(Value::Array(list));
|
||||
}
|
||||
}
|
||||
Some(b']') => {
|
||||
self.i += 1;
|
||||
return Ok(Value::Array(list));
|
||||
@@ -497,6 +513,22 @@ pub fn name_text(tag: &Tag) -> Text {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
/// SnbtGrammar: a compound or list may end with one comma; two in a row
|
||||
/// stay an error.
|
||||
#[test]
|
||||
fn a_component_takes_one_trailing_comma() {
|
||||
let parse = |s: &str| parse_snbt(s).map(|(v, _)| v);
|
||||
assert_eq!(
|
||||
parse("{text:\"a\",color:\"red\",}"),
|
||||
parse("{text:\"a\",color:\"red\"}")
|
||||
);
|
||||
assert_eq!(
|
||||
parse("[\"a\", {text:\"b\",} ,]"),
|
||||
parse("[\"a\",{text:\"b\"}]")
|
||||
);
|
||||
assert!(parse("{text:\"a\",,}").is_err());
|
||||
assert!(parse("[\"a\",,]").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_color_follows_text_color_parse_color() {
|
||||
|
||||
@@ -394,7 +394,7 @@ pub fn apply_deprecated(map: &mut HashMap<String, String>, json: &[u8]) {
|
||||
/// characters in strings, `=` or `=>` and `;`), which vanilla reads lang files with (expected
|
||||
/// from `Language.loadFromJson`, Gson). A file that is not JSON even so adds nothing; a value
|
||||
/// that is not a string, number or boolean (`GsonHelper.convertToString`) ends the file there.
|
||||
fn read_lang_file(bytes: &[u8], map: &mut HashMap<String, String>) {
|
||||
pub fn read_lang_file(bytes: &[u8], map: &mut HashMap<String, String>) {
|
||||
use horizon_world::datafix::json::{Json, parse_lenient};
|
||||
if let Ok(entries) = serde_json::from_slice::<HashMap<String, String>>(bytes) {
|
||||
map.extend(entries);
|
||||
|
||||
@@ -81,7 +81,7 @@ fn elements(tag: &Tag) -> Option<Vec<Tag>> {
|
||||
}
|
||||
/// `DoubleTag.intValue` / `FloatTag.intValue` floor before the i32 conversion;
|
||||
/// integral tags use their intValue narrowing. Byte arrays then keep the low byte.
|
||||
fn int_value(t: &Tag) -> Option<i32> {
|
||||
pub fn int_value(t: &Tag) -> Option<i32> {
|
||||
Some(match t {
|
||||
Tag::Byte(v) => i32::from(*v),
|
||||
Tag::Short(v) => i32::from(*v),
|
||||
@@ -726,11 +726,16 @@ fn parse_node(text: &str, i: usize, first: bool) -> Result<(Node, usize), PathEr
|
||||
Some(b']') => Ok((Node::AllElements, j + 1)),
|
||||
_ => {
|
||||
let start = j;
|
||||
if b.get(j) == Some(&b'-') {
|
||||
// StringReader.readInt scans all allowed number characters
|
||||
// before parsing, including malformed signs and decimal points.
|
||||
while b
|
||||
.get(j)
|
||||
.is_some_and(|c| c.is_ascii_digit() || matches!(c, b'-' | b'.'))
|
||||
{
|
||||
j += 1;
|
||||
}
|
||||
while b.get(j).is_some_and(u8::is_ascii_digit) {
|
||||
j += 1;
|
||||
if start == j {
|
||||
return Err(PathError::new("parsing.int.expected", vec![], start));
|
||||
}
|
||||
let n: i32 = text[start..j].parse().map_err(|_| {
|
||||
PathError::new(
|
||||
@@ -792,6 +797,13 @@ fn read_quoted(text: &str, i: usize) -> Result<(String, usize), PathError> {
|
||||
let mut escaped = false;
|
||||
for (o, c) in text[i + 1..].char_indices() {
|
||||
if escaped {
|
||||
if c != quote && c != '\\' {
|
||||
return Err(PathError::new(
|
||||
"parsing.quote.escape",
|
||||
vec![c.to_string()],
|
||||
i + 1 + o,
|
||||
));
|
||||
}
|
||||
out.push(c);
|
||||
escaped = false;
|
||||
} else if c == '\\' {
|
||||
@@ -840,6 +852,40 @@ fn parse_compound(text: &str, i: usize) -> Result<(Tag, usize), PathError> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn quoted_escapes_and_index_tokens_match_brigadier() {
|
||||
let cases = [
|
||||
(r#""a\q""#, "parsing.quote.escape", vec!["q"], 3),
|
||||
(r#"'a\x'"#, "parsing.quote.escape", vec!["x"], 3),
|
||||
("[1.0]", "parsing.int.invalid", vec!["1.0"], 1),
|
||||
("[.]", "parsing.int.invalid", vec!["."], 1),
|
||||
("[--1]", "parsing.int.invalid", vec!["--1"], 1),
|
||||
("[2147483648]", "parsing.int.invalid", vec!["2147483648"], 1),
|
||||
("[+1]", "parsing.int.expected", vec![], 1),
|
||||
(r#""é\q""#, "parsing.quote.escape", vec!["q"], 4),
|
||||
(r#""é"[1.0]"#, "parsing.int.invalid", vec!["1.0"], 5),
|
||||
];
|
||||
let mut failures = Vec::new();
|
||||
for (text, key, args, cursor) in cases {
|
||||
match NbtPath::parse_prefix(text) {
|
||||
Err(error) if error.key == key && error.args == args && error.cursor == cursor => {}
|
||||
other => failures.push(format!("{text:?}: {other:?}")),
|
||||
}
|
||||
}
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_quoted_escapes_still_select_the_exact_field() {
|
||||
let root = Tag::compound([("a\"b", Tag::Int(7)), ("a\\b", Tag::Int(8))]);
|
||||
for (path, value) in [(r#""a\"b""#, 7), (r#""a\\b""#, 8)] {
|
||||
assert_eq!(
|
||||
NbtPath::parse(path).unwrap().get(&root).unwrap(),
|
||||
vec![Tag::Int(value)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_provenance_handles_quotes_and_filters_without_matching_lists() {
|
||||
for text in [
|
||||
|
||||
@@ -393,19 +393,38 @@ impl FullscreenState {
|
||||
let reapply = self.set_exclusive(exclusive);
|
||||
self.set_preferred_mode(preferred);
|
||||
let mode_change = !in_video_screen && self.take_mode_change();
|
||||
match request {
|
||||
Some(false) => window.set_fullscreen(None),
|
||||
Some(true) => window.set_fullscreen(Some(fullscreen_request(
|
||||
if wanted && window.is_maximized() {
|
||||
window.set_maximized(false);
|
||||
window.set_fullscreen(Some(fullscreen_request(
|
||||
window,
|
||||
exclusive,
|
||||
preferred.as_ref(),
|
||||
))),
|
||||
None if reapply || mode_change => window.set_fullscreen(Some(fullscreen_request(
|
||||
window,
|
||||
exclusive,
|
||||
preferred.as_ref(),
|
||||
))),
|
||||
None => {}
|
||||
)));
|
||||
} else {
|
||||
match request {
|
||||
Some(false) => window.set_fullscreen(None),
|
||||
Some(true) => {
|
||||
if window.is_maximized() {
|
||||
window.set_maximized(false);
|
||||
}
|
||||
window.set_fullscreen(Some(fullscreen_request(
|
||||
window,
|
||||
exclusive,
|
||||
preferred.as_ref(),
|
||||
)));
|
||||
}
|
||||
None if reapply || mode_change => {
|
||||
if window.is_maximized() {
|
||||
window.set_maximized(false);
|
||||
}
|
||||
window.set_fullscreen(Some(fullscreen_request(
|
||||
window,
|
||||
exclusive,
|
||||
preferred.as_ref(),
|
||||
)));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,13 +611,13 @@ pub fn fullscreen_request(
|
||||
preferred: Option<&VideoMode>,
|
||||
) -> winit::window::Fullscreen {
|
||||
use winit::window::Fullscreen;
|
||||
if !exclusive {
|
||||
return Fullscreen::Borderless(None);
|
||||
}
|
||||
let Some(monitor) = window
|
||||
let monitor = window
|
||||
.current_monitor()
|
||||
.or_else(|| window.primary_monitor())
|
||||
else {
|
||||
.or_else(|| window.primary_monitor());
|
||||
if !exclusive {
|
||||
return Fullscreen::Borderless(monitor);
|
||||
}
|
||||
let Some(monitor) = monitor else {
|
||||
return Fullscreen::Borderless(None);
|
||||
};
|
||||
let wanted = preferred.and_then(|p| {
|
||||
@@ -608,7 +627,7 @@ pub fn fullscreen_request(
|
||||
});
|
||||
match wanted.or_else(|| monitor_current_mode(&monitor)) {
|
||||
Some(mode) => Fullscreen::Exclusive(mode),
|
||||
None => Fullscreen::Borderless(None),
|
||||
None => Fullscreen::Borderless(Some(monitor)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,6 +647,22 @@ pub fn icon() -> Option<winit::window::Icon> {
|
||||
winit::window::Icon::from_rgba(bytes, info.width, info.height).ok()
|
||||
}
|
||||
|
||||
/// Apply Horizon's icon to every window. Windows has a separate taskbar icon slot from the small
|
||||
/// window icon; requiring the decoded icon there prevents a generic taskbar icon from appearing.
|
||||
pub fn with_horizon_icon(
|
||||
attributes: winit::window::WindowAttributes,
|
||||
) -> winit::window::WindowAttributes {
|
||||
let icon = icon();
|
||||
#[cfg(target_os = "windows")]
|
||||
let icon = Some(icon.expect("Horizon's icon must be available on Windows"));
|
||||
|
||||
let attributes = attributes.with_window_icon(icon.clone());
|
||||
#[cfg(target_os = "windows")]
|
||||
let attributes =
|
||||
winit::platform::windows::WindowAttributesExtWindows::with_taskbar_icon(attributes, icon);
|
||||
attributes
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
//! small mixer standing in for OpenAL: one channel per source with its gain,
|
||||
//! pitch, linear distance attenuation (`AL_LINEAR_DISTANCE`, reference 0,
|
||||
//! rolloff 1), looping, relative flag and source state, Ogg/Vorbis decoding
|
||||
//! (streams on a worker thread) and OpenAL Soft's stereo panning.
|
||||
//! (streams on a worker thread) and OpenAL Soft's mixing: mono sources panned
|
||||
//! and attenuated, multi-channel buffers (stereo music and records) played
|
||||
//! direct as OpenAL does.
|
||||
use crate::Result;
|
||||
use crate::sound_engine::{
|
||||
Attenuation, Backend, Bound, ChannelId, InstanceId, PlayResult, SoundEngine, SoundInstance,
|
||||
@@ -29,7 +31,7 @@ enum State {
|
||||
}
|
||||
/// An OpenAL source.
|
||||
struct Channel {
|
||||
samples: Option<(Arc<Vec<f32>>, u32)>,
|
||||
samples: Option<(Arc<Vec<f32>>, u32, u16)>,
|
||||
position: f64,
|
||||
state: State,
|
||||
gain: f32,
|
||||
@@ -135,8 +137,8 @@ impl Drop for MixerBackend {
|
||||
static SHARED_CHANNELS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
/// Two players' copies of one sound: started within this of each other.
|
||||
const SAME_SOUND: std::time::Duration = std::time::Duration::from_millis(150);
|
||||
/// Decoded samples and their rate (`None`: the file did not load).
|
||||
type Samples = Option<(Arc<Vec<f32>>, u32)>;
|
||||
/// Decoded interleaved samples, their rate and channel count (`None`: the file did not load).
|
||||
type Samples = Option<(Arc<Vec<f32>>, u32, u16)>;
|
||||
/// Decoded static sounds (`SoundBufferLibrary`'s buffers), dropped least
|
||||
/// recently used first past `CACHE_BYTES`: kept forever, every sound heard in
|
||||
/// a session stayed (up to 1.4 GiB of samples, villages playing hundreds).
|
||||
@@ -177,7 +179,7 @@ impl SoundCache {
|
||||
fn size(samples: &Samples) -> usize {
|
||||
samples
|
||||
.as_ref()
|
||||
.map_or(0, |(s, _)| s.len() * std::mem::size_of::<f32>())
|
||||
.map_or(0, |(s, _, _)| s.len() * std::mem::size_of::<f32>())
|
||||
}
|
||||
}
|
||||
/// A static sound to decode for a channel waiting on it.
|
||||
@@ -260,7 +262,7 @@ pub struct Audio {
|
||||
#[cfg(feature = "audio")]
|
||||
device_ticks: u32,
|
||||
}
|
||||
fn decode(path: &Path) -> Option<(Vec<f32>, u32)> {
|
||||
fn decode(path: &Path) -> Option<(Vec<f32>, u32, u16)> {
|
||||
use symphonia::core::{
|
||||
audio::SampleBuffer, codecs::DecoderOptions, formats::FormatOptions, io::MediaSourceStream,
|
||||
meta::MetadataOptions, probe::Hint,
|
||||
@@ -284,7 +286,8 @@ fn decode(path: &Path) -> Option<(Vec<f32>, u32)> {
|
||||
let mut decoder = symphonia::default::get_codecs()
|
||||
.make(&track.codec_params, &DecoderOptions::default())
|
||||
.ok()?;
|
||||
let mut mono = Vec::new();
|
||||
let mut samples = Vec::new();
|
||||
let mut channels = 0u16;
|
||||
while let Ok(packet) = format.next_packet() {
|
||||
if packet.track_id() != id {
|
||||
continue;
|
||||
@@ -293,14 +296,34 @@ fn decode(path: &Path) -> Option<(Vec<f32>, u32)> {
|
||||
continue;
|
||||
};
|
||||
let spec = *buffer.spec();
|
||||
let channels = spec.channels.count().max(1);
|
||||
let mut samples = SampleBuffer::<f32>::new(buffer.capacity() as u64, spec);
|
||||
samples.copy_interleaved_ref(buffer);
|
||||
for frame in samples.samples().chunks(channels) {
|
||||
mono.push(frame.iter().sum::<f32>() / channels as f32);
|
||||
let frame_channels = spec.channels.count().max(1);
|
||||
let mut decoded = SampleBuffer::<f32>::new(buffer.capacity() as u64, spec);
|
||||
decoded.copy_interleaved_ref(buffer);
|
||||
if channels == 0 {
|
||||
channels = frame_channels.min(2) as u16;
|
||||
}
|
||||
if frame_channels <= 2 {
|
||||
samples.extend_from_slice(decoded.samples());
|
||||
} else {
|
||||
// OpenAL plays multi-channel buffers direct; a stereo output hears the odd/even split.
|
||||
for frame in decoded.samples().chunks(frame_channels) {
|
||||
let (mut left, mut right) = (0.0, 0.0);
|
||||
let (mut lefts, mut rights) = (0.0f32, 0.0f32);
|
||||
for (i, sample) in frame.iter().enumerate() {
|
||||
if i % 2 == 0 {
|
||||
left += sample;
|
||||
lefts += 1.0;
|
||||
} else {
|
||||
right += sample;
|
||||
rights += 1.0;
|
||||
}
|
||||
}
|
||||
samples.push(left / lefts.max(1.0));
|
||||
samples.push(right / rights.max(1.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some((mono, rate))
|
||||
(channels != 0).then_some((samples, rate, channels))
|
||||
}
|
||||
/// A sound file without its variant number (`mob/creeper/say2.ogg` is `mob/creeper/say`): each
|
||||
/// player's game picks its own variant of the same event.
|
||||
@@ -427,7 +450,7 @@ impl Backend for MixerBackend {
|
||||
// A streamed sound loops by restarting its stream.
|
||||
let mixer = self.mixer.clone();
|
||||
std::thread::spawn(move || {
|
||||
let samples = decode(&file).map(|(s, r)| (Arc::new(s), r));
|
||||
let samples = decode(&file).map(|(s, r, c)| (Arc::new(s), r, c));
|
||||
attach(&mixer, channel, samples, looping);
|
||||
});
|
||||
return;
|
||||
@@ -448,7 +471,7 @@ impl Backend for MixerBackend {
|
||||
// Queued twice before the first decode finished.
|
||||
let cached = cache.lock().ok().and_then(|mut c| c.get(&job.key));
|
||||
let samples = cached.unwrap_or_else(|| {
|
||||
let samples = decode(&job.file).map(|(s, r)| (Arc::new(s), r));
|
||||
let samples = decode(&job.file).map(|(s, r, c)| (Arc::new(s), r, c));
|
||||
if let Ok(mut c) = cache.lock() {
|
||||
c.insert(job.key, samples.clone());
|
||||
}
|
||||
@@ -584,9 +607,13 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
let mut voices = std::mem::take(&mut mixer.voices);
|
||||
voices.clear();
|
||||
for (id, c) in &mixer.channels {
|
||||
let Some((_, source_rate)) = &c.samples else {
|
||||
let Some((_, source_rate, source_channels)) = &c.samples else {
|
||||
continue;
|
||||
};
|
||||
// OpenAL plays a multi-channel buffer direct: no panning and no distance attenuation
|
||||
// (`CalcVoiceParams`' non-spatialized path, vanilla's HRTF-off stereo context). Vanilla's
|
||||
// music and records are stereo. A mono buffer is the positional one.
|
||||
let direct = *source_channels > 1;
|
||||
if c.state != State::Playing {
|
||||
continue;
|
||||
}
|
||||
@@ -611,24 +638,33 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
let d = if c.relative { at } else { away(listener) };
|
||||
let distance = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt() as f32;
|
||||
let gain = if camera.is_some() {
|
||||
c.gain.clamp(0.0, 1.0) * distance_gain(c.linear, distance)
|
||||
c.gain.clamp(0.0, 1.0)
|
||||
* if direct {
|
||||
1.0
|
||||
} else {
|
||||
distance_gain(c.linear, distance)
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
// Relative sources are already in OpenAL's listener space (-Z ahead).
|
||||
let [mut left, mut right] = if c.relative {
|
||||
stereo_gains(d[0] as f32, d[1] as f32, -d[2] as f32)
|
||||
let [left, right] = if direct {
|
||||
[gain, gain]
|
||||
} else {
|
||||
stereo_gains(dot(d, right), dot(d, up), dot(d, forward))
|
||||
// Relative sources are already in OpenAL's listener space (-Z ahead).
|
||||
let [mut left, mut right] = if c.relative {
|
||||
stereo_gains(d[0] as f32, d[1] as f32, -d[2] as f32)
|
||||
} else {
|
||||
stereo_gains(dot(d, right), dot(d, up), dot(d, forward))
|
||||
};
|
||||
if shared {
|
||||
// Several players on the speakers: half the stereo width, as left and right are
|
||||
// nobody's in particular.
|
||||
let middle = (left + right) / 2.0;
|
||||
left = middle + (left - middle) / 2.0;
|
||||
right = middle + (right - middle) / 2.0;
|
||||
}
|
||||
[left * gain, right * gain]
|
||||
};
|
||||
if shared {
|
||||
// Several players on the speakers: half the stereo width, as left and right are
|
||||
// nobody's in particular.
|
||||
let middle = (left + right) / 2.0;
|
||||
left = middle + (left - middle) / 2.0;
|
||||
right = middle + (right - middle) / 2.0;
|
||||
}
|
||||
let [left, right] = [left, right].map(|pan| gain * pan);
|
||||
let step = f64::from(*source_rate) / f64::from(rate) * f64::from(c.pitch.max(0.0));
|
||||
voices.push((*id, [left, right, gain], step));
|
||||
}
|
||||
@@ -636,13 +672,15 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
let Some(c) = mixer.channels.get_mut(&id) else {
|
||||
continue;
|
||||
};
|
||||
let Some((samples, _)) = c.samples.clone() else {
|
||||
let Some((samples, _, source_channels)) = c.samples.clone() else {
|
||||
continue;
|
||||
};
|
||||
let source_channels = source_channels as usize;
|
||||
let frames = samples.len() / source_channels;
|
||||
for frame in data.chunks_mut(channels) {
|
||||
let mut i = c.position as usize;
|
||||
if i + 1 >= samples.len() {
|
||||
if c.looping && samples.len() > 1 {
|
||||
if i + 1 >= frames {
|
||||
if c.looping && frames > 1 {
|
||||
c.position = 0.0;
|
||||
i = 0;
|
||||
} else {
|
||||
@@ -651,13 +689,24 @@ fn mix(mixer: &mut Mixer, data: &mut [f32], channels: usize, rate: u32) {
|
||||
}
|
||||
}
|
||||
let t = (c.position - i as f64) as f32;
|
||||
let s = samples[i] * (1.0 - t) + samples[i + 1] * t;
|
||||
if channels >= 2 {
|
||||
frame[0] += s * left;
|
||||
frame[1] += s * right;
|
||||
if source_channels > 1 {
|
||||
let l = samples[i * 2] * (1.0 - t) + samples[(i + 1) * 2] * t;
|
||||
let r = samples[i * 2 + 1] * (1.0 - t) + samples[(i + 1) * 2 + 1] * t;
|
||||
if channels >= 2 {
|
||||
frame[0] += l * left;
|
||||
frame[1] += r * right;
|
||||
} else {
|
||||
frame[0] += (l + r) * 0.5 * gain;
|
||||
}
|
||||
} else {
|
||||
// A mono output has no panning: the source's own gain.
|
||||
frame[0] += s * gain;
|
||||
let s = samples[i] * (1.0 - t) + samples[i + 1] * t;
|
||||
if channels >= 2 {
|
||||
frame[0] += s * left;
|
||||
frame[1] += s * right;
|
||||
} else {
|
||||
// A mono output has no panning: the source's own gain.
|
||||
frame[0] += s * gain;
|
||||
}
|
||||
}
|
||||
c.position += step;
|
||||
}
|
||||
@@ -807,27 +856,50 @@ fn cards_changed(seen: Option<&str>, cards: Option<&str>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// The buffer size to request for `open_device`. On Linux the ALSA backend's
|
||||
/// `BufferSize::Default` means a ~100 ms buffer, so sound effects come out
|
||||
/// about 100 ms late: request a short fixed buffer (1024 frames) by default.
|
||||
/// An environment override `HORIZON_AUDIO_BUFFER` can specify `default` or a
|
||||
/// custom frame count (e.g. 2048) when the default short buffer underruns
|
||||
/// (e.g. PipeWire min-quantum >= 1024).
|
||||
#[cfg(feature = "audio")]
|
||||
fn target_buffer_size(
|
||||
range: (u32, u32),
|
||||
var: impl Fn(&str) -> Option<std::ffi::OsString>,
|
||||
) -> Option<cpal::BufferSize> {
|
||||
if let Some(val) = var("HORIZON_AUDIO_BUFFER").and_then(|v| v.into_string().ok()) {
|
||||
if val == "default" {
|
||||
return Some(cpal::BufferSize::Default);
|
||||
}
|
||||
if let Ok(frames) = val.parse::<u32>() {
|
||||
return Some(cpal::BufferSize::Fixed(frames.clamp(range.0, range.1)));
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Some(cpal::BufferSize::Fixed(1024u32.clamp(range.0, range.1)))
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The cpal `StreamConfig` to open an output stream with. On Linux the ALSA
|
||||
/// backend's `BufferSize::Default` means a ~100 ms buffer, so sound effects
|
||||
/// come out about 100 ms late: request a short fixed buffer there. Other
|
||||
/// platforms keep cpal's default behaviour.
|
||||
#[cfg(feature = "audio")]
|
||||
fn audio_stream_config(config: &cpal::SupportedStreamConfig) -> cpal::StreamConfig {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let range = match config.buffer_size() {
|
||||
cpal::SupportedBufferSize::Range { min, max } => (*min, *max),
|
||||
cpal::SupportedBufferSize::Unknown => (256, 4096),
|
||||
};
|
||||
let target = 1024u32.clamp(range.0, range.1);
|
||||
let mut sc = config.config();
|
||||
sc.buffer_size = cpal::BufferSize::Fixed(target);
|
||||
sc
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
config.config()
|
||||
let mut sc = config.config();
|
||||
let range = match config.buffer_size() {
|
||||
cpal::SupportedBufferSize::Range { min, max } => (*min, *max),
|
||||
cpal::SupportedBufferSize::Unknown => (256, 4096),
|
||||
};
|
||||
if let Some(bs) = target_buffer_size(range, |name| std::env::var_os(name)) {
|
||||
sc.buffer_size = bs;
|
||||
}
|
||||
sc
|
||||
}
|
||||
|
||||
#[cfg(feature = "audio")]
|
||||
@@ -1687,7 +1759,7 @@ mod tests {
|
||||
|
||||
fn channel(samples: Vec<f32>, linear: Option<f32>, at: [f32; 3], looping: bool) -> Channel {
|
||||
Channel {
|
||||
samples: Some((Arc::new(samples), 48_000)),
|
||||
samples: Some((Arc::new(samples), 48_000, 1)),
|
||||
position: 0.0,
|
||||
state: State::Playing,
|
||||
gain: 1.0,
|
||||
@@ -1702,11 +1774,23 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A channel whose buffer is interleaved stereo.
|
||||
fn stereo_channel(
|
||||
samples: Vec<f32>,
|
||||
linear: Option<f32>,
|
||||
at: [f32; 3],
|
||||
looping: bool,
|
||||
) -> Channel {
|
||||
let mut c = channel(Vec::new(), linear, at, looping);
|
||||
c.samples = Some((Arc::new(samples), 48_000, 2));
|
||||
c
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoded_sounds_stay_within_the_budget_least_recently_used_out() {
|
||||
let mut cache = SoundCache::default();
|
||||
let quarter = CACHE_BYTES / 4 / std::mem::size_of::<f32>();
|
||||
let sound = || Some((Arc::new(vec![0.0f32; quarter]), 44100));
|
||||
let sound = || Some((Arc::new(vec![0.0f32; quarter]), 44100, 1));
|
||||
for key in ["a", "b", "c", "d"] {
|
||||
cache.insert(key.into(), sound());
|
||||
}
|
||||
@@ -1744,6 +1828,42 @@ mod tests {
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
#[cfg(feature = "audio")]
|
||||
fn buffer_size_override_and_defaults() {
|
||||
let env = |pairs: &'static [(&'static str, &'static str)]| {
|
||||
move |name: &str| {
|
||||
pairs
|
||||
.iter()
|
||||
.find(|(k, _)| *k == name)
|
||||
.map(|(_, v)| std::ffi::OsString::from(*v))
|
||||
}
|
||||
};
|
||||
let range = (256, 4096);
|
||||
assert_eq!(
|
||||
target_buffer_size(range, env(&[("HORIZON_AUDIO_BUFFER", "default")])),
|
||||
Some(cpal::BufferSize::Default)
|
||||
);
|
||||
assert_eq!(
|
||||
target_buffer_size(range, env(&[("HORIZON_AUDIO_BUFFER", "2048")])),
|
||||
Some(cpal::BufferSize::Fixed(2048))
|
||||
);
|
||||
assert_eq!(
|
||||
target_buffer_size(range, env(&[("HORIZON_AUDIO_BUFFER", "100")])),
|
||||
Some(cpal::BufferSize::Fixed(256))
|
||||
);
|
||||
assert_eq!(
|
||||
target_buffer_size(range, env(&[("HORIZON_AUDIO_BUFFER", "10000")])),
|
||||
Some(cpal::BufferSize::Fixed(4096))
|
||||
);
|
||||
#[cfg(target_os = "linux")]
|
||||
assert_eq!(
|
||||
target_buffer_size(range, env(&[])),
|
||||
Some(cpal::BufferSize::Fixed(1024))
|
||||
);
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
assert_eq!(target_buffer_size(range, env(&[])), None);
|
||||
}
|
||||
#[test]
|
||||
fn linear_attenuation_reaches_zero_at_the_max_distance() {
|
||||
assert_eq!(distance_gain(None, 100.0), 1.0);
|
||||
assert_eq!(distance_gain(Some(16.0), 0.0), 1.0);
|
||||
@@ -1833,6 +1953,32 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stereo_sounds_play_direct_without_panning_or_distance() {
|
||||
// OpenAL Soft plays a multi-channel buffer direct (its non-spatialized path), so vanilla's
|
||||
// stereo music and records keep their own left/right mix at full volume whichever way the
|
||||
// camera faces and however far it is. Only mono buffers are panned and attenuated.
|
||||
let wave = [0.1, -0.2, 0.3, -0.4, 0.5, -0.6]; // three stereo frames
|
||||
let expected = [wave[0], wave[1], wave[2], wave[3]];
|
||||
for (yaw, at, linear, listener) in [
|
||||
(0.0, [100.0, 0.0, 0.0], Some(16.0), [0.0; 3]),
|
||||
(180.0, [0.0, 0.0, -100.0], Some(16.0), [0.0; 3]),
|
||||
(90.0, [2.0, 1.0, -3.0], Some(16.0), [5.0, 60.0, 5.0]),
|
||||
(0.0, [0.0, 0.0, 0.0], None, [0.0; 3]),
|
||||
] {
|
||||
let mut m = Mixer {
|
||||
listener,
|
||||
listener_yaw: yaw,
|
||||
..Mixer::default()
|
||||
};
|
||||
m.channels
|
||||
.insert(1, stereo_channel(wave.to_vec(), linear, at, false));
|
||||
let mut data = [0.0; 4];
|
||||
mix(&mut m, &mut data, 2, 48_000);
|
||||
assert_eq!(data, expected, "yaw {yaw}, source {at:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panning_follows_the_camera_rotation() {
|
||||
let gains = |yaw: f32, pitch: f32, at: [f32; 3]| {
|
||||
|
||||
@@ -3407,7 +3407,9 @@ impl BeamPart {
|
||||
(at(b, self.end), [s.u0, v1]),
|
||||
] {
|
||||
out.push(Vertex {
|
||||
normal: [0; 4],
|
||||
// The beacon beam render types keep the default back-face culling: seen
|
||||
// from inside a beam, its walls must not show.
|
||||
normal: CULL_BACK,
|
||||
pos: p.to_array(),
|
||||
uv,
|
||||
layer: 6,
|
||||
@@ -4277,6 +4279,23 @@ mod tests {
|
||||
/// submits the unit cube's two Y faces. Both are wound outward, so of each
|
||||
/// face only the side looking at the camera draws: the portal shows one
|
||||
/// surface from above and one from below, never both at once.
|
||||
/// `RenderPipelines.BEACON_BEAM_OPAQUE` / `_TRANSLUCENT` keep the default
|
||||
/// back-face culling, so a camera inside a beam sees no wall of it.
|
||||
#[test]
|
||||
fn beacon_beam_faces_cull_their_backs() {
|
||||
let rect = Rect {
|
||||
u0: 0.0,
|
||||
v0: 0.0,
|
||||
u1: 1.0,
|
||||
v1: 1.0,
|
||||
w: 16,
|
||||
h: 16,
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
beacon_beam(Vec3::ZERO, &[(0xFFFFFF, 4)], 0.0, 1.0, rect, &mut out);
|
||||
assert_eq!(out.len(), 32, "solid and glow, four quads each");
|
||||
assert!(out.iter().all(|v| v.normal == CULL_BACK));
|
||||
}
|
||||
#[test]
|
||||
fn the_end_portal_cube_is_one_sided() {
|
||||
let mut out = Vec::new();
|
||||
|
||||
@@ -572,7 +572,7 @@ impl ControllerUi {
|
||||
&cols,
|
||||
&lines,
|
||||
self.scroll.amount,
|
||||
Page::name,
|
||||
|page| lang::get(page.name()),
|
||||
|batch, setting: Setting, y| {
|
||||
let item = setting.item;
|
||||
let [rx, ry, rw, rh] = cols.row(y);
|
||||
|
||||
@@ -896,6 +896,13 @@ pub fn worldgen_overrides(
|
||||
let mut warnings = Vec::new();
|
||||
for pack in packs {
|
||||
for file in pack.list_kinds(&overrides::PACK_KINDS) {
|
||||
if let Some(id) = overrides::template_id(&file) {
|
||||
match pack.read(&file) {
|
||||
Some(nbt) => out.set_template(&id, nbt),
|
||||
None => warnings.push(format!("{}: {file}: unreadable", pack.id)),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let read = || String::from_utf8(pack.read(&file)?).ok();
|
||||
match out.add_file(&pack.id, &file, read) {
|
||||
PackFile::Read | PackFile::Other => {}
|
||||
@@ -956,6 +963,7 @@ mod tests {
|
||||
for pack in ["a", "b", "off"] {
|
||||
write(pack, "pack.mcmeta", "{}");
|
||||
write(pack, "data/minecraft/worldgen/noise/cave.json", pack);
|
||||
write(pack, "data/p/structure/room/a.nbt", pack);
|
||||
write(
|
||||
pack,
|
||||
"data/minecraft/tags/worldgen/biome/is_hill.json",
|
||||
@@ -992,6 +1000,8 @@ mod tests {
|
||||
o.entries("noises").collect::<Vec<_>>(),
|
||||
[("minecraft:cave", "a")]
|
||||
);
|
||||
assert_eq!(o.templates().len(), 1);
|
||||
assert_eq!(&*o.templates()["p:room/a"], b"a");
|
||||
assert_eq!(
|
||||
o.entries("biomes").collect::<Vec<_>>(),
|
||||
[("minecraft:plains", "{}")]
|
||||
|
||||
@@ -98,14 +98,42 @@ pub struct Source {
|
||||
/// The [`MAX`] sources nearest `camera` within reach, as the shaders read them: camera-relative
|
||||
/// position and level. Overwrites `out`.
|
||||
pub fn select(sources: &[Source], camera: DVec3, out: &mut Vec<[f32; 4]>) {
|
||||
select_iter(sources.iter().copied(), camera, out);
|
||||
}
|
||||
|
||||
/// Streaming selection with the same stable distance order as [`select`]. Only the best
|
||||
/// [`MAX`] sources are kept, and each source's distance is evaluated once.
|
||||
pub fn select_iter(
|
||||
sources: impl IntoIterator<Item = Source>,
|
||||
camera: DVec3,
|
||||
out: &mut Vec<[f32; 4]>,
|
||||
) {
|
||||
out.clear();
|
||||
let key = |s: &Source| s.at.distance_squared(camera);
|
||||
let mut near: Vec<&Source> = sources
|
||||
.iter()
|
||||
.filter(|s| s.level > 0 && key(s) <= REACH * REACH)
|
||||
.collect();
|
||||
near.sort_by(|a, b| key(a).total_cmp(&key(b)));
|
||||
out.extend(near.into_iter().take(MAX).map(|s| {
|
||||
let mut near = [(
|
||||
0.0_f64,
|
||||
Source {
|
||||
at: DVec3::ZERO,
|
||||
level: 0,
|
||||
},
|
||||
); MAX];
|
||||
let mut len = 0;
|
||||
for source in sources {
|
||||
let distance = source.at.distance_squared(camera);
|
||||
// The original <= test also rejects NaN and infinite distances.
|
||||
if source.level == 0 || !(0.0..=REACH * REACH).contains(&distance) {
|
||||
continue;
|
||||
}
|
||||
let at = near[..len].partition_point(|(d, _)| d.total_cmp(&distance).is_le());
|
||||
if at == MAX {
|
||||
continue;
|
||||
}
|
||||
// Insert after equal distances: the first MAX sources in input order win ties.
|
||||
let end = len.min(MAX - 1);
|
||||
near.copy_within(at..end, at + 1);
|
||||
near[at] = (distance, source);
|
||||
len = (len + 1).min(MAX);
|
||||
}
|
||||
out.extend(near[..len].iter().map(|(_, s)| {
|
||||
let p = (s.at - camera).as_vec3();
|
||||
[p.x, p.y, p.z, f32::from(s.level.min(15))]
|
||||
}));
|
||||
@@ -179,6 +207,104 @@ mod tests {
|
||||
assert!(out.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn streaming_selection_matches_stable_full_sort() {
|
||||
let camera = DVec3::new(30_000_000.0, -64.0, -30_000_000.0);
|
||||
let mut seed = 0x8469_d28b_314a_723fu64;
|
||||
let mut sources = Vec::new();
|
||||
for i in 0..2048 {
|
||||
let offset = std::array::from_fn::<_, 3, _>(|_| {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 7;
|
||||
seed ^= seed << 17;
|
||||
(seed % 241) as f64 - 120.0
|
||||
});
|
||||
sources.push(Source {
|
||||
at: camera + DVec3::from(offset),
|
||||
level: (i % 22) as u8,
|
||||
});
|
||||
}
|
||||
sources.extend([
|
||||
Source {
|
||||
at: camera + DVec3::X * 96.0,
|
||||
level: 14,
|
||||
},
|
||||
Source {
|
||||
at: camera - DVec3::X * 96.0,
|
||||
level: 15,
|
||||
},
|
||||
Source {
|
||||
at: camera + DVec3::X * 96.0001,
|
||||
level: 15,
|
||||
},
|
||||
Source {
|
||||
at: DVec3::splat(f64::NAN),
|
||||
level: 15,
|
||||
},
|
||||
Source {
|
||||
at: DVec3::splat(f64::INFINITY),
|
||||
level: 15,
|
||||
},
|
||||
]);
|
||||
let reference = |input: &[Source], camera: DVec3| {
|
||||
let mut selected: Vec<_> = input
|
||||
.iter()
|
||||
.filter(|s| s.level > 0 && s.at.distance_squared(camera) <= 96.0 * 96.0)
|
||||
.collect();
|
||||
selected.sort_by(|a, b| {
|
||||
a.at.distance_squared(camera)
|
||||
.total_cmp(&b.at.distance_squared(camera))
|
||||
});
|
||||
selected
|
||||
.into_iter()
|
||||
.take(16)
|
||||
.map(|s| {
|
||||
let p = (s.at - camera).as_vec3();
|
||||
[p.x, p.y, p.z, f32::from(s.level.min(15))]
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let mut out = vec![[999.0; 4]; 20];
|
||||
for input in [sources.clone(), sources.into_iter().rev().collect()] {
|
||||
for n in [0, 1, 15, 16, 17, 40, input.len()] {
|
||||
select_iter(input[..n].iter().copied(), camera, &mut out);
|
||||
assert_eq!(out, reference(&input[..n], camera));
|
||||
}
|
||||
select_iter(input.iter().copied(), DVec3::splat(f64::NAN), &mut out);
|
||||
assert!(out.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_distances_keep_first_sources_and_reach_is_inclusive() {
|
||||
// Every source ties: neither a different position nor a different light level
|
||||
// may win the sixteenth slot just because it was inserted later.
|
||||
let sources: Vec<_> = (0..40)
|
||||
.map(|i| Source {
|
||||
at: if i % 2 == 0 {
|
||||
DVec3::X * 96.0
|
||||
} else {
|
||||
-DVec3::X * 96.0
|
||||
},
|
||||
level: (i % 15 + 1) as u8,
|
||||
})
|
||||
.collect();
|
||||
let mut out = Vec::new();
|
||||
select_iter(sources, DVec3::ZERO, &mut out);
|
||||
assert_eq!(out.len(), 16);
|
||||
for (i, light) in out.iter().enumerate() {
|
||||
assert_eq!(
|
||||
*light,
|
||||
[
|
||||
if i % 2 == 0 { 96.0 } else { -96.0 },
|
||||
0.0,
|
||||
0.0,
|
||||
(i % 15 + 1) as f32
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_falls_off_a_level_per_block() {
|
||||
let torch = [[0.0, 0.0, 0.0, 14.0]];
|
||||
|
||||
@@ -413,6 +413,13 @@ pub fn clear_distance_fog(fog: &mut FogData, input: &FogInputs) {
|
||||
(fog.environmental_start, fog.environmental_end) = (1e9, 2e9);
|
||||
}
|
||||
}
|
||||
/// Whether the frame draws no sky, only the clear to the fog colour: `LevelRenderer.addSkyPass`
|
||||
/// leaves the sky out in lava and powder snow and while blindness or darkness is on (`Camera`'s
|
||||
/// `doesMobEffectBlockSky`), and `GameRenderer.renderLevel` gives `LevelRenderer.render`
|
||||
/// `shouldRenderSky` false while a boss bar makes world fog (the Ender Dragon's).
|
||||
pub fn sky_pass_skipped(fog_type: FogType, effect_blocks_sky: bool, boss_world_fog: bool) -> bool {
|
||||
matches!(fog_type, FogType::Lava | FogType::PowderSnow) || effect_blocks_sky || boss_world_fog
|
||||
}
|
||||
/// `AtmosphericFogEnvironment.updateRainFogState`.
|
||||
pub fn update_rain_fog(
|
||||
state: &mut FogState,
|
||||
@@ -480,6 +487,20 @@ mod tests {
|
||||
assert_eq!(fog(surface - 0.01, -90.0), FogType::Atmospheric);
|
||||
}
|
||||
#[test]
|
||||
fn the_dragon_fights_world_fog_leaves_out_the_sky() {
|
||||
use FogType::*;
|
||||
// GameRenderer.renderLevel: no sky pass under a createWorldFog boss bar, in clear air
|
||||
// as in water.
|
||||
assert!(sky_pass_skipped(Atmospheric, false, true));
|
||||
assert!(sky_pass_skipped(Water, false, true));
|
||||
// LevelRenderer.addSkyPass's own conditions.
|
||||
assert!(sky_pass_skipped(Lava, false, false));
|
||||
assert!(sky_pass_skipped(PowderSnow, false, false));
|
||||
assert!(sky_pass_skipped(Atmospheric, true, false));
|
||||
assert!(!sky_pass_skipped(Atmospheric, false, false));
|
||||
assert!(!sky_pass_skipped(Water, false, false));
|
||||
}
|
||||
#[test]
|
||||
fn boss_world_fog_closes_in() {
|
||||
// AtmosphericFogEnvironment.setupFog under a createWorldFog boss bar.
|
||||
let sky = Sky {
|
||||
|
||||
@@ -874,7 +874,8 @@ pub struct Frame<'a> {
|
||||
pub cave_culling: bool,
|
||||
/// Title screen: only the dedicated panorama cube map and the HUD.
|
||||
pub title: bool,
|
||||
/// Lava, powder snow, blindness or darkness blocks the sky pass.
|
||||
/// Lava, powder snow, blindness, darkness or a boss bar's world fog leaves out the sky pass
|
||||
/// (`fog::sky_pass_skipped`).
|
||||
pub sky_blocked: bool,
|
||||
/// Dimensions without a skybox (the Nether) clear to the fog colour.
|
||||
pub skybox: bool,
|
||||
@@ -1385,6 +1386,8 @@ pub struct Renderer {
|
||||
hdr_lighting_setting: bool,
|
||||
/// The shader pack's post-processing, and the dimension (legacy id) it was loaded for.
|
||||
pack_post: Option<(i32, Option<pack_post::PackPost>)>,
|
||||
/// A `shaders/` pack whose pipelines build on a thread of their own (`load_shader_pack`).
|
||||
pack_loading: Option<PackLoading>,
|
||||
/// The post passes of one of Horizon's own shader packs; never beside `pack_post`'s.
|
||||
native: Option<native_pack::NativePack>,
|
||||
/// Time the shader pack's passes on the GPU (F3, or `HORIZON_SHADER_PACK_TIMING`).
|
||||
@@ -2115,10 +2118,22 @@ async fn open_with_retry(
|
||||
);
|
||||
match open_backend(backends, window, offscreen, true, layers, allow_cpu).await {
|
||||
Ok(opened) => {
|
||||
let info = opened.adapter.get_info();
|
||||
if is_broken_haswell_vulkan(&info) {
|
||||
log::warn!(
|
||||
"Horizon renderer: skipping {} on Vulkan because Mesa hasvk suffers from \
|
||||
vertex corruption on Haswell; falling back to next backend",
|
||||
info.name
|
||||
);
|
||||
return Err(format!(
|
||||
"Mesa hasvk on Haswell ({}) has broken vertex buffers under Vulkan; falling back",
|
||||
info.name
|
||||
));
|
||||
}
|
||||
log::warn!(
|
||||
"Horizon renderer: using {} although its driver reports no Vulkan \
|
||||
conformance version",
|
||||
opened.adapter.get_info().name
|
||||
info.name
|
||||
);
|
||||
Ok(opened)
|
||||
}
|
||||
@@ -2129,6 +2144,19 @@ async fn open_with_retry(
|
||||
Ok(opened) => Ok(opened),
|
||||
}
|
||||
}
|
||||
/// Intel Haswell (Gen 7.5) Mesa Vulkan driver (`hasvk`) reports 0.0.0.0 and suffers
|
||||
/// from severe vertex buffer corruption and polygon tearing (#9912), but its OpenGL driver
|
||||
/// (`crocus`) is fully conformant and stable. Ivy Bridge (`IVB`) keeps Vulkan (#4059).
|
||||
pub(crate) fn is_broken_haswell_vulkan(info: &wgpu::AdapterInfo) -> bool {
|
||||
info.backend == wgpu::Backend::Vulkan
|
||||
&& info.vendor == 0x8086
|
||||
&& (matches!(
|
||||
info.device,
|
||||
0x0400..=0x042f // Haswell Desktop / Mobile
|
||||
| 0x0a00..=0x0a2f // Haswell ULT (Ultrabook)
|
||||
| 0x0d00..=0x0d2f // Haswell CRW (Crystal Well)
|
||||
) || ["HSW", "Haswell"].iter().any(|tag| info.name.contains(tag)))
|
||||
}
|
||||
/// Opens a device on `backends` alone: its surface, adapter and device, or what
|
||||
/// stopped it. `allow_nonconformant` is `open_with_retry`'s second try; a
|
||||
/// software adapter is taken only when `allow_cpu`.
|
||||
@@ -4711,6 +4739,7 @@ impl Renderer {
|
||||
frame_format,
|
||||
hdr_marker: None,
|
||||
pack_post: None,
|
||||
pack_loading: None,
|
||||
native: None,
|
||||
pass_timing: pack_timing::PassTiming::logging(),
|
||||
own_timing: None,
|
||||
@@ -6462,7 +6491,17 @@ impl Renderer {
|
||||
world,
|
||||
)
|
||||
});
|
||||
let post = match post.transpose() {
|
||||
self.pack_loading = None;
|
||||
self.install_shader_pack(dimension, post.transpose())
|
||||
}
|
||||
/// Runs the pack `set_shader_pack` or `load_shader_pack` built (`None`: no pack), or none when
|
||||
/// it can't run, for the dimension `dimension`; the programs that run and their notes.
|
||||
fn install_shader_pack(
|
||||
&mut self,
|
||||
dimension: i32,
|
||||
post: std::result::Result<Option<pack_post::PackPost>, pack_post::Refused>,
|
||||
) -> std::result::Result<Option<(Vec<String>, Vec<String>)>, pack_post::Refused> {
|
||||
let post = match post {
|
||||
Ok(post) => post,
|
||||
Err(e) => {
|
||||
self.pack_post = Some((dimension, None));
|
||||
@@ -6481,8 +6520,138 @@ impl Renderer {
|
||||
}
|
||||
Ok(report)
|
||||
}
|
||||
/// As `set_shader_pack` with a pack, but its pipelines build on a thread of their own while
|
||||
/// the game keeps drawing without one: the driver compiles a big pack's programs for seconds.
|
||||
/// `poll_shader_pack` runs it once they are built. `label` and `started` come back with it.
|
||||
pub fn load_shader_pack(
|
||||
&mut self,
|
||||
dimension: i32,
|
||||
pack: horizon_shaders::Post,
|
||||
block_ids: Vec<i32>,
|
||||
label: String,
|
||||
started: std::time::Instant,
|
||||
) {
|
||||
let device = self.device.clone();
|
||||
let queue = self.queue.clone();
|
||||
let adapter = self.adapter.clone();
|
||||
let format = self.frame_format;
|
||||
let atlas = self.pack_atlas.as_ref().map(|a| (a.view.clone(), a.size()));
|
||||
let sprite_rects = self
|
||||
.pack_atlas
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |a| a.rects_table());
|
||||
let material_maps = self
|
||||
.pack_material_atlases
|
||||
.as_ref()
|
||||
.map(|a| [a[0].view.clone(), a[1].view.clone()]);
|
||||
let lightmap = self.lightmap_view.clone();
|
||||
let gui_size = self
|
||||
.hud
|
||||
.atlas_texture
|
||||
.as_ref()
|
||||
.map_or([1, 1], |t| [t.width(), t.height()]);
|
||||
let celestial = self
|
||||
.hud
|
||||
.atlas_texture
|
||||
.as_ref()
|
||||
.map(|t| t.create_view(&Default::default()));
|
||||
let flat = self.pack_terrain_flat;
|
||||
let table = (self.flat.as_ref())
|
||||
.filter(|_| flat)
|
||||
.map(|f| f.draw_layout.clone());
|
||||
let terrain = self.pack_atlas.is_some();
|
||||
let clouds = self.cloud_bindings.clone();
|
||||
let build = move || {
|
||||
let instance = wgpu::vertex_attr_array![5=>Float32x4];
|
||||
let vertex = wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<TerrainVertex>() as u64,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &TERRAIN_ATTRIBUTES,
|
||||
};
|
||||
let instances = wgpu::VertexBufferLayout {
|
||||
array_stride: 16,
|
||||
step_mode: wgpu::VertexStepMode::Instance,
|
||||
attributes: &instance,
|
||||
};
|
||||
let buffers = [Some(vertex), Some(instances)];
|
||||
let world = pack_post::World {
|
||||
atlas,
|
||||
sprite_rects: &sprite_rects,
|
||||
material_maps,
|
||||
lightmap,
|
||||
gui_size,
|
||||
terrain: terrain.then(|| pack_post::Geometry {
|
||||
buffers: if flat { &buffers[..1] } else { &buffers },
|
||||
table: table.as_ref(),
|
||||
depth_write: true,
|
||||
atlas: None,
|
||||
material_maps: true,
|
||||
ids: None,
|
||||
cull: true,
|
||||
}),
|
||||
block_ids: &block_ids,
|
||||
celestial,
|
||||
clouds: Some(&clouds),
|
||||
weather: true,
|
||||
hand: true,
|
||||
};
|
||||
pack_post::PackPost::new(&device, &queue, &adapter, pack, format, world)
|
||||
};
|
||||
// A pack is not the game's code: a panic building it turns the pack off (#3525).
|
||||
let thread = std::thread::Builder::new()
|
||||
.name("Shader pack".into())
|
||||
.spawn(move || crate::crash::contain(build));
|
||||
match thread {
|
||||
Ok(thread) => {
|
||||
// The last pack doesn't draw this dimension meanwhile.
|
||||
self.pack_post = None;
|
||||
self.pack_loading = Some(PackLoading {
|
||||
dimension,
|
||||
thread,
|
||||
label,
|
||||
started,
|
||||
});
|
||||
}
|
||||
Err(e) => log::error!("shader pack {label}: no thread to build it on: {e}"),
|
||||
}
|
||||
}
|
||||
/// The pack `load_shader_pack` builds, once its thread is done: it runs from now on, as
|
||||
/// `set_shader_pack` would run it. Its dimension, label, start and what `set_shader_pack`
|
||||
/// returns.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn poll_shader_pack(
|
||||
&mut self,
|
||||
) -> Option<(
|
||||
i32,
|
||||
String,
|
||||
std::time::Instant,
|
||||
std::result::Result<Option<(Vec<String>, Vec<String>)>, pack_post::Refused>,
|
||||
)> {
|
||||
if !self.pack_loading.as_ref()?.thread.is_finished() {
|
||||
return None;
|
||||
}
|
||||
let loading = self.pack_loading.take()?;
|
||||
let built = match loading.thread.join() {
|
||||
Ok(Ok(built)) => built,
|
||||
Ok(Err(panic)) => Err(pack_post::Refused {
|
||||
reason: format!("building it panicked: {panic}"),
|
||||
notes: Vec::new(),
|
||||
}),
|
||||
Err(_) => Err(pack_post::Refused {
|
||||
reason: "its thread panicked".into(),
|
||||
notes: Vec::new(),
|
||||
}),
|
||||
};
|
||||
let report = self.install_shader_pack(loading.dimension, built.map(Some));
|
||||
Some((loading.dimension, loading.label, loading.started, report))
|
||||
}
|
||||
/// Whether a `shaders/` pack is still building (`load_shader_pack`).
|
||||
pub fn shader_pack_loading(&self) -> bool {
|
||||
self.pack_loading.is_some()
|
||||
}
|
||||
/// Drops the running shader pack, of either kind.
|
||||
pub fn drop_shader_packs(&mut self) {
|
||||
self.pack_loading = None;
|
||||
self.pack_post = None;
|
||||
self.native = None;
|
||||
self.pack_atlas = None;
|
||||
@@ -6634,17 +6803,11 @@ impl Renderer {
|
||||
if let Some(atlas) = &mut atlas {
|
||||
atlas.separate_ao = self.separate_ao;
|
||||
}
|
||||
self.pack_material_atlases = atlas.as_ref().and_then(|_| {
|
||||
self.pack_material_atlases = atlas.as_ref().and_then(|colour| {
|
||||
let maps = self.pbr_maps.as_ref()?;
|
||||
let make = |kind: usize, encoder: &mut wgpu::CommandEncoder| {
|
||||
let arrays: Vec<_> = maps.iter().map(|pair| pair[kind].clone()).collect();
|
||||
pack_atlas::PackAtlas::material(
|
||||
&self.device,
|
||||
encoder,
|
||||
&arrays,
|
||||
self.sprite_split,
|
||||
layers,
|
||||
)
|
||||
colour.material_for(&self.device, encoder, &arrays, layers)
|
||||
};
|
||||
Some([make(0, &mut encoder)?, make(1, &mut encoder)?])
|
||||
});
|
||||
@@ -6715,7 +6878,8 @@ impl Renderer {
|
||||
|
||||
/// The dimension the shader pack was last set for.
|
||||
pub fn shader_pack_dimension(&self) -> Option<i32> {
|
||||
self.pack_post.as_ref().map(|p| p.0)
|
||||
let loading = self.pack_loading.as_ref().map(|l| l.dimension);
|
||||
loading.or_else(|| self.pack_post.as_ref().map(|p| p.0))
|
||||
}
|
||||
/// The Texture Filtering and Max Anisotropy options (`filtering`: 0 none, 1 RGSS, 2
|
||||
/// anisotropic; `bit`: `maxAnisotropyBit`). All take LevelRenderer's chunk layer sampler
|
||||
@@ -11526,6 +11690,21 @@ impl Frustum {
|
||||
.iter()
|
||||
.any(|plane| plane.truncate().dot(center) + plane.w < -radius)
|
||||
}
|
||||
/// Depth clip planes alone: X/Y projection jitter cannot change this test.
|
||||
/// Retaining side-of-screen geometry makes this safe before TAA selects a phase.
|
||||
pub fn intersects_depth(&self, min: Vec3, max: Vec3) -> bool {
|
||||
if !min.is_finite() || !max.is_finite() || !min.cmple(max).all() {
|
||||
return true;
|
||||
}
|
||||
!self.planes[4..].iter().any(|p| {
|
||||
let v = Vec3::new(
|
||||
if p.x >= 0.0 { max.x } else { min.x },
|
||||
if p.y >= 0.0 { max.y } else { min.y },
|
||||
if p.z >= 0.0 { max.z } else { min.z },
|
||||
);
|
||||
p.truncate().dot(v) + p.w < 0.0
|
||||
})
|
||||
}
|
||||
/// Whether the camera-relative box `min..max` is entirely inside. Then
|
||||
/// `intersects` accepts every box within it: each plane's distance only
|
||||
/// grows from this box's nearest corner to the inner box's farthest, and
|
||||
@@ -11732,6 +11911,47 @@ fn frustum(m: Mat4) -> [Vec4; 6] {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn depth_culling_keeps_jittered_edges_and_depth_crossing_models() {
|
||||
use super::*;
|
||||
for (yaw, pitch) in [(0.0, 0.0), (73.0, -35.0), (-142.0, 79.0)] {
|
||||
let pose = Mat4::from_translation(Vec3::new(0.03, -0.02, 0.01))
|
||||
* crate::camera::view_matrix(yaw, pitch);
|
||||
let matrix = world_projection(70.0, 16.0 / 9.0, 512.0) * pose;
|
||||
let view = Frustum {
|
||||
planes: frustum(matrix),
|
||||
};
|
||||
for size in [[1, 1], [320, 180], [1280, 720], [3840, 2160]] {
|
||||
for (x, y) in [(-1.0, 1.0), (1.0, -1.0)] {
|
||||
let jitter = Mat4::from_translation(Vec3::new(
|
||||
x / size[0] as f32,
|
||||
y / size[1] as f32,
|
||||
0.0,
|
||||
));
|
||||
let inverse = (jitter * matrix).inverse();
|
||||
for (x, y) in [(-1.0, -1.0), (1.0, 1.0), (-1.0, 1.0), (1.0, -1.0)] {
|
||||
for z in [0.001, 0.5, 0.999] {
|
||||
let p = inverse * Vec4::new(x, y, z, 1.0);
|
||||
let p = p.truncate() / p.w;
|
||||
assert!(view.intersects_depth(
|
||||
p - Vec3::splat(1.0e-4),
|
||||
p + Vec3::splat(1.0e-4)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let point = |p| pose.inverse().transform_point3(p);
|
||||
for z in [10.0, -600.0] {
|
||||
let p = point(Vec3::new(0.0, 0.0, z));
|
||||
assert!(!view.intersects_depth(p - Vec3::splat(0.1), p + Vec3::splat(0.1)));
|
||||
}
|
||||
// An oversized model straddling the eye must stay, even with its origin behind.
|
||||
let p = point(Vec3::ZERO);
|
||||
assert!(view.intersects_depth(p - Vec3::splat(10.0), p + Vec3::splat(10.0)));
|
||||
assert!(view.intersects_depth(Vec3::splat(f32::NAN), Vec3::ONE));
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn opaque_gizmo_lines_upload_first_for_the_depth_writing_pipeline() {
|
||||
let line = |argb: u32| [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, f32::from_bits(argb), 2.5];
|
||||
@@ -13225,6 +13445,36 @@ mod sprite_upload_tests {
|
||||
assert_eq!(adapter_requested_split(&info, None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broken_haswell_vulkan_detects_haswell_and_leaves_ivy_bridge() {
|
||||
let mut info = wgpu::AdapterInfo {
|
||||
name: "Intel(R) HD Graphics 4400 (HSW GT2)".into(),
|
||||
vendor: 0x8086,
|
||||
device: 0x0a16,
|
||||
device_type: wgpu::DeviceType::IntegratedGpu,
|
||||
device_pci_bus_id: String::new(),
|
||||
driver: "Mesa 26.2.5".into(),
|
||||
driver_info: "Mesa 26.2.5".into(),
|
||||
backend: wgpu::Backend::Vulkan,
|
||||
subgroup_min_size: 0,
|
||||
subgroup_max_size: 0,
|
||||
transient_saves_memory: None,
|
||||
limit_bucket: None,
|
||||
};
|
||||
assert!(is_broken_haswell_vulkan(&info));
|
||||
|
||||
// Ivy Bridge keeps Vulkan (#4059)
|
||||
info.name = "Intel(R) HD Graphics 4000 (IVB GT2)".into();
|
||||
info.device = 0x0166;
|
||||
assert!(!is_broken_haswell_vulkan(&info));
|
||||
|
||||
// Haswell on OpenGL is stable (crocus)
|
||||
info.name = "Intel(R) HD Graphics 4400 (HSW GT2)".into();
|
||||
info.device = 0x0a16;
|
||||
info.backend = wgpu::Backend::Gl;
|
||||
assert!(!is_broken_haswell_vulkan(&info));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mip_chain_allocates_exactly_its_size() {
|
||||
for size in [16, 64] {
|
||||
@@ -13739,3 +13989,15 @@ mod shadow_caster_tests {
|
||||
assert!(!kept(50.0, 0.0, 0.0, [-2.0, 3.0, 0.0]));
|
||||
}
|
||||
}
|
||||
|
||||
/// A `shaders/` pack building on its own thread (`Renderer::load_shader_pack`): the dimension it
|
||||
/// is for, the thread (a panic in it contained), and what the game named it with and when.
|
||||
struct PackLoading {
|
||||
dimension: i32,
|
||||
#[allow(clippy::type_complexity)]
|
||||
thread: std::thread::JoinHandle<
|
||||
std::result::Result<std::result::Result<pack_post::PackPost, pack_post::Refused>, String>,
|
||||
>,
|
||||
label: String,
|
||||
started: std::time::Instant,
|
||||
}
|
||||
@@ -220,7 +220,7 @@ impl Cull {
|
||||
/// write storage.
|
||||
pub fn supported(adapter: &wgpu::Adapter, device: &wgpu::Device) -> Option<DepthTest> {
|
||||
let depth_test = match adapter.get_info().backend {
|
||||
wgpu::Backend::Vulkan
|
||||
wgpu::Backend::Vulkan | wgpu::Backend::Gl
|
||||
if device
|
||||
.features()
|
||||
.contains(wgpu::Features::SHADER_EARLY_DEPTH_TEST) =>
|
||||
|
||||
@@ -512,26 +512,36 @@ impl NativeFrame {
|
||||
|
||||
/// Writes this frame's inputs, for a world of `size`.
|
||||
pub fn write(&mut self, queue: &wgpu::Queue, frame: &Frame, size: [u32; 2]) {
|
||||
let inputs = self.next_inputs(frame, size);
|
||||
queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(&inputs));
|
||||
}
|
||||
|
||||
/// This frame's inputs, moving the eased values and the frame count on.
|
||||
fn next_inputs(&mut self, frame: &Frame, size: [u32; 2]) -> FrameInputs {
|
||||
// Under `HORIZON_STILL` the world's ticks are the clock (`PackInputs::still_ticks`), and
|
||||
// eased values sit at their targets.
|
||||
let still = frame.pack.still_ticks;
|
||||
let seconds = still.map_or_else(
|
||||
|| self.start.elapsed().as_secs_f32() % 3600.0,
|
||||
|ticks| (ticks as f32 / 20.0) % 3600.0,
|
||||
);
|
||||
// A scripted clock (`PackInputs::frame_timing`) comes first, and eased values
|
||||
// move by its delta.
|
||||
let now = std::time::Instant::now();
|
||||
let since = match still {
|
||||
Some(_) => f32::INFINITY,
|
||||
None => self.wetness.1.map_or(0.0, |t| (now - t).as_secs_f32()),
|
||||
let timing = frame.pack.frame_timing;
|
||||
// Seconds, frame index, and the seconds eased values move by when not the
|
||||
// measured ones.
|
||||
let (seconds, index, step) = match (timing, frame.pack.still_ticks) {
|
||||
(Some(t), _) => (t.elapsed, t.counter, Some(t.delta)),
|
||||
(None, Some(ticks)) => (
|
||||
(ticks as f32 / 20.0) % 3600.0,
|
||||
ticks as u32,
|
||||
Some(f32::INFINITY),
|
||||
),
|
||||
(None, None) => (
|
||||
self.start.elapsed().as_secs_f32() % 3600.0,
|
||||
self.frames,
|
||||
None,
|
||||
),
|
||||
};
|
||||
let since = step.unwrap_or_else(|| self.wetness.1.map_or(0.0, |t| (now - t).as_secs_f32()));
|
||||
self.wetness = (wetness(self.wetness.0, frame.pack.rain, since), Some(now));
|
||||
let mut inputs = frame_inputs(
|
||||
frame,
|
||||
size,
|
||||
seconds,
|
||||
still.map_or(self.frames, |ticks| ticks as u32),
|
||||
self.shadow_camera.as_ref(),
|
||||
);
|
||||
let mut inputs = frame_inputs(frame, size, seconds, index, self.shadow_camera.as_ref());
|
||||
inputs.wetness = self.wetness.0;
|
||||
if !self.reflects {
|
||||
inputs.has_reflection = 0;
|
||||
@@ -542,8 +552,11 @@ impl NativeFrame {
|
||||
inputs.previous_view_projection = view_proj;
|
||||
// `write` skips title frames, so a gap means a pause or another world: the frame
|
||||
// before says nothing about this one, and the eased values start over.
|
||||
// A scripted clock's frames follow each other however long one takes to draw.
|
||||
match self.last {
|
||||
Some((previous, camera, time)) if (now - time).as_secs_f32() < MAX_FRAME_GAP => {
|
||||
Some((previous, camera, time))
|
||||
if timing.is_some() || (now - time).as_secs_f32() < MAX_FRAME_GAP =>
|
||||
{
|
||||
inputs.previous_view_projection = previous;
|
||||
inputs.camera_motion = (frame.camera - camera).as_vec3().to_array();
|
||||
inputs.delta_time = (now - time).as_secs_f32();
|
||||
@@ -562,12 +575,11 @@ impl NativeFrame {
|
||||
(f32::from(u8::from(inputs.eye_in == 1)), 0.25),
|
||||
(f32::from(u8::from(inputs.eye_in == 2)), 0.25),
|
||||
];
|
||||
let dt = match still {
|
||||
Some(_) => {
|
||||
inputs.delta_time = 0.05;
|
||||
f32::INFINITY
|
||||
}
|
||||
None => inputs.delta_time,
|
||||
let dt = step.unwrap_or(inputs.delta_time);
|
||||
inputs.delta_time = match (timing, step) {
|
||||
(Some(t), _) => t.delta,
|
||||
(None, Some(_)) => 0.05,
|
||||
(None, None) => inputs.delta_time,
|
||||
};
|
||||
for (value, (target, half_life)) in self.eye.iter_mut().zip(targets) {
|
||||
*value = eased(*value, target, half_life, dt);
|
||||
@@ -577,7 +589,7 @@ impl NativeFrame {
|
||||
inputs.in_water = water;
|
||||
inputs.in_lava = lava;
|
||||
self.frames = self.frames.wrapping_add(1);
|
||||
queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(&inputs));
|
||||
inputs
|
||||
}
|
||||
|
||||
/// Takes the post stage's pending history reset. It stays pending if post rendering is
|
||||
@@ -1320,6 +1332,48 @@ mod tests {
|
||||
assert_eq!(wetness(0.25, 0.25, 5.0), 0.25);
|
||||
}
|
||||
|
||||
/// A scripted clock comes before STILL's ticks, eased values and wetness move by its
|
||||
/// delta, and a long wait between its frames is not a gap that starts them over.
|
||||
#[test]
|
||||
fn a_scripted_clock_drives_time_frame_index_and_eased_values() {
|
||||
use crate::gfx::pack_uniforms::FrameTiming;
|
||||
let Some((device, _)) = device() else {
|
||||
return;
|
||||
};
|
||||
let hud = crate::hud::HudBatch::new([4, 4]);
|
||||
let mut frame = crate::gfx::pack_uniforms::initial_history_tests::frame(&hud);
|
||||
let mut native = NativeFrame::new(&device, None, Vec::new(), Default::default(), None);
|
||||
frame.pack.still_ticks = Some(47_811);
|
||||
frame.pack.eye_light = [0, 15];
|
||||
frame.pack.rain = 1.0;
|
||||
frame.pack.frame_timing = Some(FrameTiming::nth(1, 2000.0));
|
||||
let first = native.next_inputs(&frame, [4, 4]);
|
||||
assert_eq!(
|
||||
(first.time, first.frame_index, first.delta_time),
|
||||
(0.0, 1, 0.0)
|
||||
);
|
||||
// Wetness starts dry, as on a fresh pack; frame 1's delta of 0 leaves it there.
|
||||
assert_eq!((first.eye_light_smooth[1], first.wetness), (1.0, 0.0));
|
||||
assert!(native.take_history_reset());
|
||||
native.last.as_mut().unwrap().2 -= std::time::Duration::from_secs(5);
|
||||
frame.pack.eye_light = [0, 0];
|
||||
frame.pack.frame_timing = Some(FrameTiming::nth(2, 2000.0));
|
||||
let second = native.next_inputs(&frame, [4, 4]);
|
||||
assert_eq!(
|
||||
(second.time, second.frame_index, second.delta_time),
|
||||
(2.0, 2, 2.0)
|
||||
);
|
||||
// Two seconds: the eye light's 2 s half-life halves it, and wetness rises
|
||||
// by 1 - 0.5^(2/30) (a 30 s half-life while it rains harder).
|
||||
assert_eq!(second.eye_light_smooth[1], 0.5);
|
||||
assert!(
|
||||
(second.wetness - 0.045_158_4).abs() < 1e-6,
|
||||
"{}",
|
||||
second.wetness
|
||||
);
|
||||
assert!(!native.take_history_reset());
|
||||
}
|
||||
|
||||
fn device() -> Option<(wgpu::Device, wgpu::Queue)> {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
|
||||
|
||||
@@ -30,6 +30,7 @@ struct Stitched {
|
||||
/// Each layer's sprite at the first level: its top left corner and its side.
|
||||
rects: Vec<[u32; 3]>,
|
||||
padding: u32,
|
||||
material: bool,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
/// One per sprite array.
|
||||
groups: Vec<wgpu::BindGroup>,
|
||||
@@ -66,10 +67,19 @@ fn vs(@builtin(vertex_index) v: u32, @location(0) rect: vec4<i32>, @location(1)
|
||||
out.source = source.xy;
|
||||
return out;
|
||||
}
|
||||
fn sprite_texel(in: Out) -> vec2<i32> {
|
||||
return clamp(vec2<i32>(floor(in.position.xy)) - in.rect.xy, vec2(0), vec2(in.rect.z - 1));
|
||||
}
|
||||
@fragment
|
||||
fn fs(in: Out) -> @location(0) vec4<f32> {
|
||||
let texel = clamp(vec2<i32>(floor(in.position.xy)) - in.rect.xy, vec2(0), vec2(in.rect.z - 1));
|
||||
return textureLoad(sprites, texel, i32(in.source.x), i32(in.source.y));
|
||||
return textureLoad(sprites, sprite_texel(in), i32(in.source.x), i32(in.source.y));
|
||||
}
|
||||
@fragment
|
||||
fn fs_material(in: Out) -> @location(0) vec4<f32> {
|
||||
let texel = sprite_texel(in);
|
||||
let edge = i32(textureDimensions(sprites, 0).x);
|
||||
let sampled = min((texel * 2 + 1) * edge / (2 * in.rect.z), vec2(edge - 1));
|
||||
return textureLoad(sprites, sampled, i32(in.source.x), i32(in.source.y));
|
||||
}
|
||||
"#;
|
||||
|
||||
@@ -210,14 +220,72 @@ impl PackAtlas {
|
||||
stitch_mip_level(&sides, mip_option).min(array_levels.saturating_sub(deepest + 1));
|
||||
let max = device.limits().max_texture_dimension_2d;
|
||||
let layout = stitch::stitch(&sides, mip, anisotropy_bit, max)?;
|
||||
let rects = layout
|
||||
.holders
|
||||
.iter()
|
||||
.zip(&sides)
|
||||
.map(|(&[x, y], &side)| [x + layout.padding, y + layout.padding, side])
|
||||
.collect();
|
||||
Some(Self::with_rects(
|
||||
device,
|
||||
encoder,
|
||||
arrays,
|
||||
split,
|
||||
sprite,
|
||||
[layout.width.max(1), layout.height.max(1)],
|
||||
rects,
|
||||
layout.padding,
|
||||
mip + 1,
|
||||
false,
|
||||
))
|
||||
}
|
||||
|
||||
/// Companion maps use the colour atlas's coordinates, not a second packing.
|
||||
pub fn material_for(
|
||||
&self,
|
||||
device: &wgpu::Device,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
arrays: &[wgpu::Texture],
|
||||
layers: u32,
|
||||
) -> Option<Self> {
|
||||
let Some(stitched) = &self.stitched else {
|
||||
return Self::material(device, encoder, arrays, self.split, layers);
|
||||
};
|
||||
Some(Self::with_rects(
|
||||
device,
|
||||
encoder,
|
||||
arrays,
|
||||
self.split,
|
||||
arrays[0].width(),
|
||||
self.size(),
|
||||
stitched.rects.clone(),
|
||||
stitched.padding,
|
||||
1,
|
||||
true,
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn with_rects(
|
||||
device: &wgpu::Device,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
arrays: &[wgpu::Texture],
|
||||
split: u32,
|
||||
sprite: u32,
|
||||
size: [u32; 2],
|
||||
rects: Vec<[u32; 3]>,
|
||||
padding: u32,
|
||||
levels: u32,
|
||||
material: bool,
|
||||
) -> Self {
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("Shader pack block atlas"),
|
||||
size: wgpu::Extent3d {
|
||||
width: layout.width.max(1),
|
||||
height: layout.height.max(1),
|
||||
width: size[0],
|
||||
height: size[1],
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: mip + 1,
|
||||
mip_level_count: levels,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
@@ -268,7 +336,7 @@ impl PackAtlas {
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some("fs"),
|
||||
entry_point: Some(if material { "fs_material" } else { "fs" }),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::TextureFormat::Rgba8Unorm.into())],
|
||||
}),
|
||||
@@ -293,12 +361,6 @@ impl PackAtlas {
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let rects = layout
|
||||
.holders
|
||||
.iter()
|
||||
.zip(&sides)
|
||||
.map(|(&[x, y], &side)| [x + layout.padding, y + layout.padding, side])
|
||||
.collect::<Vec<_>>();
|
||||
let layers = rects.len() as u32;
|
||||
let atlas = Self {
|
||||
texture,
|
||||
@@ -309,13 +371,14 @@ impl PackAtlas {
|
||||
separate_ao: false,
|
||||
stitched: Some(Stitched {
|
||||
rects,
|
||||
padding: layout.padding,
|
||||
padding,
|
||||
material,
|
||||
pipeline,
|
||||
groups,
|
||||
}),
|
||||
};
|
||||
atlas.copy(device, encoder, arrays, 0..layers);
|
||||
Some(atlas)
|
||||
atlas
|
||||
}
|
||||
|
||||
/// Whether the sprites keep their own sizes ([`PackAtlas::stitched`]).
|
||||
@@ -425,7 +488,11 @@ impl PackAtlas {
|
||||
rect: [x, y, side, stitched.padding].map(|v| (v >> level) as i32),
|
||||
source: [
|
||||
layer % self.split,
|
||||
(self.sprite / side).ilog2() + level,
|
||||
if stitched.material {
|
||||
0
|
||||
} else {
|
||||
(self.sprite / side).ilog2() + level
|
||||
},
|
||||
size[0],
|
||||
size[1],
|
||||
],
|
||||
@@ -563,7 +630,7 @@ impl PackAtlas {
|
||||
"layout(set = 1, binding = 0) readonly buffer HzBlockSlots { uint hz_block_slots[]; };\n\
|
||||
layout(set = 1, binding = 1) readonly buffer HzSlotOrigins { vec4 hz_slot_origins[]; };\n",
|
||||
);
|
||||
// Of each sprite layer, `mc_Entity` (id, then 1 for fluids or -1) and
|
||||
// Of each sprite layer, `mc_Entity` (id, then 1 for fluids or 0) and
|
||||
// `at_midBlock.w` (its block's light), the game's table; and the arena page's
|
||||
// vertices (`TerrainVertex`, six words each).
|
||||
declarations.push_str(
|
||||
@@ -641,7 +708,7 @@ impl PackAtlas {
|
||||
let entity = if flat {
|
||||
"vec4(float(hz_block_ids[3u * hz_layer]), float(hz_block_ids[3u * hz_layer + 1u]), 0.0, 0.0)"
|
||||
} else {
|
||||
"vec4(-1.0, -1.0, 0.0, 0.0)"
|
||||
"vec4(-1.0, 0.0, 0.0, 0.0)"
|
||||
};
|
||||
// The block a face belongs to: half a block behind its centre (the quad's diagonal);
|
||||
// w, the light it gives (Iris's `BLOCK_EMISSION_ATTRIBUTE`, 0 to 15).
|
||||
@@ -894,6 +961,144 @@ mod tests {
|
||||
assert_eq!([at(9, 5), at(8, 4), at(10, 6)], [[100, 0, 0, 255]; 3]);
|
||||
}
|
||||
|
||||
fn material_fixture(animated: bool) {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
// No adapter (a CI runner without a GPU): skip, as the module's other readback tests do.
|
||||
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
|
||||
return;
|
||||
};
|
||||
let (device, queue) =
|
||||
pollster::block_on(adapter.request_device(&Default::default())).unwrap();
|
||||
let colour_array = upscaled_array(&device, &queue, &[2, 4, 1]);
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
let colour = PackAtlas::stitched(
|
||||
&device,
|
||||
&mut encoder,
|
||||
&[colour_array],
|
||||
3,
|
||||
4,
|
||||
&[2, 4, 1],
|
||||
0,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
for kind in 0..2 {
|
||||
// Different source resolution, with encoded channel boundaries and defaults.
|
||||
let markers = if kind == 0 {
|
||||
[
|
||||
[128, 126, 255, 19],
|
||||
crate::sprites::FLAT_NORMAL,
|
||||
[130, 125, 254, 21],
|
||||
]
|
||||
} else {
|
||||
[
|
||||
[17, 240, 65, 255],
|
||||
[18, 0, 66, 0],
|
||||
crate::sprites::NO_SPECULAR,
|
||||
]
|
||||
};
|
||||
let pixels: Vec<_> = markers.iter().flat_map(|m| m.repeat(4)).collect();
|
||||
let array = wgpu::util::DeviceExt::create_texture_with_data(
|
||||
&device,
|
||||
&queue,
|
||||
&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d {
|
||||
width: 2,
|
||||
height: 2,
|
||||
depth_or_array_layers: 3,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
},
|
||||
wgpu::util::TextureDataOrder::LayerMajor,
|
||||
&pixels,
|
||||
);
|
||||
let arrays = [array];
|
||||
let material = colour
|
||||
.material_for(&device, &mut encoder, &arrays, 3)
|
||||
.unwrap();
|
||||
assert_eq!(material.size(), [16, 8]);
|
||||
assert_eq!(material.rects_table(), colour.rects_table());
|
||||
assert_eq!(material.texture.mip_level_count(), 1);
|
||||
queue.submit([encoder.finish()]);
|
||||
encoder = device.create_command_encoder(&Default::default());
|
||||
let changed = [243, 1, 229, 0];
|
||||
if animated {
|
||||
// Alter every source layer, but copy only the animated layer.
|
||||
queue.write_texture(
|
||||
arrays[0].as_image_copy(),
|
||||
&changed.repeat(12),
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
bytes_per_row: Some(8),
|
||||
rows_per_image: Some(2),
|
||||
offset: 0,
|
||||
},
|
||||
arrays[0].size(),
|
||||
);
|
||||
material.copy(&device, &mut encoder, &arrays, [0]);
|
||||
}
|
||||
let read = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: None,
|
||||
size: 256 * 8,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
encoder.copy_texture_to_buffer(
|
||||
material.texture.as_image_copy(),
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &read,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
bytes_per_row: Some(256),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
material.texture.size(),
|
||||
);
|
||||
queue.submit([encoder.finish()]);
|
||||
encoder = device.create_command_encoder(&Default::default());
|
||||
read.slice(..).map_async(wgpu::MapMode::Read, |_| {});
|
||||
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
|
||||
let data = read.slice(..).get_mapped_range().unwrap();
|
||||
// Independent, hand-worked colour rectangles, including all padding pixels.
|
||||
for (layer, [x, y, side]) in [[9usize, 1, 2], [1, 1, 4], [9, 5, 1]]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let expected = if animated && layer == 0 {
|
||||
changed
|
||||
} else {
|
||||
markers[layer]
|
||||
};
|
||||
for py in y - 1..=y + side {
|
||||
for px in x - 1..=x + side {
|
||||
let at = py * 256 + px * 4;
|
||||
assert_eq!(
|
||||
&data[at..at + 4],
|
||||
&expected,
|
||||
"kind={kind}, layer={layer}, ({px},{py})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
queue.submit([encoder.finish()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stitched_material_maps_share_colour_rectangles() {
|
||||
material_fixture(false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animated_stitched_material_updates_only_its_rectangle() {
|
||||
material_fixture(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terrain_inputs_respects_separate_ao() {
|
||||
let instance = crate::gfx::test_gpu::instance();
|
||||
|
||||
@@ -54,8 +54,12 @@ pub struct PackHand {
|
||||
slots: wgpu::Buffer,
|
||||
pub slot_group: wgpu::BindGroup,
|
||||
stride: u64,
|
||||
/// This frame's runs: first quad, quads, and the dynamic offset of their slot.
|
||||
/// This frame's runs: first quad, quads, and the dynamic offset of their slot. The opaque
|
||||
/// quads come first in the buffer, then the translucent ones (`translucent_runs`), which a
|
||||
/// pack's `gbuffers_hand_water` draws after deferred.
|
||||
pub runs: Vec<(u32, u32, u32)>,
|
||||
pub translucent_runs: Vec<(u32, u32, u32)>,
|
||||
order: Vec<Vertex>,
|
||||
}
|
||||
|
||||
impl PackHand {
|
||||
@@ -120,6 +124,8 @@ impl PackHand {
|
||||
slot_group,
|
||||
stride,
|
||||
runs: Vec::new(),
|
||||
translucent_runs: Vec::new(),
|
||||
order: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,17 +172,25 @@ impl PackHand {
|
||||
) {
|
||||
self.quads = (hand.len() / 4) as u32;
|
||||
self.runs.clear();
|
||||
self.translucent_runs.clear();
|
||||
if self.quads == 0 {
|
||||
return;
|
||||
}
|
||||
let split = split(hand, items);
|
||||
self.order = split.order;
|
||||
let mut contents = Vec::new();
|
||||
for (start, quads, id) in runs(self.quads, items) {
|
||||
let slot = (self.runs.len() as u64).min(SLOTS - 1);
|
||||
if slot as usize == self.runs.len() {
|
||||
contents.extend_from_slice(&draw_slot(id));
|
||||
for (i, &(first, quads, id, translucent)) in split.runs.iter().enumerate() {
|
||||
let slot = (i as u64).min(SLOTS - 1);
|
||||
if slot == i as u64 {
|
||||
contents.extend_from_slice(&draw_slot(id, translucent));
|
||||
contents.resize(((slot + 1) * self.stride) as usize, 0);
|
||||
}
|
||||
self.runs.push((start, quads, (slot * self.stride) as u32));
|
||||
let run = (first, quads, (slot * self.stride) as u32);
|
||||
if translucent {
|
||||
self.translucent_runs.push(run);
|
||||
} else {
|
||||
self.runs.push(run);
|
||||
}
|
||||
}
|
||||
queue.write_buffer(&self.slots, 0, &contents);
|
||||
if self.quads > self.capacity {
|
||||
@@ -185,7 +199,7 @@ impl PackHand {
|
||||
Self::vertex_buffer(device, &self.layout, &self.uniform, self.capacity);
|
||||
self.indices = super::pack_entities::quad_indices(device, self.capacity);
|
||||
}
|
||||
queue.write_buffer(&self.vertices, 0, bytemuck::cast_slice(hand));
|
||||
queue.write_buffer(&self.vertices, 0, bytemuck::cast_slice(&self.order));
|
||||
let matrices = [projection, projection.inverse()].map(|m| m.to_cols_array());
|
||||
queue.write_buffer(&self.uniform, 0, bytemuck::cast_slice(&matrices));
|
||||
}
|
||||
@@ -198,11 +212,62 @@ impl PackHand {
|
||||
/// The hand's runs a frame keeps slots for.
|
||||
const SLOTS: u64 = 8;
|
||||
|
||||
/// `HzDraw` of a hand run: `renderStage` the solid hand's, `blockEntityId` -1 as outside block
|
||||
/// entities, and `currentRenderedItemId`.
|
||||
fn draw_slot(id: i32) -> [u8; SLOT as usize] {
|
||||
/// The hand's quads reordered, opaque first and translucent after (each in their order), and
|
||||
/// their runs: first quad, quads, `currentRenderedItemId`, translucent.
|
||||
struct Split {
|
||||
order: Vec<Vertex>,
|
||||
runs: Vec<(u32, u32, i32, bool)>,
|
||||
}
|
||||
|
||||
fn split(hand: &[Vertex], items: &[(u32, i32)]) -> Split {
|
||||
let quads = (hand.len() / 4) as u32;
|
||||
let mut ids = vec![0; quads as usize];
|
||||
for (start, n, id) in runs(quads, items) {
|
||||
ids[start as usize..(start + n) as usize].fill(id);
|
||||
}
|
||||
let kind = |q: usize| translucent_quad(&hand[q * 4..q * 4 + 4]);
|
||||
let opaque: Vec<usize> = (0..quads as usize).filter(|&q| !kind(q)).collect();
|
||||
let clear: Vec<usize> = (0..quads as usize).filter(|&q| kind(q)).collect();
|
||||
let mut out = Split {
|
||||
order: Vec::with_capacity(hand.len()),
|
||||
runs: Vec::new(),
|
||||
};
|
||||
for &q in opaque.iter().chain(&clear) {
|
||||
out.order.extend_from_slice(&hand[q * 4..q * 4 + 4]);
|
||||
}
|
||||
for (translucent, list, base) in [(false, &opaque, 0), (true, &clear, opaque.len())] {
|
||||
let mut i = 0;
|
||||
while i < list.len() {
|
||||
let id = ids[list[i]];
|
||||
let mut n = 1;
|
||||
while i + n < list.len() && ids[list[i + n]] == id {
|
||||
n += 1;
|
||||
}
|
||||
out.runs
|
||||
.push(((base + i) as u32, n as u32, id, translucent));
|
||||
i += n;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A held item's quad drawn from a translucent sheet (`item_render`: its overlay layer).
|
||||
fn translucent_quad(quad: &[Vertex]) -> bool {
|
||||
matches!(
|
||||
quad[0].layer & 0xffff,
|
||||
super::TRANSLUCENT_ITEM_OVERLAY | super::TRANSLUCENT_GLINT_ITEM_OVERLAY
|
||||
)
|
||||
}
|
||||
|
||||
/// `HzDraw` of a hand run: `renderStage` the solid or translucent hand's, `blockEntityId` -1 as
|
||||
/// outside block entities, and `currentRenderedItemId`.
|
||||
fn draw_slot(id: i32, translucent: bool) -> [u8; SLOT as usize] {
|
||||
let mut bytes = [0u8; SLOT as usize];
|
||||
let stage = horizon_shaders::render_stage("HAND_SOLID") as i32;
|
||||
let stage = horizon_shaders::render_stage(if translucent {
|
||||
"HAND_TRANSLUCENT"
|
||||
} else {
|
||||
"HAND_SOLID"
|
||||
}) as i32;
|
||||
bytes[0..4].copy_from_slice(&stage.to_le_bytes());
|
||||
bytes[8..12].copy_from_slice(&(-1i32).to_le_bytes());
|
||||
bytes[12..16].copy_from_slice(&id.to_le_bytes());
|
||||
@@ -233,6 +298,42 @@ fn runs(quads: u32, items: &[(u32, i32)]) -> Vec<(u32, u32, i32)> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The arm, then an item whose middle quad is translucent, then a second item: the opaque
|
||||
/// quads keep their order first, the translucent one comes after them, and each run keeps
|
||||
/// its item's id.
|
||||
#[test]
|
||||
fn translucent_quads_go_after_the_opaque_ones() {
|
||||
let quad = |layer: u32, tag: f32| {
|
||||
[Vertex {
|
||||
layer,
|
||||
pos: [tag, 0.0, 0.0],
|
||||
..bytemuck::Zeroable::zeroed()
|
||||
}; 4]
|
||||
};
|
||||
let glass = super::super::TRANSLUCENT_ITEM_OVERLAY;
|
||||
let hand: Vec<Vertex> = [
|
||||
quad(0, 0.),
|
||||
quad(0, 1.),
|
||||
quad(glass, 2.),
|
||||
quad(0, 3.),
|
||||
quad(0, 4.),
|
||||
]
|
||||
.concat();
|
||||
// The arm is quad 0; item 7 quads 1..4; item 9 quad 4.
|
||||
let split = split(&hand, &[(4, 7), (16, 9)]);
|
||||
let order: Vec<f32> = split.order.chunks(4).map(|q| q[0].pos[0]).collect();
|
||||
assert_eq!(order, [0., 1., 3., 4., 2.]);
|
||||
assert_eq!(
|
||||
split.runs,
|
||||
[
|
||||
(0, 1, 0, false),
|
||||
(1, 2, 7, false),
|
||||
(3, 1, 9, false),
|
||||
(4, 1, 7, true)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Each item's quads draw with its id; geometry before the first item is the arm's (0).
|
||||
#[test]
|
||||
fn runs_split_the_hand_by_item() {
|
||||
|
||||
@@ -30,11 +30,12 @@ impl PackImage {
|
||||
}
|
||||
}
|
||||
|
||||
/// The images `image.*` declares.
|
||||
pub(super) fn images(defs: Vec<CustomImage>) -> Vec<PackImage> {
|
||||
/// The images `image.*` declares; `packed_float`: this device stores into `Rg11b10Ufloat`
|
||||
/// (`format`).
|
||||
pub(super) fn images(defs: Vec<CustomImage>, packed_float: bool) -> Vec<PackImage> {
|
||||
defs.into_iter()
|
||||
.map(|def| PackImage {
|
||||
format: format(def.format),
|
||||
format: format(def.format, packed_float),
|
||||
dimension: if def.is_1d() {
|
||||
wgpu::TextureViewDimension::D1
|
||||
} else if def.is_3d() {
|
||||
@@ -63,7 +64,10 @@ fn texture_dimension(view: wgpu::TextureViewDimension) -> wgpu::TextureDimension
|
||||
}
|
||||
|
||||
/// The wgpu format of an image's internal format: a storage format of the same channels.
|
||||
pub(super) fn format(f: Format) -> wgpu::TextureFormat {
|
||||
/// `R11F_G11F_B10F` is itself where the device stores into it (`packed_float`): no alpha, so it
|
||||
/// reads 1 there, as Iris 1.11.7 read it with a probe pack of ours (#10490); elsewhere an
|
||||
/// `Rgba16Float` stand-in, which keeps an alpha.
|
||||
pub(super) fn format(f: Format, packed_float: bool) -> wgpu::TextureFormat {
|
||||
use wgpu::TextureFormat as T;
|
||||
let pick = |r, rg, rgba| match f.channels {
|
||||
1 => r,
|
||||
@@ -78,6 +82,7 @@ pub(super) fn format(f: Format) -> wgpu::TextureFormat {
|
||||
(Kind::Uint, 16) => pick(T::R16Uint, T::Rg16Uint, T::Rgba16Uint),
|
||||
(Kind::Uint, _) => pick(T::R32Uint, T::Rg32Uint, T::Rgba32Uint),
|
||||
(Kind::Float, 32) => pick(T::R32Float, T::Rg32Float, T::Rgba32Float),
|
||||
(Kind::Float, 11) if packed_float => T::Rg11b10Ufloat,
|
||||
(Kind::Float, _) => pick(T::R16Float, T::Rg16Float, T::Rgba16Float),
|
||||
(Kind::Snorm, _) => pick(T::R8Snorm, T::Rg8Snorm, T::Rgba8Snorm),
|
||||
(Kind::Unorm, 16) => pick(T::R16Unorm, T::Rg16Unorm, T::Rgba16Unorm),
|
||||
@@ -87,8 +92,12 @@ pub(super) fn format(f: Format) -> wgpu::TextureFormat {
|
||||
|
||||
/// Whether this device can make `def`: its format's features, and a size in pixels within the
|
||||
/// device's limits.
|
||||
pub(super) fn makeable(device: &wgpu::Device, def: &CustomImage) -> Result<(), String> {
|
||||
let format = format(def.format);
|
||||
pub(super) fn makeable(
|
||||
device: &wgpu::Device,
|
||||
def: &CustomImage,
|
||||
packed_float: bool,
|
||||
) -> Result<(), String> {
|
||||
let format = format(def.format, packed_float);
|
||||
let missing = format.required_features() - device.features();
|
||||
if !missing.is_empty() {
|
||||
return Err(format!(
|
||||
|
||||
@@ -34,27 +34,51 @@ mod packed;
|
||||
#[cfg(test)]
|
||||
mod world_target_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod celestial_blend_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod higher_shadowcolor_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod single_side_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod sky_disc_blend_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod undrawn_terrain_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod shadow_faces_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod r11f_image_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod forced_flip_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod weather_depth_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod float_into_integer_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod custom_type_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod sky_depth_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod runtime_length_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod translucent_color_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod noise_resolution_tests;
|
||||
|
||||
/// The texture behind a pack sampler name.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Source {
|
||||
@@ -366,6 +390,9 @@ struct Pass {
|
||||
name: String,
|
||||
group: Group,
|
||||
pipeline: Pipeline,
|
||||
/// The same program as vanilla blends the sky's parts (`PostProgram::vanilla_sky_blends`):
|
||||
/// without blending for the disc and the void, then with `STARS_BLEND` for the stars.
|
||||
sky_parts: Option<[wgpu::RenderPipeline; 2]>,
|
||||
layout: wgpu::BindGroupLayout,
|
||||
ubo: wgpu::Buffer,
|
||||
block: Vec<u8>,
|
||||
@@ -477,7 +504,7 @@ pub(super) struct World<'a> {
|
||||
pub lightmap: wgpu::TextureView,
|
||||
/// Terrain's geometry, when the pack draws it.
|
||||
pub terrain: Option<Geometry<'a>>,
|
||||
/// Of each block sprite layer, `mc_Entity` (id, then 1 for fluids or -1) and `at_midBlock.w`
|
||||
/// Of each block sprite layer, `mc_Entity` (id, then 1 for fluids or 0) and `at_midBlock.w`
|
||||
/// (its block's light): three values a layer.
|
||||
pub block_ids: &'a [i32],
|
||||
/// The atlas of the sun and moon's sprites.
|
||||
@@ -555,6 +582,10 @@ pub struct PackPost {
|
||||
terrain_cutout: Option<Pass>,
|
||||
/// What draws translucent terrain (`gbuffers_water` or its fallback), after `deferred`.
|
||||
water: Option<Pass>,
|
||||
/// The pack has no program for solid and cutout terrain, or for translucent terrain:
|
||||
/// nothing draws it (`horizon_shaders::Post::terrain_undrawn`).
|
||||
terrain_undrawn: bool,
|
||||
water_undrawn: bool,
|
||||
/// What draws the sky (`gbuffers_skybasic` or its fallback), before terrain, and its vertices.
|
||||
sky: Option<Sky>,
|
||||
/// What draws entities and block entities, after terrain, and their vertices.
|
||||
@@ -583,6 +614,8 @@ pub struct PackPost {
|
||||
/// What draws the first-person hand (`gbuffers_hand` or its fallback), after the opaque
|
||||
/// world, and its vertices.
|
||||
hand: Option<(Pass, super::pack_hand::PackHand)>,
|
||||
/// `gbuffers_hand_water` (or its fallback): the hand's translucent quads, after deferred.
|
||||
hand_water: Option<Pass>,
|
||||
/// The pre-translucent copy: `depthtex1`, or `depthtex2` without a shader hand.
|
||||
depth1: bool,
|
||||
/// A program after the hand reads `depthtex2`: it is copied before the hand.
|
||||
@@ -848,10 +881,11 @@ fn source(name: &str, shadow_space: bool, shadow_pass: bool, watershadow: bool)
|
||||
}
|
||||
}
|
||||
|
||||
/// 256×256 RGB white noise (6.4), from a fixed seed so every run looks the same.
|
||||
fn noise(device: &wgpu::Device, queue: &wgpu::Queue) -> wgpu::TextureView {
|
||||
/// `size`×`size` RGB white noise (6.4), from a fixed seed so every run looks the same.
|
||||
fn noise(device: &wgpu::Device, queue: &wgpu::Queue, size: u32) -> wgpu::TextureView {
|
||||
let size = size.min(device.limits().max_texture_dimension_2d);
|
||||
let mut state = 0x2545_f491_4f6c_dd1du64;
|
||||
let texels: Vec<u8> = (0..256 * 256 * 4)
|
||||
let texels: Vec<u8> = (0..size as usize * size as usize * 4)
|
||||
.map(|i| {
|
||||
state ^= state << 13;
|
||||
state ^= state >> 7;
|
||||
@@ -859,7 +893,7 @@ fn noise(device: &wgpu::Device, queue: &wgpu::Queue) -> wgpu::TextureView {
|
||||
if i % 4 == 3 { 255 } else { (state >> 56) as u8 }
|
||||
})
|
||||
.collect();
|
||||
texture(device, queue, "Shader pack noisetex", 256, &texels)
|
||||
texture(device, queue, "Shader pack noisetex", size, &texels)
|
||||
}
|
||||
|
||||
fn texture(
|
||||
@@ -1273,8 +1307,9 @@ fn bindings(
|
||||
let mut reads = 0u16;
|
||||
let mut shadow_reads = 0u16;
|
||||
let watershadow = interface.samplers.iter().any(|s| s.name == "watershadow");
|
||||
// A gbuffers pass draws into its buffers and the depth buffer: reading them as well would
|
||||
// be a feedback loop (undefined in GL, refused by wgpu), so those read a blank texture.
|
||||
// Opaque gbuffers draws cannot sample their own attachments. Translucent draws can
|
||||
// read colortex4 and above from the preceding deferred stage; those reads use a copy
|
||||
// taken before the draw, so the sampled texture is never also an attachment.
|
||||
let in_place = match geometry {
|
||||
Some(_) => buffers.iter().fold(0u16, |m, &b| m | 1 << b),
|
||||
None => 0,
|
||||
@@ -1384,7 +1419,9 @@ fn bindings(
|
||||
Source::Depth => geometry.is_some(),
|
||||
// Translucent terrain reads the copy; opaque terrain would read itself.
|
||||
Source::Depth1 | Source::Depth2 => group == Group::Gbuffers,
|
||||
Source::Color(n) => in_place & 1 << n != 0,
|
||||
Source::Color(n) => {
|
||||
(group != Group::Translucent || n < 4) && in_place & 1 << n != 0
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let mut sample_type = match src {
|
||||
@@ -1512,13 +1549,20 @@ fn bindings(
|
||||
}
|
||||
(None, None) => stand_in(&i.name, notes),
|
||||
};
|
||||
if actual != format {
|
||||
// An `R11F_G11F_B10F` image or buffer this device makes `RGBA16F` (a float stand-in
|
||||
// that neither clamps nor drops NaN) takes `layout(r11f_g11f_b10f)` as `rgba16f`
|
||||
// (`storage_formats_of`): Iris 1.11.7 stored into and read back such an image with a
|
||||
// probe pack of ours (#10490), where Horizon refused the program.
|
||||
let stand_in = format == wgpu::TextureFormat::Rg11b10Ufloat
|
||||
&& actual == wgpu::TextureFormat::Rgba16Float;
|
||||
if actual != format && !stand_in {
|
||||
notes.push(format!(
|
||||
"{name}: `{}` is {} but its texture is {actual:?}",
|
||||
i.name, i.format
|
||||
));
|
||||
return None;
|
||||
}
|
||||
let format = actual;
|
||||
let access = match i.access.as_str() {
|
||||
"writeonly" => wgpu::StorageTextureAccess::WriteOnly,
|
||||
"readonly" => wgpu::StorageTextureAccess::ReadOnly,
|
||||
@@ -1549,6 +1593,51 @@ fn bindings(
|
||||
Some((entries, samplers, slots, reads, shadow_reads, images))
|
||||
}
|
||||
|
||||
/// Makes each storage image of `module` in group 0 declare the format its layout entry binds:
|
||||
/// a `layout(r11f_g11f_b10f)` image whose texture is the `RGBA16F` stand-in (`bindings`).
|
||||
fn storage_formats_of(module: &mut naga::Module, entries: &[wgpu::BindGroupLayoutEntry]) {
|
||||
let globals: Vec<_> = module.global_variables.iter().map(|(h, _)| h).collect();
|
||||
for handle in globals {
|
||||
let var = &module.global_variables[handle];
|
||||
let Some(naga::ResourceBinding { group: 0, binding }) = var.binding else {
|
||||
continue;
|
||||
};
|
||||
let naga::TypeInner::Image {
|
||||
dim,
|
||||
arrayed,
|
||||
class: naga::ImageClass::Storage { format, access },
|
||||
} = module.types[var.ty].inner
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let bound = entries.iter().find_map(|e| match e.ty {
|
||||
wgpu::BindingType::StorageTexture { format, .. } if e.binding == binding => {
|
||||
Some(format)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if format == naga::StorageFormat::Rg11b10Ufloat
|
||||
&& bound == Some(wgpu::TextureFormat::Rgba16Float)
|
||||
{
|
||||
let ty = module.types.insert(
|
||||
naga::Type {
|
||||
name: module.types[var.ty].name.clone(),
|
||||
inner: naga::TypeInner::Image {
|
||||
dim,
|
||||
arrayed,
|
||||
class: naga::ImageClass::Storage {
|
||||
format: naga::StorageFormat::Rgba16Float,
|
||||
access,
|
||||
},
|
||||
},
|
||||
},
|
||||
naga::Span::UNDEFINED,
|
||||
);
|
||||
module.global_variables[handle].ty = ty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The wgpu format of an image's format qualifier.
|
||||
fn image_format(qualifier: &str) -> Option<wgpu::TextureFormat> {
|
||||
use wgpu::TextureFormat as T;
|
||||
@@ -1664,10 +1753,15 @@ fn build_pass(
|
||||
features, interface, &p.name, p.group, &p.buffers, geometry, formats, shadow, notes, pack,
|
||||
)?;
|
||||
let final_pass = p.group == Group::Final;
|
||||
// Outputs into an integer buffer, and its integer kind (`packed::bitcast_outputs`).
|
||||
let mut int_outputs = Vec::new();
|
||||
let mut targets = Vec::new();
|
||||
let mut writes = Vec::new();
|
||||
// Outputs into a buffer of a small packed format, and the bits it keeps.
|
||||
let mut packed_outputs = Vec::new();
|
||||
// Each output's own `blend.<program>.<buffer>`, and whether its buffer blends at all: what
|
||||
// the sky's part pipelines keep (`PostProgram::vanilla_sky_blends`).
|
||||
let mut own_blends: Vec<(Option<wgpu::BlendState>, bool)> = Vec::new();
|
||||
for &location in &interface.outputs {
|
||||
let l = location as usize;
|
||||
if targets.len() <= l {
|
||||
@@ -1679,6 +1773,15 @@ fn build_pass(
|
||||
}
|
||||
} else if let Some(&buffer) = p.buffers.get(l) {
|
||||
let format = formats[buffer as usize];
|
||||
match format.sample_type(None, Some(features)) {
|
||||
Some(wgpu::TextureSampleType::Uint) => {
|
||||
int_outputs.push((location, naga::ScalarKind::Uint));
|
||||
}
|
||||
Some(wgpu::TextureSampleType::Sint) => {
|
||||
int_outputs.push((location, naga::ScalarKind::Sint));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let mut state = target(format);
|
||||
if !p.group.shadow()
|
||||
&& pack.rgb & 1 << buffer != 0
|
||||
@@ -1686,19 +1789,21 @@ fn build_pass(
|
||||
{
|
||||
t.write_mask = wgpu::ColorWrites::COLOR;
|
||||
}
|
||||
if let (Some(t), Some(b)) = (
|
||||
&mut state,
|
||||
p.buffer_blends
|
||||
.iter()
|
||||
.find(|(n, _)| *n == buffer)
|
||||
.map_or(p.blend, |(_, b)| *b),
|
||||
) && format
|
||||
let own = p.buffer_blends.iter().find(|(n, _)| *n == buffer);
|
||||
let blendable = format
|
||||
.guaranteed_format_features(features)
|
||||
.flags
|
||||
.contains(wgpu::TextureFormatFeatureFlags::BLENDABLE)
|
||||
.contains(wgpu::TextureFormatFeatureFlags::BLENDABLE);
|
||||
if let (Some(t), Some(b)) = (&mut state, own.map_or(p.blend, |(_, b)| *b))
|
||||
&& blendable
|
||||
{
|
||||
t.blend = Some(blend_state(b));
|
||||
}
|
||||
if own_blends.len() <= l {
|
||||
own_blends.resize(l + 1, (None, false));
|
||||
}
|
||||
let own = own.and_then(|(_, b)| *b).filter(|_| blendable);
|
||||
own_blends[l] = (own.map(blend_state), blendable);
|
||||
if !p.group.shadow()
|
||||
&& let Some(bits) = pack.packed[buffer as usize]
|
||||
{
|
||||
@@ -1758,6 +1863,7 @@ fn build_pass(
|
||||
});
|
||||
let module = |mut m: naga::Module| {
|
||||
share_samplers(&mut m, &slots);
|
||||
storage_formats_of(&mut m, &entries);
|
||||
device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some(&p.name),
|
||||
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(m)),
|
||||
@@ -1765,55 +1871,76 @@ fn build_pass(
|
||||
};
|
||||
let mut fragment = program.fragment;
|
||||
packed::quantise_outputs(&mut fragment, &packed_outputs);
|
||||
packed::bitcast_outputs(&mut fragment, &int_outputs);
|
||||
let (vs, fs) = (module(program.vertex), module(fragment));
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(&p.name),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &vs,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: Default::default(),
|
||||
// A geometry stage reads its vertices itself.
|
||||
buffers: geometry
|
||||
.filter(|_| p.geometry_triangles == 0)
|
||||
.map_or(&[], |g| g.buffers),
|
||||
},
|
||||
// Rows flipped to GL's turn front faces clockwise.
|
||||
primitive: wgpu::PrimitiveState {
|
||||
front_face: wgpu::FrontFace::Cw,
|
||||
cull_mode: geometry
|
||||
.filter(|g| g.cull && p.geometry_triangles == 0)
|
||||
.map(|_| wgpu::Face::Back),
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: geometry.map(|g| wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: Some(g.depth_write),
|
||||
depth_compare: Some(wgpu::CompareFunction::LessEqual),
|
||||
stencil: Default::default(),
|
||||
bias: if p.name == "gbuffers_damagedblock" {
|
||||
wgpu::DepthBiasState {
|
||||
constant: -10,
|
||||
slope_scale: -1.0,
|
||||
clamp: 0.0,
|
||||
}
|
||||
} else {
|
||||
Default::default()
|
||||
let descriptor = |targets: &[Option<wgpu::ColorTargetState>]| {
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(&p.name),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &vs,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: Default::default(),
|
||||
// A geometry stage reads its vertices itself.
|
||||
buffers: geometry
|
||||
.filter(|_| p.geometry_triangles == 0)
|
||||
.map_or(&[], |g| g.buffers),
|
||||
},
|
||||
}),
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &fs,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &targets,
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
// Rows flipped to GL's turn front faces clockwise.
|
||||
primitive: wgpu::PrimitiveState {
|
||||
front_face: wgpu::FrontFace::Cw,
|
||||
cull_mode: geometry
|
||||
.filter(|g| g.cull && p.geometry_triangles == 0)
|
||||
.map(|_| wgpu::Face::Back),
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: geometry.map(|g| wgpu::DepthStencilState {
|
||||
format: wgpu::TextureFormat::Depth32Float,
|
||||
depth_write_enabled: Some(g.depth_write),
|
||||
depth_compare: Some(wgpu::CompareFunction::LessEqual),
|
||||
stencil: Default::default(),
|
||||
bias: if p.name == "gbuffers_damagedblock" {
|
||||
wgpu::DepthBiasState {
|
||||
constant: -10,
|
||||
slope_scale: -1.0,
|
||||
clamp: 0.0,
|
||||
}
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
}),
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &fs,
|
||||
entry_point: Some("main"),
|
||||
compilation_options: Default::default(),
|
||||
targets,
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let pipeline = descriptor(&targets);
|
||||
// The program's own blend replaced by vanilla's for the part; a buffer's own
|
||||
// `blend.<program>.<buffer>` still applies (Iris 1.11.7 blended the sky disc into that
|
||||
// buffer only, with a probe pack of ours).
|
||||
let sky_parts = p.vanilla_sky_blends.then(|| {
|
||||
[None, Some(blend_state(horizon_shaders::STARS_BLEND))].map(|part| {
|
||||
let targets: Vec<_> = (targets.iter().enumerate())
|
||||
.map(|(l, t)| {
|
||||
let (own, blendable) = own_blends.get(l).copied().unwrap_or((None, false));
|
||||
t.clone().map(|t| wgpu::ColorTargetState {
|
||||
blend: own.or(part.filter(|_| blendable)),
|
||||
..t
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
descriptor(&targets)
|
||||
})
|
||||
});
|
||||
(layout, pipeline)
|
||||
(layout, pipeline, sky_parts)
|
||||
});
|
||||
let (layout, pipeline) = match created {
|
||||
let (layout, pipeline, sky_parts) = match created {
|
||||
Ok(created) => created,
|
||||
Err(e) => {
|
||||
notes.push(format!("{}: skipped: {e}", p.name));
|
||||
@@ -1851,6 +1978,7 @@ fn build_pass(
|
||||
name: p.name,
|
||||
group: p.group,
|
||||
pipeline: Pipeline::Render(pipeline),
|
||||
sky_parts,
|
||||
layout,
|
||||
samplers,
|
||||
writes,
|
||||
@@ -1865,7 +1993,13 @@ fn build_pass(
|
||||
ids: geometry.is_some_and(|g| g.ids.is_some()),
|
||||
groups: HashMap::new(),
|
||||
portal_groups: HashMap::new(),
|
||||
copies: image_buffers(&images) & reads,
|
||||
copies: (image_buffers(&images)
|
||||
| if p.group == Group::Translucent {
|
||||
written
|
||||
} else {
|
||||
0
|
||||
})
|
||||
& reads,
|
||||
reads: reads | image_buffers(&images),
|
||||
buffers: !interface.buffers.is_empty(),
|
||||
images,
|
||||
@@ -1938,6 +2072,7 @@ fn build_compute(
|
||||
});
|
||||
let mut module = c.module;
|
||||
share_samplers(&mut module, &slots);
|
||||
storage_formats_of(&mut module, &entries);
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some(&name),
|
||||
source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)),
|
||||
@@ -1983,6 +2118,7 @@ fn build_compute(
|
||||
render: c.work_groups_render,
|
||||
local,
|
||||
},
|
||||
sky_parts: None,
|
||||
layout,
|
||||
samplers,
|
||||
writes: Vec::new(),
|
||||
@@ -2067,6 +2203,7 @@ impl PackPost {
|
||||
}
|
||||
let textures = pack_textures::upload(device, queue, post.textures, &mut notes);
|
||||
let shadow_config = post.config.shadow.clone();
|
||||
let noise_size = post.config.noise_resolution.unwrap_or(256);
|
||||
// shadowcolor2 to 7 only for a pack that names `HIGHER_SHADOWCOLOR`.
|
||||
let shadow_colors = (1u16 << post.shadow_colors) - 1;
|
||||
let mut custom = post.custom;
|
||||
@@ -2109,9 +2246,14 @@ impl PackPost {
|
||||
// An image this device can't make or store into: the programs using it are skipped.
|
||||
let mut image_defs = std::mem::take(&mut post.images);
|
||||
let mut left_out = Vec::new();
|
||||
let packed_float = gpu.storable(adapter, wgpu::TextureFormat::Rg11b10Ufloat, features);
|
||||
image_defs.retain(|def| {
|
||||
let made = pack_images::makeable(device, def).and_then(|()| {
|
||||
if gpu.storable(adapter, pack_images::format(def.format), features) {
|
||||
let made = pack_images::makeable(device, def, packed_float).and_then(|()| {
|
||||
if gpu.storable(
|
||||
adapter,
|
||||
pack_images::format(def.format, packed_float),
|
||||
features,
|
||||
) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("this GPU can't store into its format".into())
|
||||
@@ -2133,7 +2275,7 @@ impl PackPost {
|
||||
));
|
||||
left_out.push(def);
|
||||
}
|
||||
let images = pack_images::images(image_defs);
|
||||
let images = pack_images::images(image_defs, packed_float);
|
||||
let mut objects = std::mem::take(&mut post.buffer_objects);
|
||||
if gpu.storage_stages().is_empty() {
|
||||
objects.clear();
|
||||
@@ -2288,6 +2430,7 @@ impl PackPost {
|
||||
cull: g.cull,
|
||||
});
|
||||
let split = post.terrain.is_some() && post.terrain_cutout.is_some();
|
||||
let (terrain_undrawn, water_undrawn) = (post.terrain_undrawn, post.water_undrawn);
|
||||
let mut terrain = post
|
||||
.terrain
|
||||
.zip(terrain_geometry.as_ref())
|
||||
@@ -2445,6 +2588,20 @@ impl PackPost {
|
||||
};
|
||||
build(p, Some(&g)).map(|pass| (pass, geometry))
|
||||
});
|
||||
// Translucent held items, drawn from the hand's own buffer.
|
||||
let hand_water = post.hand_water.and_then(|p| {
|
||||
let (_, geometry) = hand.as_ref()?;
|
||||
let g = Geometry {
|
||||
buffers: &entity_buffers,
|
||||
table: Some(&geometry.layout),
|
||||
depth_write: true,
|
||||
atlas: world.celestial.clone(),
|
||||
material_maps: false,
|
||||
ids: None,
|
||||
cull: false,
|
||||
};
|
||||
build(p, Some(&g))
|
||||
});
|
||||
let weather = post
|
||||
.weather
|
||||
.zip(weather_geometry.as_ref())
|
||||
@@ -2565,6 +2722,9 @@ impl PackPost {
|
||||
if let Some((h, _)) = &hand {
|
||||
notes.push(format!("the hand draws with {}", h.name));
|
||||
}
|
||||
if let Some(h) = &hand_water {
|
||||
notes.push(format!("translucent held items draw with {}", h.name));
|
||||
}
|
||||
if let Some([w, _]) = &weather {
|
||||
notes.push(format!("rain and snow draw with {}", w.name));
|
||||
}
|
||||
@@ -2618,6 +2778,7 @@ impl PackPost {
|
||||
.chain(&lines)
|
||||
.chain(weather.iter().flatten())
|
||||
.chain(hand.iter().map(|h| &h.0))
|
||||
.chain(&hand_water)
|
||||
};
|
||||
let shadow_targets = shadow_formats.map(|f| {
|
||||
let colors = every_pass().fold(0u16, |m, p| {
|
||||
@@ -2671,6 +2832,7 @@ impl PackPost {
|
||||
.chain(&water)
|
||||
.chain(weather.iter().take(1).flatten())
|
||||
.chain(hand.iter().map(|h| &h.0))
|
||||
.chain(&hand_water)
|
||||
.find(touched)
|
||||
.map(|_| true)
|
||||
.or_else(|| passes.iter().find(touched).map(|_| true));
|
||||
@@ -2834,6 +2996,8 @@ impl PackPost {
|
||||
terrain,
|
||||
terrain_cutout,
|
||||
water,
|
||||
terrain_undrawn,
|
||||
water_undrawn,
|
||||
sky,
|
||||
entities,
|
||||
clouds,
|
||||
@@ -2848,6 +3012,7 @@ impl PackPost {
|
||||
weather_draw: None,
|
||||
weather_generation: 0,
|
||||
hand,
|
||||
hand_water,
|
||||
depth1,
|
||||
depth2,
|
||||
shadow_targets,
|
||||
@@ -2888,7 +3053,7 @@ impl PackPost {
|
||||
targets: None,
|
||||
noise: pack_textures::noise(&textures)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| noise(device, queue)),
|
||||
.unwrap_or_else(|| noise(device, queue, noise_size)),
|
||||
dummy: texture(device, queue, "Shader pack stand-in", 1, &white),
|
||||
dummy_black: texture(device, queue, "Shader pack black stand-in", 1, &black),
|
||||
dummy_normal: texture(
|
||||
@@ -3478,6 +3643,16 @@ impl PackPost {
|
||||
open.as_mut().expect("open").1 = i;
|
||||
}
|
||||
let rp = &mut open.as_mut().expect("open").0;
|
||||
// Each part with vanilla's blend for it: one program, three pipelines.
|
||||
let part_pipeline =
|
||||
pass.sky_parts
|
||||
.as_ref()
|
||||
.and_then(|[plain, stars]| match part.stage {
|
||||
"SKY" | "VOID" => Some(plain),
|
||||
"STARS" => Some(stars),
|
||||
_ => None,
|
||||
});
|
||||
rp.set_pipeline(part_pipeline.unwrap_or(pass.render()));
|
||||
if pass.draw {
|
||||
self.draws.bind(rp, part.stage);
|
||||
}
|
||||
@@ -3816,8 +3991,14 @@ impl PackPost {
|
||||
rp.set_bind_group(1, &geometry.group, &[]);
|
||||
rp.set_vertex_buffer(0, geometry.vertices());
|
||||
rp.set_index_buffer(geometry.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
// The arm and each held item: their own `currentRenderedItemId`.
|
||||
for &(first, quads, offset) in &geometry.runs {
|
||||
// The arm and each held item: their own `currentRenderedItemId`. Translucent
|
||||
// quads wait for `gbuffers_hand_water` when the pack has one.
|
||||
let late = self.hand_water.is_some();
|
||||
let runs = geometry
|
||||
.runs
|
||||
.iter()
|
||||
.chain(geometry.translucent_runs.iter().filter(|_| !late));
|
||||
for &(first, quads, offset) in runs {
|
||||
if pass.draw {
|
||||
rp.set_bind_group(2, &geometry.slot_group, &[offset]);
|
||||
}
|
||||
@@ -3944,9 +4125,53 @@ impl PackPost {
|
||||
terrain,
|
||||
);
|
||||
self.draw_entities(device, queue, encoder, size, true);
|
||||
self.draw_hand_water(device, queue, encoder, size);
|
||||
self.draw_weather(device, queue, encoder, size);
|
||||
}
|
||||
|
||||
/// The hand's translucent quads (uploaded by `draw_hand` this frame), with
|
||||
/// `gbuffers_hand_water`.
|
||||
fn draw_hand_water(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
size: [u32; 2],
|
||||
) {
|
||||
let Some(mut pass) = self.hand_water.take() else {
|
||||
return;
|
||||
};
|
||||
let hand = self.hand.take();
|
||||
if let Some((_, geometry)) = hand
|
||||
.as_ref()
|
||||
.filter(|(_, g)| g.quads > 0 && !g.translucent_runs.is_empty())
|
||||
{
|
||||
let group = self.bind(device, queue, encoder, &mut pass, size);
|
||||
{
|
||||
let mut rp = self.begin(encoder, &pass);
|
||||
rp.set_pipeline(pass.render());
|
||||
rp.set_bind_group(0, &group, &[]);
|
||||
if pass.buffers
|
||||
&& let Some(g) = &self.buffers.group
|
||||
{
|
||||
rp.set_bind_group(4, g, &[]);
|
||||
}
|
||||
rp.set_bind_group(1, &geometry.group, &[]);
|
||||
rp.set_vertex_buffer(0, geometry.vertices());
|
||||
rp.set_index_buffer(geometry.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
for &(first, quads, offset) in &geometry.translucent_runs {
|
||||
if pass.draw {
|
||||
rp.set_bind_group(2, &geometry.slot_group, &[offset]);
|
||||
}
|
||||
rp.draw_indexed(first * 6..(first + quads) * 6, 0, 0..1);
|
||||
}
|
||||
}
|
||||
self.mips_valid &= !pass.written;
|
||||
}
|
||||
self.hand = hand;
|
||||
self.hand_water = Some(pass);
|
||||
}
|
||||
|
||||
/// The rain, then the snow, `set_weather` gave for this frame, with `gbuffers_weather`.
|
||||
fn draw_weather(
|
||||
&mut self,
|
||||
@@ -4061,9 +4286,9 @@ impl PackPost {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the pack draws terrain itself.
|
||||
/// Whether the pack draws terrain itself, or has nothing draw it.
|
||||
pub fn has_terrain(&self) -> bool {
|
||||
self.terrain.is_some() || self.terrain_cutout.is_some()
|
||||
self.terrain.is_some() || self.terrain_cutout.is_some() || self.terrain_undrawn
|
||||
}
|
||||
|
||||
/// Whether the pack's shadow pass draws terrain (`shadow_view`).
|
||||
@@ -4091,7 +4316,7 @@ impl PackPost {
|
||||
|
||||
/// Whether the pack draws translucent terrain itself.
|
||||
pub fn has_water(&self) -> bool {
|
||||
self.water.is_some()
|
||||
self.water.is_some() || self.water_undrawn
|
||||
}
|
||||
|
||||
/// The names of the programs that run, in order.
|
||||
@@ -4122,6 +4347,7 @@ impl PackPost {
|
||||
.chain(&mut self.lines)
|
||||
.chain(self.weather.iter_mut().flatten())
|
||||
.chain(self.hand.iter_mut().map(|h| &mut h.0))
|
||||
.chain(&mut self.hand_water)
|
||||
.chain(&mut self.shadow_terrain)
|
||||
.chain(&mut self.shadow_water)
|
||||
.chain(&mut self.shadow_comp);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
//! The sun and moon blend as vanilla's `RenderPipelines.CELESTIAL` (`BlendFunction.OVERLAY`):
|
||||
//! their colour is added to the sky's, and their own alpha replaces the buffer's. Iris 1.11.7
|
||||
//! left the sun's alpha in a buffer the sky had written first, with a probe pack of ours
|
||||
//! (#10490). Original fixture, no pack files.
|
||||
use super::depth_test::{Gpu, SIZE, VERTEX, fixture_frame};
|
||||
use super::*;
|
||||
use crate::hud::HudBatch;
|
||||
use wgpu::TextureFormat as T;
|
||||
|
||||
/// The sky writes alpha 1 into colortex1, then the sun (default blend) writes alpha 0.5 there;
|
||||
/// final shows colortex1's alpha in red.
|
||||
fn files() -> Vec<(String, String)> {
|
||||
let program = |alpha: &str| {
|
||||
format!(
|
||||
"#version 120\n/* DRAWBUFFERS:1 */\n\
|
||||
void main() {{ gl_FragData[0] = vec4(0.0, 0.0, 0.0, {alpha}); }}\n"
|
||||
)
|
||||
};
|
||||
let vertex = "#version 120\nvoid main() { gl_Position = ftransform(); }\n";
|
||||
vec![
|
||||
("gbuffers_skybasic.vsh".into(), vertex.into()),
|
||||
("gbuffers_skybasic.fsh".into(), program("1.0")),
|
||||
("gbuffers_skytextured.vsh".into(), vertex.into()),
|
||||
("gbuffers_skytextured.fsh".into(), program("0.5")),
|
||||
("final.vsh".into(), VERTEX.into()),
|
||||
(
|
||||
"final.fsh".into(),
|
||||
"#version 120\nuniform sampler2D colortex1;\nvarying vec2 uv;\n\
|
||||
void main() { gl_FragColor = vec4(texture2D(colortex1, uv).a, 0.0, 0.0, 1.0); }\n"
|
||||
.into(),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// A quad across the whole view, `z` blocks in front of the camera.
|
||||
fn quad(z: f32) -> [crate::mesher::Vertex; 4] {
|
||||
[
|
||||
[-400.0, -400.0],
|
||||
[400.0, -400.0],
|
||||
[400.0, 400.0],
|
||||
[-400.0, 400.0],
|
||||
]
|
||||
.map(|[x, y]| crate::mesher::Vertex {
|
||||
pos: [x, y, z],
|
||||
uv: [0.0; 2],
|
||||
layer: 0,
|
||||
color: u32::MAX,
|
||||
light: 0,
|
||||
normal: [0; 4],
|
||||
glint_uv: [0.0; 2],
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_sun_leaves_its_own_alpha_over_the_sky() {
|
||||
let Some(gpu) = Gpu::new() else { return };
|
||||
let color = gpu.texture(T::Rgba8Unorm);
|
||||
let depth = gpu.texture(T::Depth32Float);
|
||||
// The world drew nothing: the far plane (reverse-Z) everywhere.
|
||||
gpu.clear(&color, &depth, 0.0);
|
||||
let sun = gpu.texture(T::Rgba8Unorm);
|
||||
let mut post = gpu.load(
|
||||
files(),
|
||||
World {
|
||||
atlas: None,
|
||||
material_maps: None,
|
||||
lightmap: color.clone(),
|
||||
terrain: None,
|
||||
block_ids: &[],
|
||||
sprite_rects: &[],
|
||||
celestial: Some(sun),
|
||||
gui_size: SIZE,
|
||||
clouds: None,
|
||||
weather: false,
|
||||
hand: false,
|
||||
},
|
||||
horizon_shaders::GameInputs {
|
||||
sky: Some(super::super::pack_sky::inputs()),
|
||||
celestial: Some(super::super::pack_sky::celestial_inputs()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(post.sky.is_some(), "{:?}", post.notes);
|
||||
let hud = HudBatch::new(SIZE);
|
||||
let mut frame = fixture_frame(&hud);
|
||||
frame.title = false;
|
||||
frame.skybox = true;
|
||||
let sun = quad(100.0);
|
||||
frame.celestial = &sun;
|
||||
let pixels = gpu.render(&mut post, &color, &depth, &frame, &mut |_, _, _, _| {});
|
||||
// The top four rows see the sky disc (sky_depth_tests); the sun covers the whole view.
|
||||
for (i, p) in pixels[..4 * SIZE[0] as usize].iter().enumerate() {
|
||||
assert!(
|
||||
p[0].abs_diff(128) <= 1,
|
||||
"pixel {i}: colortex1 alpha {} / 255, expected the sun's 0.5",
|
||||
p[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! A program writing a float `vec4` into an integer buffer draws: its other buffers get what it
|
||||
//! wrote, and the integer buffer the floats' bits, as Iris 1.11.7 on NVIDIA 615.78.08 did with
|
||||
//! a probe pack of ours (#10490). Original fixture, no pack files.
|
||||
use super::depth_test::{Gpu, SIZE, VERTEX, fixture_frame};
|
||||
use super::*;
|
||||
use crate::hud::HudBatch;
|
||||
use wgpu::TextureFormat as T;
|
||||
|
||||
#[test]
|
||||
fn a_float_output_into_an_integer_buffer_writes_its_bits() {
|
||||
let Some(gpu) = Gpu::new() else { return };
|
||||
let files = vec![
|
||||
("composite.vsh".into(), VERTEX.into()),
|
||||
(
|
||||
"composite.fsh".into(),
|
||||
"#version 130\n/*\nconst int colortex3Format = RGBA32UI;\n*/\n/* DRAWBUFFERS:03 */\n\
|
||||
void main() {\n\
|
||||
\x20 gl_FragData[0] = vec4(0.25, 0.5, 0.75, 1.0);\n\
|
||||
\x20 gl_FragData[1] = vec4(1.5, 2.0, 7.0, 0.25);\n}\n"
|
||||
.into(),
|
||||
),
|
||||
("final.vsh".into(), VERTEX.into()),
|
||||
(
|
||||
"final.fsh".into(),
|
||||
"#version 130\nuniform sampler2D colortex0;\nuniform usampler2D colortex3;\n\
|
||||
varying vec2 uv;\n\
|
||||
void main() {\n\
|
||||
\x20 uvec4 q = texelFetch(colortex3, ivec2(gl_FragCoord.xy), 0);\n\
|
||||
\x20 bvec4 bits = equal(q, uvec4(0x3FC00000u, 0x40000000u, 0x40E00000u, 0x3E800000u));\n\
|
||||
\x20 gl_FragColor = uv.y > 0.5 ? texture2D(colortex0, uv)\n\
|
||||
\x20 : vec4(float(bits.x && bits.y), float(bits.z), float(bits.w), 1.0);\n}\n"
|
||||
.into(),
|
||||
),
|
||||
];
|
||||
let color = gpu.texture(T::Rgba8Unorm);
|
||||
let depth = gpu.texture(T::Depth32Float);
|
||||
gpu.clear(&color, &depth, 0.0);
|
||||
let mut post = gpu.load(
|
||||
files,
|
||||
World {
|
||||
atlas: None,
|
||||
material_maps: None,
|
||||
lightmap: color.clone(),
|
||||
terrain: None,
|
||||
block_ids: &[],
|
||||
sprite_rects: &[],
|
||||
celestial: None,
|
||||
gui_size: SIZE,
|
||||
clouds: None,
|
||||
weather: false,
|
||||
hand: false,
|
||||
},
|
||||
horizon_shaders::GameInputs::default(),
|
||||
);
|
||||
let hud = HudBatch::new(SIZE);
|
||||
let mut frame = fixture_frame(&hud);
|
||||
frame.title = false;
|
||||
let pixels = gpu.render(&mut post, &color, &depth, &frame, &mut |_, _, _, _| {});
|
||||
let width = SIZE[0] as usize;
|
||||
let (top, bottom) = (pixels[0], pixels[(SIZE[1] as usize - 1) * width]);
|
||||
assert!(
|
||||
top[0].abs_diff(64) <= 1 && top[1].abs_diff(128) <= 1 && top[2].abs_diff(191) <= 1,
|
||||
"colortex0 {top:?}, notes {:?}",
|
||||
post.notes
|
||||
);
|
||||
assert_eq!(bottom, [255, 255, 255, 255], "colortex3's bits");
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//! `noiseTextureResolution` sizes the `noisetex` Horizon makes, as Iris 1.11.7 did with probe packs
|
||||
//! of ours (#10490): a declaration in a fragment shader counts, one in a vertex shader doesn't.
|
||||
//! Original fixture, no pack files.
|
||||
use super::depth_test::{Gpu, SIZE, VERTEX, fixture_frame};
|
||||
use super::*;
|
||||
use crate::hud::HudBatch;
|
||||
use wgpu::TextureFormat as T;
|
||||
|
||||
/// The width of `noisetex` that `final` sees, with these lines added to `final.vsh` and `final.fsh`.
|
||||
fn noise_width(vertex_extra: &str, fragment_extra: &str) -> u8 {
|
||||
let Some(gpu) = Gpu::new() else { return 0 };
|
||||
let vertex = VERTEX.replacen('\n', &format!("\n{vertex_extra}"), 1);
|
||||
let files = vec![
|
||||
("final.vsh".into(), vertex),
|
||||
(
|
||||
"final.fsh".into(),
|
||||
format!(
|
||||
"#version 130\n{fragment_extra}uniform sampler2D noisetex;\nvarying vec2 uv;\n\
|
||||
void main() {{ gl_FragColor = vec4(float(textureSize(noisetex, 0).x) / 255.0, 0.0, 0.0, 1.0); }}\n"
|
||||
),
|
||||
),
|
||||
];
|
||||
let color = gpu.texture(T::Rgba8Unorm);
|
||||
let depth = gpu.texture(T::Depth32Float);
|
||||
gpu.clear(&color, &depth, 0.0);
|
||||
let mut post = gpu.load(
|
||||
files,
|
||||
World {
|
||||
atlas: None,
|
||||
material_maps: None,
|
||||
lightmap: color.clone(),
|
||||
terrain: None,
|
||||
block_ids: &[],
|
||||
sprite_rects: &[],
|
||||
celestial: None,
|
||||
gui_size: SIZE,
|
||||
clouds: None,
|
||||
weather: false,
|
||||
hand: false,
|
||||
},
|
||||
horizon_shaders::GameInputs::default(),
|
||||
);
|
||||
let hud = HudBatch::new(SIZE);
|
||||
let mut frame = fixture_frame(&hud);
|
||||
frame.title = false;
|
||||
let pixels = gpu.render(&mut post, &color, &depth, &frame, &mut |_, _, _, _| {});
|
||||
assert!(post.notes.is_empty(), "{:?}", post.notes);
|
||||
pixels[0][0]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fragment_shader_sets_the_noise_resolution() {
|
||||
if Gpu::new().is_none() {
|
||||
return;
|
||||
}
|
||||
let declared = "#define NOISE 64\nconst int noiseTextureResolution = NOISE;\n";
|
||||
assert_eq!(noise_width("", declared), 64);
|
||||
// Not from a vertex shader: the default 256, read as 255 (1.0).
|
||||
assert_eq!(
|
||||
noise_width("const int noiseTextureResolution = 64;\n", ""),
|
||||
255
|
||||
);
|
||||
}
|
||||
@@ -180,6 +180,124 @@ pub(super) fn quantise_outputs(module: &mut naga::Module, outputs: &[(u32, [u8;
|
||||
f.body = out;
|
||||
}
|
||||
|
||||
/// A program's `vec4` float output into an integer buffer (`RGBA32UI`...) writes the float's
|
||||
/// bits there, and the program draws its other buffers. Horizon refused such a program, since
|
||||
/// wgpu has no float output into an integer target. Iris 1.11.7 drew it, with a probe pack of
|
||||
/// ours (#10490): its other buffers kept every bit written, NaN payloads included, and the
|
||||
/// integer buffer read the floats' bits back (1.5 as 0x3FC00000). GL leaves the integer
|
||||
/// buffer's value undefined, so those bits are what this driver (NVIDIA 615.78.08) wrote.
|
||||
pub(super) fn bitcast_outputs(module: &mut naga::Module, outputs: &[(u32, naga::ScalarKind)]) {
|
||||
let Some(ep) =
|
||||
(module.entry_points.iter_mut()).find(|e| e.stage == naga::ShaderStage::Fragment)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(result) = &ep.function.result else {
|
||||
return;
|
||||
};
|
||||
let naga::TypeInner::Struct { members, span } = module.types[result.ty].inner.clone() else {
|
||||
return;
|
||||
};
|
||||
let mut new_members = members.clone();
|
||||
// (member, integer kind) of each output that changes.
|
||||
let mut changed = Vec::new();
|
||||
for (i, m) in members.iter().enumerate() {
|
||||
let Some(naga::Binding::Location { location, .. }) = m.binding else {
|
||||
continue;
|
||||
};
|
||||
let Some(&(_, kind)) = outputs.iter().find(|o| o.0 == location) else {
|
||||
continue;
|
||||
};
|
||||
if !matches!(
|
||||
module.types[m.ty].inner,
|
||||
naga::TypeInner::Vector {
|
||||
size: VectorSize::Quad,
|
||||
scalar: naga::Scalar::F32
|
||||
}
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let ty = module.types.insert(
|
||||
naga::Type {
|
||||
name: None,
|
||||
inner: naga::TypeInner::Vector {
|
||||
size: VectorSize::Quad,
|
||||
scalar: naga::Scalar { kind, width: 4 },
|
||||
},
|
||||
},
|
||||
naga::Span::UNDEFINED,
|
||||
);
|
||||
new_members[i].ty = ty;
|
||||
if let Some(naga::Binding::Location {
|
||||
interpolation,
|
||||
sampling,
|
||||
..
|
||||
}) = &mut new_members[i].binding
|
||||
{
|
||||
*interpolation = Some(naga::Interpolation::Flat);
|
||||
*sampling = None;
|
||||
}
|
||||
changed.push((i, kind));
|
||||
}
|
||||
if changed.is_empty() {
|
||||
return;
|
||||
}
|
||||
let old_name = module.types[result.ty].name.clone();
|
||||
let struct_ty = module.types.insert(
|
||||
naga::Type {
|
||||
name: old_name.map(|n| n + "_bits"),
|
||||
inner: naga::TypeInner::Struct {
|
||||
members: new_members,
|
||||
span,
|
||||
},
|
||||
},
|
||||
naga::Span::UNDEFINED,
|
||||
);
|
||||
let f = &mut ep.function;
|
||||
if let Some(r) = &mut f.result {
|
||||
r.ty = struct_ty;
|
||||
}
|
||||
let body = std::mem::take(&mut f.body);
|
||||
let mut out = naga::Block::with_capacity(body.len() + 1);
|
||||
for (statement, sp) in body.span_into_iter() {
|
||||
let Statement::Return { value: Some(value) } = statement else {
|
||||
out.push(statement, sp);
|
||||
continue;
|
||||
};
|
||||
let Expression::Compose { components, .. } = &f.expressions[value] else {
|
||||
out.push(statement, sp);
|
||||
continue;
|
||||
};
|
||||
let mut components = components.clone();
|
||||
let mut first = None;
|
||||
for &(member, kind) in &changed {
|
||||
let e = f.expressions.append(
|
||||
Expression::As {
|
||||
expr: components[member],
|
||||
kind,
|
||||
convert: None,
|
||||
},
|
||||
sp,
|
||||
);
|
||||
first.get_or_insert(e);
|
||||
components[member] = e;
|
||||
}
|
||||
let value = f.expressions.append(
|
||||
Expression::Compose {
|
||||
ty: struct_ty,
|
||||
components,
|
||||
},
|
||||
sp,
|
||||
);
|
||||
out.push(
|
||||
Statement::Emit(naga::Range::new_from_bounds(first.unwrap_or(value), value)),
|
||||
sp,
|
||||
);
|
||||
out.push(Statement::Return { value: Some(value) }, sp);
|
||||
}
|
||||
f.body = out;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
//! An `R11F_G11F_B10F` image that a compute program declares `layout(r11f_g11f_b10f)` is stored
|
||||
//! into and read back, as Iris 1.11.7 did with a probe pack of ours (#10490): its program is no
|
||||
//! longer refused when the image is the `RGBA16F` stand-in. Original fixture, no pack files.
|
||||
use super::depth_test::{Gpu, SIZE, VERTEX, fixture_frame};
|
||||
use super::*;
|
||||
use crate::hud::HudBatch;
|
||||
use wgpu::TextureFormat as T;
|
||||
|
||||
#[test]
|
||||
fn an_r11f_image_is_stored_into_and_read_back() {
|
||||
let Some(gpu) = Gpu::new() else { return };
|
||||
let files = vec![
|
||||
(
|
||||
"shaders.properties".into(),
|
||||
"image.imgA = texA RGBA R11F_G11F_B10F FLOAT false false 4 4\n".into(),
|
||||
),
|
||||
(
|
||||
"composite.csh".into(),
|
||||
"#version 430\nlayout(local_size_x = 1) in;\nconst ivec3 workGroups = ivec3(1, 1, 1);\n\
|
||||
layout(r11f_g11f_b10f) writeonly uniform image2D imgA;\n\
|
||||
void main() { imageStore(imgA, ivec2(1, 1), vec4(0.25, 1.5, 3.0, 1.0)); }\n"
|
||||
.into(),
|
||||
),
|
||||
("final.vsh".into(), VERTEX.into()),
|
||||
(
|
||||
"final.fsh".into(),
|
||||
"#version 130\nuniform sampler2D texA;\nvarying vec2 uv;\n\
|
||||
void main() { gl_FragColor = vec4(texelFetch(texA, ivec2(1, 1), 0).rgb / 4.0, 1.0); }\n"
|
||||
.into(),
|
||||
),
|
||||
];
|
||||
let color = gpu.texture(T::Rgba8Unorm);
|
||||
let depth = gpu.texture(T::Depth32Float);
|
||||
gpu.clear(&color, &depth, 0.0);
|
||||
let mut post = gpu.load(
|
||||
files,
|
||||
World {
|
||||
atlas: None,
|
||||
material_maps: None,
|
||||
lightmap: color.clone(),
|
||||
terrain: None,
|
||||
block_ids: &[],
|
||||
sprite_rects: &[],
|
||||
celestial: None,
|
||||
gui_size: SIZE,
|
||||
clouds: None,
|
||||
weather: false,
|
||||
hand: false,
|
||||
},
|
||||
horizon_shaders::GameInputs::default(),
|
||||
);
|
||||
let hud = HudBatch::new(SIZE);
|
||||
let mut frame = fixture_frame(&hud);
|
||||
frame.title = false;
|
||||
let pixels = gpu.render(&mut post, &color, &depth, &frame, &mut |_, _, _, _| {});
|
||||
// (0.25, 1.5, 3.0) / 4 = (0.0625, 0.375, 0.75).
|
||||
let p = pixels[0];
|
||||
assert!(
|
||||
p[0].abs_diff(16) <= 1 && p[1].abs_diff(96) <= 1 && p[2].abs_diff(191) <= 1,
|
||||
"{p:?}, notes {:?}",
|
||||
post.notes
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//! `.length()` of a runtime-sized array in a named buffer block reads the buffer's own length, as
|
||||
//! Iris 1.11.7 did with a probe pack of ours (#10490): `bufferObject.0 = 32` holds 8 floats. The
|
||||
//! program was refused before. Original fixture, no pack files.
|
||||
use super::depth_test::{Gpu, SIZE, VERTEX, fixture_frame};
|
||||
use super::*;
|
||||
use crate::hud::HudBatch;
|
||||
use wgpu::TextureFormat as T;
|
||||
|
||||
#[test]
|
||||
fn a_runtime_arrays_length_is_the_buffers() {
|
||||
let Some(gpu) = Gpu::new() else { return };
|
||||
let files = vec![
|
||||
("shaders.properties".into(), "bufferObject.0 = 32\n".into()),
|
||||
("final.vsh".into(), VERTEX.into()),
|
||||
(
|
||||
"final.fsh".into(),
|
||||
"#version 430 compatibility\nin vec2 uv;\n\
|
||||
layout(std430, binding = 0) buffer Values { float v[]; } values;\n\
|
||||
void main() { gl_FragColor = vec4(float(values.v.length()) / 32.0, 0.0, 0.0, 1.0); }\n"
|
||||
.into(),
|
||||
),
|
||||
];
|
||||
let color = gpu.texture(T::Rgba8Unorm);
|
||||
let depth = gpu.texture(T::Depth32Float);
|
||||
gpu.clear(&color, &depth, 0.0);
|
||||
let mut post = gpu.load(
|
||||
files,
|
||||
World {
|
||||
atlas: None,
|
||||
material_maps: None,
|
||||
lightmap: color.clone(),
|
||||
terrain: None,
|
||||
block_ids: &[],
|
||||
sprite_rects: &[],
|
||||
celestial: None,
|
||||
gui_size: SIZE,
|
||||
clouds: None,
|
||||
weather: false,
|
||||
hand: false,
|
||||
},
|
||||
horizon_shaders::GameInputs::default(),
|
||||
);
|
||||
let hud = HudBatch::new(SIZE);
|
||||
let mut frame = fixture_frame(&hud);
|
||||
frame.title = false;
|
||||
let pixels = gpu.render(&mut post, &color, &depth, &frame, &mut |_, _, _, _| {});
|
||||
// 32 bytes of floats: 8 entries, 8 / 32 = 0.25.
|
||||
let p = pixels[0];
|
||||
assert!(p[0].abs_diff(64) <= 1, "{p:?}, notes {:?}", post.notes);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//! The sky disc draws unblended, as vanilla draws it (`RenderPipelines.SKY`): what
|
||||
//! `gbuffers_skybasic` writes there stays, alpha included. Iris 1.11.7 left exactly that with
|
||||
//! probe packs of ours (#10490). Original fixture, no pack files.
|
||||
use super::depth_test::{Gpu, SIZE, VERTEX, fixture_frame};
|
||||
use super::*;
|
||||
use crate::hud::HudBatch;
|
||||
use wgpu::TextureFormat as T;
|
||||
|
||||
/// `gbuffers_skybasic` writes (0.2, 0.4, 0.6, 0.25) into colortex1, with `blend` as the
|
||||
/// properties line; final shows colortex1's colour on the top half and its alpha on the bottom
|
||||
/// half, each seeing the top of the view.
|
||||
fn files(blend: &str) -> Vec<(String, String)> {
|
||||
vec![
|
||||
("shaders.properties".into(), blend.into()),
|
||||
(
|
||||
"gbuffers_skybasic.vsh".into(),
|
||||
"#version 120\nvoid main() { gl_Position = ftransform(); }\n".into(),
|
||||
),
|
||||
(
|
||||
"gbuffers_skybasic.fsh".into(),
|
||||
"#version 120\n/* DRAWBUFFERS:1 */\n\
|
||||
void main() { gl_FragData[0] = vec4(0.2, 0.4, 0.6, 0.25); }\n"
|
||||
.into(),
|
||||
),
|
||||
("final.vsh".into(), VERTEX.into()),
|
||||
(
|
||||
"final.fsh".into(),
|
||||
"#version 120\nuniform sampler2D colortex1;\nvarying vec2 uv;\n\
|
||||
void main() {\n\
|
||||
\x20 vec4 c = texture2D(colortex1, vec2(uv.x, uv.y > 0.5 ? uv.y : uv.y + 0.5));\n\
|
||||
\x20 gl_FragColor = uv.y > 0.5 ? vec4(c.rgb, 1.0) : vec4(c.a, c.a, c.a, 1.0);\n}\n"
|
||||
.into(),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// The pack's picture of a frame whose world drew nothing.
|
||||
fn picture(gpu: &Gpu, blend: &str) -> Vec<[u8; 4]> {
|
||||
let color = gpu.texture(T::Rgba8Unorm);
|
||||
let depth = gpu.texture(T::Depth32Float);
|
||||
// The far plane everywhere (reverse-Z).
|
||||
gpu.clear(&color, &depth, 0.0);
|
||||
let mut post = gpu.load(
|
||||
files(blend),
|
||||
World {
|
||||
atlas: None,
|
||||
material_maps: None,
|
||||
lightmap: color.clone(),
|
||||
terrain: None,
|
||||
block_ids: &[],
|
||||
sprite_rects: &[],
|
||||
celestial: None,
|
||||
gui_size: SIZE,
|
||||
clouds: None,
|
||||
weather: false,
|
||||
hand: false,
|
||||
},
|
||||
horizon_shaders::GameInputs {
|
||||
sky: Some(super::super::pack_sky::inputs()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(post.sky.is_some(), "{:?}", post.notes);
|
||||
let hud = HudBatch::new(SIZE);
|
||||
let mut frame = fixture_frame(&hud);
|
||||
frame.title = false;
|
||||
frame.skybox = true;
|
||||
gpu.render(&mut post, &color, &depth, &frame, &mut |_, _, _, _| {})
|
||||
}
|
||||
|
||||
/// The top four rows see the sky disc (sky_depth_tests); final repeats them in rows 8 to 11.
|
||||
#[test]
|
||||
fn the_sky_disc_keeps_what_the_program_wrote() {
|
||||
let Some(gpu) = Gpu::new() else { return };
|
||||
let width = SIZE[0] as usize;
|
||||
let pixels = picture(&gpu, "");
|
||||
for (i, p) in pixels[..4 * width].iter().enumerate() {
|
||||
let near = |got: u8, want: u8| got.abs_diff(want) <= 1;
|
||||
assert!(
|
||||
near(p[0], 51) && near(p[1], 102) && near(p[2], 153),
|
||||
"pixel {i}: colour {p:?}, expected (0.2, 0.4, 0.6)"
|
||||
);
|
||||
}
|
||||
for (i, p) in pixels[8 * width..12 * width].iter().enumerate() {
|
||||
assert!(p[0].abs_diff(64) <= 1, "pixel {i}: alpha {} / 255", p[0]);
|
||||
}
|
||||
// A pack's own `blend.gbuffers_skybasic` still blends the disc: alpha over the cleared 1.
|
||||
let blended = picture(
|
||||
&gpu,
|
||||
"blend.gbuffers_skybasic = SRC_ALPHA ONE_MINUS_SRC_ALPHA ONE ZERO\n",
|
||||
);
|
||||
assert!(blended[0][0].abs_diff(51) > 8, "{:?}", blended[0]);
|
||||
// So does a `blend.gbuffers_skybasic.colortex1` of its own, into that buffer.
|
||||
let own = picture(
|
||||
&gpu,
|
||||
"blend.gbuffers_skybasic.colortex1 = SRC_ALPHA ONE_MINUS_SRC_ALPHA ONE ZERO\n",
|
||||
);
|
||||
assert!(own[0][0].abs_diff(51) > 8, "{:?}", own[0]);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
//! A translucent draw reads the preceding scene even when it writes the same color buffer.
|
||||
//! Original programs and independently calculated colors; no shader-pack assets.
|
||||
use super::depth_test::{Gpu, SIZE, VERTEX, fixture_frame};
|
||||
use super::*;
|
||||
use crate::hud::HudBatch;
|
||||
use wgpu::{TextureFormat as T, util::DeviceExt};
|
||||
|
||||
#[test]
|
||||
fn translucent_draws_sample_the_color_written_by_deferred() {
|
||||
let Some(gpu) = Gpu::new() else { return };
|
||||
let positions: [[f32; 3]; 6] = [
|
||||
[-1.0, -1.0, 0.0],
|
||||
[1.0, -1.0, 0.0],
|
||||
[1.0, 1.0, 0.0],
|
||||
[-1.0, -1.0, 0.0],
|
||||
[1.0, 1.0, 0.0],
|
||||
[-1.0, 1.0, 0.0],
|
||||
];
|
||||
let vertices = gpu
|
||||
.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Original translucent quad"),
|
||||
contents: bytemuck::cast_slice(&positions),
|
||||
usage: wgpu::BufferUsages::VERTEX,
|
||||
});
|
||||
const ATTRS: [wgpu::VertexAttribute; 1] = wgpu::vertex_attr_array![0=>Float32x3];
|
||||
let layouts = [Some(wgpu::VertexBufferLayout {
|
||||
array_stride: 12,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &ATTRS,
|
||||
})];
|
||||
// Odd and even numbers of deferred passes leave the current scene on different sides.
|
||||
// Exercise two buffer numbers and the legacy sampler aliases too.
|
||||
for (buffer, sampler) in [(4, "colortex4"), (7, "gaux4")] {
|
||||
for stages in [1, 2] {
|
||||
for blended in [false, true] {
|
||||
let mut files = vec![
|
||||
("deferred.vsh".into(), VERTEX.into()),
|
||||
(
|
||||
"deferred.fsh".into(),
|
||||
format!(
|
||||
"#version 120\n/* RENDERTARGETS: {buffer} */\nvoid main(){{gl_FragData[0]=vec4(0.25,0.5,0.75,1.0);}}"
|
||||
),
|
||||
),
|
||||
(
|
||||
"gbuffers_water.vsh".into(),
|
||||
"#version 120\nvoid main(){gl_Position=gl_Vertex;}".into(),
|
||||
),
|
||||
(
|
||||
"gbuffers_water.fsh".into(),
|
||||
format!(
|
||||
"#version 120\n/* RENDERTARGETS: {buffer} */\nuniform sampler2D {sampler};\nvoid main(){{gl_FragData[0]=vec4(texture2D({sampler},vec2(0.5)).rgb+vec3(0.125,0.0,0.0),0.5);}}"
|
||||
),
|
||||
),
|
||||
("final.vsh".into(), VERTEX.into()),
|
||||
(
|
||||
"final.fsh".into(),
|
||||
format!(
|
||||
"#version 120\nuniform sampler2D colortex{buffer};varying vec2 uv;\nvoid main(){{gl_FragColor=vec4(texture2D(colortex{buffer},uv).rgb,1.0);}}"
|
||||
),
|
||||
),
|
||||
(
|
||||
"shaders.properties".into(),
|
||||
format!(
|
||||
"blend.gbuffers_water={}\n",
|
||||
if blended {
|
||||
"SRC_ALPHA ONE_MINUS_SRC_ALPHA ONE ONE_MINUS_SRC_ALPHA"
|
||||
} else {
|
||||
"off"
|
||||
}
|
||||
),
|
||||
),
|
||||
];
|
||||
if stages == 2 {
|
||||
files.extend([
|
||||
("deferred1.vsh".into(), VERTEX.into()),
|
||||
("deferred1.fsh".into(), format!(
|
||||
"#version 120\n/* RENDERTARGETS: {buffer} */\nvoid main(){{gl_FragData[0]=vec4(0.5,0.75,0.25,1.0);}}"
|
||||
)),
|
||||
]);
|
||||
}
|
||||
let color = gpu.texture(T::Rgba8Unorm);
|
||||
let depth = gpu.texture(T::Depth32Float);
|
||||
gpu.clear(&color, &depth, 0.0);
|
||||
let mut post = gpu.load(
|
||||
files,
|
||||
World {
|
||||
atlas: None,
|
||||
material_maps: None,
|
||||
lightmap: color.clone(),
|
||||
terrain: Some(Geometry {
|
||||
buffers: &layouts,
|
||||
table: None,
|
||||
depth_write: true,
|
||||
atlas: None,
|
||||
material_maps: false,
|
||||
ids: None,
|
||||
cull: false,
|
||||
}),
|
||||
block_ids: &[],
|
||||
sprite_rects: &[],
|
||||
celestial: None,
|
||||
gui_size: SIZE,
|
||||
clouds: None,
|
||||
weather: false,
|
||||
hand: false,
|
||||
},
|
||||
horizon_shaders::GameInputs {
|
||||
terrain: Some(horizon_shaders::GbuffersInputs {
|
||||
declarations: "layout(location=0)in vec3 fixture_position;\n".into(),
|
||||
body: "hz_Vertex=vec4(fixture_position,1.0);\n".into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(post.has_water(), "{:?}", post.notes);
|
||||
let mut expected = if stages == 1 {
|
||||
[0.25f32, 0.5, 0.75]
|
||||
} else {
|
||||
[0.5, 0.75, 0.25]
|
||||
};
|
||||
expected[0] += if blended { 0.0625 } else { 0.125 };
|
||||
let expected = expected.map(|c| (c * 255.0).round() as u8);
|
||||
let hud = HudBatch::new(SIZE);
|
||||
for frame in 0..2 {
|
||||
let pixels = gpu.render(
|
||||
&mut post,
|
||||
&color,
|
||||
&depth,
|
||||
&fixture_frame(&hud),
|
||||
&mut |rp, list, _, _| {
|
||||
if list == TerrainList::Translucent {
|
||||
rp.set_vertex_buffer(0, vertices.slice(..));
|
||||
rp.draw(0..6, 0..1);
|
||||
}
|
||||
},
|
||||
);
|
||||
for p in pixels {
|
||||
assert!(
|
||||
p[..3]
|
||||
.iter()
|
||||
.zip(expected)
|
||||
.all(|(&a, b)| a.abs_diff(b) <= 2),
|
||||
"buffer {buffer}, {stages} deferred stages, blended {blended}, frame {frame}: {p:?}, expected {expected:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//! A pack with no program for terrain draws none (#10455): Iris 1.11.7 drew no terrain, solid
|
||||
//! or translucent, into any buffer or depth with probe packs of ours having no gbuffers
|
||||
//! programs. The game hands the pack its terrain, so it doesn't draw it either. The programs
|
||||
//! here are original and cut down.
|
||||
use super::destination_tests::Fixture;
|
||||
use std::io::{Cursor, Write};
|
||||
|
||||
const VERTEX: &str = "#version 120\nvoid main() { gl_Position = ftransform(); }\n";
|
||||
const WHITE: &str =
|
||||
"#version 120\n/* DRAWBUFFERS:0 */\nvoid main() { gl_FragData[0] = vec4(1.0); }\n";
|
||||
|
||||
/// A pack with `programs` (each drawing white into colortex0) loaded for a game that can hand it
|
||||
/// terrain: whether it takes solid and translucent terrain from the game.
|
||||
fn takes_terrain(programs: &[&str]) -> Option<(bool, bool)> {
|
||||
let gpu = Fixture::new()?;
|
||||
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
for name in programs {
|
||||
for (ext, source) in [("vsh", VERTEX), ("fsh", WHITE)] {
|
||||
zip.start_file(
|
||||
format!("shaders/{name}.{ext}"),
|
||||
zip::write::SimpleFileOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
zip.write_all(source.as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
let pack = horizon_shaders::Pack::from_zip_bytes(
|
||||
"undrawn-terrain",
|
||||
zip.finish().unwrap().into_inner(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut macros = horizon_shaders::Macros::standard();
|
||||
let (properties, _) = horizon_shaders::read_properties(&pack, &mut macros);
|
||||
let inputs = horizon_shaders::GameInputs {
|
||||
terrain: Some(Default::default()),
|
||||
..Default::default()
|
||||
};
|
||||
let post = horizon_shaders::load_post(&pack, "", &properties, ¯os, &inputs);
|
||||
let post = gpu.try_build(post).ok()?;
|
||||
Some((post.has_terrain(), post.has_water()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pack_without_terrain_programs_takes_terrain_from_the_game() {
|
||||
let Some(taken) = takes_terrain(&["composite"]) else {
|
||||
return;
|
||||
};
|
||||
assert_eq!(taken, (true, true));
|
||||
}
|
||||
@@ -94,6 +94,31 @@ pub struct PackInputs {
|
||||
/// `frameCounter`) instead of the wall clock and the frames drawn, and smoothed values
|
||||
/// (`wetness`, `eyeBrightnessSmooth`, `centerDepthSmooth`) at their targets.
|
||||
pub still_ticks: Option<u64>,
|
||||
/// A scripted clock (`screenshot-sequence`): taken before `still_ticks` and the wall
|
||||
/// clock, and smoothed values move by its `delta`.
|
||||
pub frame_timing: Option<FrameTiming>,
|
||||
}
|
||||
|
||||
/// One frame of a scripted pack clock: the frame's number from 1, the seconds since frame 1,
|
||||
/// and the seconds since the frame before (0 on frame 1).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct FrameTiming {
|
||||
pub counter: u32,
|
||||
pub elapsed: f32,
|
||||
pub delta: f32,
|
||||
}
|
||||
|
||||
impl FrameTiming {
|
||||
/// Frame `n` (from 1) of frames `step_ms` milliseconds apart (`16.666667` for 60 fps). The
|
||||
/// elapsed time is worked out in f64, so frame 10 000 is as exact as frame 2.
|
||||
pub fn nth(n: u32, step_ms: f64) -> Self {
|
||||
let step = step_ms / 1000.0;
|
||||
Self {
|
||||
counter: n,
|
||||
elapsed: (f64::from(n.saturating_sub(1)) * step) as f32,
|
||||
delta: if n <= 1 { 0.0 } else { step as f32 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `cloudHeight` uniform: the dimension's `visual/cloud_height`
|
||||
@@ -349,8 +374,10 @@ pub struct FrameUniforms {
|
||||
start: std::time::Instant,
|
||||
last: Option<std::time::Instant>,
|
||||
frame: u32,
|
||||
/// The last frame's `PackInputs::still_ticks`, for `eval_custom`.
|
||||
still_ticks: Option<u64>,
|
||||
/// The tick `eval_custom` restarts the custom uniforms at: every frame's under STILL; on a
|
||||
/// scripted clock's frame 1 only 0 (where a freshly parsed pack starts), so its later frames
|
||||
/// carry their smoothing and randoms.
|
||||
custom_restart: Option<u64>,
|
||||
previous: Option<(glam::DVec3, Mat4, Mat4)>,
|
||||
wetness: f32,
|
||||
eye_smooth: [f32; 2],
|
||||
@@ -369,7 +396,7 @@ impl Default for FrameUniforms {
|
||||
start: std::time::Instant::now(),
|
||||
last: None,
|
||||
frame: 0,
|
||||
still_ticks: None,
|
||||
custom_restart: None,
|
||||
previous: None,
|
||||
wetness: 0.0,
|
||||
eye_smooth: [0.0; 2],
|
||||
@@ -687,9 +714,16 @@ impl FrameUniforms {
|
||||
let now = std::time::Instant::now();
|
||||
let measured = self.last.map_or(0.0, |l| (now - l).as_secs_f32());
|
||||
self.last = Some(now);
|
||||
self.still_ticks = inputs.still_ticks;
|
||||
self.custom_restart = match inputs.frame_timing {
|
||||
Some(timing) => (timing.counter <= 1).then_some(0),
|
||||
None => inputs.still_ticks,
|
||||
};
|
||||
// Smoothing over an endless step lands on its target.
|
||||
let dt = inputs.still_ticks.map_or(measured, |_| f32::INFINITY);
|
||||
let dt = match (inputs.frame_timing, inputs.still_ticks) {
|
||||
(Some(timing), _) => timing.delta,
|
||||
(None, Some(_)) => f32::INFINITY,
|
||||
(None, None) => measured,
|
||||
};
|
||||
let aspect = size[0] as f32 / size[1].max(1) as f32;
|
||||
// Packs take gbufferProjection for a plain perspective (they invert only its perspective
|
||||
// terms): the view bob and the nausea spin belong to the model-view, as GL drew them.
|
||||
@@ -781,13 +815,14 @@ impl FrameUniforms {
|
||||
let (shadow_view, shadow_view_inverse) = shadow_model_view(frame, &self.shadow);
|
||||
let day_time = inputs.day_time.rem_euclid(24000) as i32;
|
||||
let world_day = inputs.day_time.div_euclid(24000);
|
||||
let (seconds, frame_time, frame_counter) = match inputs.still_ticks {
|
||||
Some(ticks) => (
|
||||
let (seconds, frame_time, frame_counter) = match (inputs.frame_timing, inputs.still_ticks) {
|
||||
(Some(timing), _) => (timing.elapsed, timing.delta, timing.counter as i32),
|
||||
(None, Some(ticks)) => (
|
||||
(ticks as f32 / 20.0) % 3600.0,
|
||||
0.05,
|
||||
(ticks % 720_720) as i32,
|
||||
),
|
||||
None => (
|
||||
(None, None) => (
|
||||
(now - self.start).as_secs_f32() % 3600.0,
|
||||
measured,
|
||||
self.frame as i32,
|
||||
@@ -929,7 +964,7 @@ impl FrameUniforms {
|
||||
/// Evaluates the pack's custom uniforms with this frame's values and puts them after
|
||||
/// `NAMES`' values, in `custom.uniforms()` order.
|
||||
pub fn eval_custom(&mut self, custom: &mut horizon_shaders::Custom) {
|
||||
if let Some(tick) = self.still_ticks {
|
||||
if let Some(tick) = self.custom_restart {
|
||||
custom.restart_at(tick);
|
||||
}
|
||||
self.values.truncate(NAMES.len());
|
||||
@@ -1298,7 +1333,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod initial_history_tests {
|
||||
pub(super) mod initial_history_tests {
|
||||
use super::*;
|
||||
use crate::hud::HudBatch;
|
||||
fn float(u: &FrameUniforms, name: &str) -> [f32; 16] {
|
||||
@@ -2193,6 +2228,145 @@ mod initial_history_tests {
|
||||
assert_eq!(float(&u, "centerDepthSmooth")[0], 0.0);
|
||||
}
|
||||
|
||||
/// A scripted clock's values reach the pack as they are, before STILL's ticks.
|
||||
#[test]
|
||||
fn a_scripted_clock_comes_before_the_still_ticks() {
|
||||
let hud = HudBatch::new([800, 450]);
|
||||
let f = frame(&hud);
|
||||
for still_ticks in [None, Some(47_811)] {
|
||||
let mut u = FrameUniforms::default();
|
||||
for (n, elapsed, delta) in [(1, 0.0, 0.0), (64, 3.15, 0.05)] {
|
||||
let input = PackInputs {
|
||||
day_time: 6_001,
|
||||
still_ticks,
|
||||
frame_timing: Some(FrameTiming {
|
||||
counter: n,
|
||||
elapsed,
|
||||
delta,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
u.update(&f, &input, [800, 450]);
|
||||
match u.values[index("frameCounter").unwrap()] {
|
||||
Value::I(v) => assert_eq!(v[0], n as i32),
|
||||
_ => panic!("integer uniform expected"),
|
||||
}
|
||||
assert_eq!(float(&u, "frameTimeCounter")[0], elapsed);
|
||||
assert_eq!(float(&u, "frameTime")[0], delta);
|
||||
match u.values[index("worldTime").unwrap()] {
|
||||
Value::I(v) => assert_eq!(v[0], 6_001),
|
||||
_ => panic!("integer uniform expected"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Smoothed values move by a scripted clock's delta, also under STILL, which would
|
||||
/// otherwise put them at their targets.
|
||||
#[test]
|
||||
fn a_scripted_clock_moves_smoothed_values_by_its_delta() {
|
||||
let hud = HudBatch::new([800, 450]);
|
||||
let f = frame(&hud);
|
||||
let mut u = FrameUniforms::default();
|
||||
let mut input = PackInputs {
|
||||
rain: 1.0,
|
||||
eye_light: [0, 15],
|
||||
still_ticks: Some(47_811),
|
||||
frame_timing: Some(FrameTiming::nth(1, 1000.0)),
|
||||
..Default::default()
|
||||
};
|
||||
u.center_depth_raw = Some(1.0);
|
||||
u.update(&f, &input, [800, 450]);
|
||||
input.rain = 0.0;
|
||||
input.eye_light = [0, 0];
|
||||
u.center_depth_raw = Some(0.0);
|
||||
// A second each: eyeBrightnessSmooth halves (1 s half-life),
|
||||
// centerDepthSmooth halves ten times (0.1 s), wetness falls by 0.5^(1/20) (20 s).
|
||||
for (n, eye, depth, wetness) in [
|
||||
(2, 120, 1.0 / 1_024.0, 0.965_936_3),
|
||||
(3, 60, 1.0 / 1_048_576.0, 0.933_033),
|
||||
] {
|
||||
input.frame_timing = Some(FrameTiming::nth(n, 1000.0));
|
||||
u.update(&f, &input, [800, 450]);
|
||||
match u.values[index("eyeBrightnessSmooth").unwrap()] {
|
||||
Value::I(v) => assert_eq!(v[1], eye),
|
||||
_ => panic!("integer uniform expected"),
|
||||
}
|
||||
assert_eq!(float(&u, "centerDepthSmooth")[0], depth);
|
||||
assert!((float(&u, "wetness")[0] - wetness).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom uniforms start over on a scripted clock's frame 1 and carry their smoothing
|
||||
/// and randoms through the frames after it, so a sequence drawn again repeats.
|
||||
#[test]
|
||||
fn custom_uniforms_start_over_on_a_scripted_frame_1() {
|
||||
let properties = horizon_shaders::Properties::parse(
|
||||
"uniform.float.s = smooth(rainStrength, 10.0)
|
||||
uniform.float.r = random()
|
||||
",
|
||||
&horizon_shaders::Macros::standard(),
|
||||
);
|
||||
let mut custom = horizon_shaders::Custom::parse(&properties);
|
||||
let hud = HudBatch::new([800, 450]);
|
||||
let f = frame(&hud);
|
||||
let mut u = FrameUniforms::default();
|
||||
let mut run = |u: &mut FrameUniforms| -> Vec<[u32; 2]> {
|
||||
(1..=3)
|
||||
.map(|n| {
|
||||
let input = PackInputs {
|
||||
rain: if n == 1 { 0.0 } else { 1.0 },
|
||||
still_ticks: Some(47_811),
|
||||
frame_timing: Some(FrameTiming::nth(n, 1000.0)),
|
||||
..Default::default()
|
||||
};
|
||||
u.update(&f, &input, [800, 450]);
|
||||
u.eval_custom(&mut custom);
|
||||
std::array::from_fn(|i| match u.values[NAMES.len() + i] {
|
||||
Value::F(v) => v[0].to_bits(),
|
||||
_ => panic!("float uniform expected"),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let first = run(&mut u);
|
||||
// The smoothed rain moves toward 1 over frames 2 and 3; the randoms differ
|
||||
// each frame.
|
||||
let s: Vec<f32> = first.iter().map(|v| f32::from_bits(v[0])).collect();
|
||||
assert!(
|
||||
s[0] == 0.0 && s[0] < s[1] && s[1] < s[2] && s[2] < 1.0,
|
||||
"{s:?}"
|
||||
);
|
||||
assert!(first[0][1] != first[1][1] && first[1][1] != first[2][1]);
|
||||
assert_eq!(run(&mut u), first);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_nth_scripted_frame_counts_from_1() {
|
||||
assert_eq!(
|
||||
FrameTiming::nth(1, 50.0),
|
||||
FrameTiming {
|
||||
counter: 1,
|
||||
elapsed: 0.0,
|
||||
delta: 0.0
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
FrameTiming::nth(2, 50.0),
|
||||
FrameTiming {
|
||||
counter: 2,
|
||||
elapsed: 0.05,
|
||||
delta: 0.05
|
||||
}
|
||||
);
|
||||
assert_eq!(FrameTiming::nth(64, 50.0).elapsed, 3.15);
|
||||
assert_eq!(FrameTiming::nth(10_000, 50.0).elapsed, 499.95);
|
||||
assert_eq!(FrameTiming::nth(3, 1.0).elapsed, 0.002);
|
||||
// 60 fps: frame 61 is one second in, not 61 × 16 ms or 61 × 17 ms.
|
||||
let sixty = FrameTiming::nth(61, 1000.0 / 60.0);
|
||||
assert_eq!((sixty.elapsed, sixty.delta), (1.0, 1.0 / 60.0));
|
||||
}
|
||||
|
||||
/// Iris 1.11.7 (probe pack): `ambientLight` is the dimension type's (0.1 in the Nether,
|
||||
/// 0.25 in the End), and `playerMood` and `constantMood` both the cave mood.
|
||||
#[test]
|
||||
@@ -2210,7 +2384,7 @@ mod initial_history_tests {
|
||||
assert_eq!(float(&u, "constantMood")[0], 0.0235);
|
||||
}
|
||||
|
||||
fn frame(hud: &HudBatch) -> Frame<'_> {
|
||||
pub(in crate::gfx) fn frame(hud: &HudBatch) -> Frame<'_> {
|
||||
Frame {
|
||||
eye: glam::DVec3::ZERO,
|
||||
eye_rotation: [0.0; 2],
|
||||
|
||||
@@ -1576,7 +1576,7 @@ impl GraphicsUi {
|
||||
&cols,
|
||||
&lines,
|
||||
self.scroll.amount,
|
||||
Page::name,
|
||||
|page| lang::get(page.name()),
|
||||
|batch, setting: Setting, y| {
|
||||
let item = setting.item;
|
||||
let rect = cols.row(y);
|
||||
|
||||
@@ -76,6 +76,7 @@ pub struct HudAtlas {
|
||||
pub sprites: HashMap<String, Rect>,
|
||||
/// Resolved player-profile names and UUIDs for inline head components.
|
||||
pub object_skins: HashMap<String, String>,
|
||||
pub object_skins_revision: u64,
|
||||
/// Sprites with a 0 < alpha < 255 texel (`SpriteContents.transparency().hasTranslucent`),
|
||||
/// which puts their particles on vanilla's translucent layer.
|
||||
pub translucent: HashSet<String>,
|
||||
@@ -786,21 +787,29 @@ impl HudAtlas {
|
||||
.or_else(|| self.sprites.get(MISSING_SPRITE))
|
||||
.copied()
|
||||
}
|
||||
pub fn clear_object_skins(&mut self) {
|
||||
self.object_skins.clear();
|
||||
self.object_skins_revision = Self::next_revision();
|
||||
}
|
||||
/// Keep a ready profile skin when another source only has its default.
|
||||
pub fn set_object_skin(&mut self, key: String, skin: String) {
|
||||
use std::collections::hash_map::Entry;
|
||||
if self.object_skins.len() >= 1024 && !self.object_skins.contains_key(&key) {
|
||||
self.object_skins.clear();
|
||||
self.clear_object_skins();
|
||||
}
|
||||
match self.object_skins.entry(key) {
|
||||
Entry::Occupied(entry)
|
||||
if skin.starts_with("entity/player/") && self.sprites.contains_key(entry.get()) => {
|
||||
}
|
||||
Entry::Occupied(mut entry) => {
|
||||
entry.insert(skin);
|
||||
if entry.get() != &skin {
|
||||
entry.insert(skin);
|
||||
self.object_skins_revision = Self::next_revision();
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(skin);
|
||||
self.object_skins_revision = Self::next_revision();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -920,6 +929,7 @@ impl HudAtlas {
|
||||
height: 1,
|
||||
sprites: HashMap::new(),
|
||||
object_skins: HashMap::new(),
|
||||
object_skins_revision: Self::next_revision(),
|
||||
translucent: HashSet::new(),
|
||||
flags: HashMap::new(),
|
||||
sprite_animations: Vec::new(),
|
||||
@@ -985,6 +995,7 @@ impl HudAtlas {
|
||||
translucent: HashSet::new(),
|
||||
flags: HashMap::new(),
|
||||
object_skins: HashMap::new(),
|
||||
object_skins_revision: Self::next_revision(),
|
||||
sprite_animations: Vec::new(),
|
||||
glint: (vec![0; 4], 1, 1),
|
||||
glint_armor: (vec![0; 4], 1, 1),
|
||||
@@ -1436,6 +1447,7 @@ impl HudAtlas {
|
||||
height,
|
||||
sprites,
|
||||
object_skins: HashMap::new(),
|
||||
object_skins_revision: Self::next_revision(),
|
||||
translucent,
|
||||
flags,
|
||||
sprite_animations,
|
||||
@@ -2890,6 +2902,28 @@ impl HudRenderer {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn object_skin_revision_tracks_changes_and_capacity_clear() {
|
||||
let mut atlas = HudAtlas::boot();
|
||||
atlas.set_object_skin("name:player".into(), "skins/server".into());
|
||||
let revision = atlas.object_skins_revision;
|
||||
atlas.set_object_skin("name:player".into(), "skins/server".into());
|
||||
assert_eq!(atlas.object_skins_revision, revision);
|
||||
atlas.set_object_skin("name:player".into(), "skins/inline".into());
|
||||
assert_ne!(atlas.object_skins_revision, revision);
|
||||
for i in 1..1024 {
|
||||
atlas.set_object_skin(format!("name:{i}"), "skins/server".into());
|
||||
}
|
||||
let revision = atlas.object_skins_revision;
|
||||
atlas.set_object_skin("name:overflow".into(), "skins/server".into());
|
||||
assert_ne!(atlas.object_skins_revision, revision);
|
||||
assert_eq!(atlas.object_skins.len(), 1);
|
||||
let revision = atlas.object_skins_revision;
|
||||
atlas.clear_object_skins();
|
||||
assert_ne!(atlas.object_skins_revision, revision);
|
||||
assert!(atlas.object_skins.is_empty());
|
||||
}
|
||||
|
||||
/// The mod image slot is paid only by a game with a native mod loaded.
|
||||
#[test]
|
||||
fn the_mod_image_slot_exists_only_when_a_mod_is_loaded() {
|
||||
|
||||
@@ -97,6 +97,7 @@ pub mod settings;
|
||||
pub mod settings_shell;
|
||||
pub mod shader_options;
|
||||
pub mod shader_pack_list;
|
||||
pub mod shader_ui;
|
||||
pub mod signs;
|
||||
pub mod skins;
|
||||
pub mod sound_engine;
|
||||
|
||||
@@ -52,6 +52,8 @@ pub struct LightTables {
|
||||
/// what `light_chunk` stores per cell, so a cell costs 2 bytes and the
|
||||
/// flood fill never reads the (large) `cells` table.
|
||||
info: Vec<u32>,
|
||||
/// `info`'s low 16 bits.
|
||||
packed: Vec<u16>,
|
||||
/// `blocked(type_a, type_b, direction)` as a bit per direction.
|
||||
block_masks: Vec<u8>,
|
||||
}
|
||||
@@ -73,6 +75,7 @@ impl LightTables {
|
||||
faces: 2,
|
||||
types: 1,
|
||||
info: Vec::new(),
|
||||
packed: Vec::new(),
|
||||
block_masks: Vec::new(),
|
||||
};
|
||||
tables.rebuild_cache();
|
||||
@@ -101,6 +104,9 @@ impl LightTables {
|
||||
types <= 4096,
|
||||
"light tables: {types} cell configurations exceed the 12-bit type field"
|
||||
);
|
||||
// The region `light_chunk` floods over is filled with 0, and every air
|
||||
// cell of a section writes `info` of state 0 over it (`states_of`).
|
||||
debug_assert_eq!(info[0], 0, "state 0 (air) must hold no light info");
|
||||
let mut block_masks = vec![0u8; types * types];
|
||||
for a in 0..types {
|
||||
for b in 0..types {
|
||||
@@ -121,6 +127,7 @@ impl LightTables {
|
||||
}
|
||||
}
|
||||
self.types = types;
|
||||
self.packed = info.iter().map(|&i| i as u16).collect();
|
||||
self.info = info;
|
||||
self.block_masks = block_masks;
|
||||
}
|
||||
@@ -212,6 +219,7 @@ impl LightTables {
|
||||
faces,
|
||||
types: 0,
|
||||
info: Vec::new(),
|
||||
packed: Vec::new(),
|
||||
block_masks: Vec::new(),
|
||||
};
|
||||
tables.rebuild_cache();
|
||||
@@ -276,7 +284,8 @@ struct LightScratch {
|
||||
sky_queue: Vec<u32>,
|
||||
present: Vec<bool>,
|
||||
lowest: Vec<i32>,
|
||||
ids: Box<[u32; 4096]>,
|
||||
/// What `states_of` keeps between sections.
|
||||
section: SectionScratch,
|
||||
}
|
||||
|
||||
impl LightScratch {
|
||||
@@ -289,7 +298,7 @@ impl LightScratch {
|
||||
sky_queue: Vec::new(),
|
||||
present: vec![false; W * W],
|
||||
lowest: vec![0; W * W],
|
||||
ids: Box::new([0u32; 4096]),
|
||||
section: SectionScratch::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,10 +316,132 @@ impl LightScratch {
|
||||
}
|
||||
}
|
||||
|
||||
/// One section's values mapped to the packed light info, and the palette's
|
||||
/// info (`states_of`, kept between sections).
|
||||
struct SectionScratch {
|
||||
stage: Box<[u16; 4096]>,
|
||||
stage32: Box<[u32; 4096]>,
|
||||
info: Vec<u32>,
|
||||
packed: Vec<u16>,
|
||||
}
|
||||
impl SectionScratch {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
stage: Box::new([0u16; 4096]),
|
||||
stage32: Box::new([0u32; 4096]),
|
||||
info: Vec::new(),
|
||||
packed: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static SCRATCH: RefCell<LightScratch> = RefCell::new(LightScratch::new());
|
||||
}
|
||||
|
||||
/// Where a section sits in the 48-wide region.
|
||||
#[derive(Clone, Copy)]
|
||||
struct At {
|
||||
x: usize,
|
||||
y: usize,
|
||||
z: usize,
|
||||
}
|
||||
impl At {
|
||||
/// The region index of the section's row `row` (`ly << 4 | lz`), the
|
||||
/// start of a run of 16 cells along x.
|
||||
#[inline]
|
||||
fn row(self, row: usize) -> usize {
|
||||
(self.y + (row >> 4)) * (W * W) + (self.z + (row & 15)) * W + self.x
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy one section's block states into the region's `states` as the packed
|
||||
/// light info `light_chunk` floods over, and seed `queue` (and `block`) with
|
||||
/// the cells that emit light. The values are mapped to that info in one pass
|
||||
/// over the section's packed storage into `stage` (index order), then copied a
|
||||
/// row of 16 along x at a time: the region's rows are 48 apart, so the two
|
||||
/// cannot be one.
|
||||
fn states_of(
|
||||
section: &Section,
|
||||
tables: &LightTables,
|
||||
at: At,
|
||||
states: &mut [u16],
|
||||
block: &mut [u8],
|
||||
queue: &mut Vec<u32>,
|
||||
scratch: &mut SectionScratch,
|
||||
) {
|
||||
if section.blocks.bits() == 0 {
|
||||
// A section of one state (the caller has skipped the air ones): a row
|
||||
// of the region is that state's info.
|
||||
let info = tables.info[section.blocks.get(0) as usize];
|
||||
for ly in 0..16 {
|
||||
let mut p = at.row(ly << 4);
|
||||
for _ in 0..16 {
|
||||
states[p..p + 16].fill(info as u16);
|
||||
if info > 0xFFFF {
|
||||
for x in 0..16 {
|
||||
block[p + x] = (info >> 16) as u8;
|
||||
queue.push((p + x) as u32);
|
||||
}
|
||||
}
|
||||
p += W;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// The info of the ids the section stores: a list palette's ids are mapped
|
||||
// once, a global palette's are state ids, which `tables.info` holds. Only
|
||||
// a section whose palette emits light (an info's high bits) needs the
|
||||
// emission of each cell.
|
||||
let (info, packed, emits) = match section.blocks.palette() {
|
||||
Some(list) => {
|
||||
scratch.info.clear();
|
||||
scratch
|
||||
.info
|
||||
.extend(list.iter().map(|&id| tables.info[id as usize]));
|
||||
scratch.packed.clear();
|
||||
scratch
|
||||
.packed
|
||||
.extend(scratch.info.iter().map(|&i| i as u16));
|
||||
let emits = scratch.info.iter().any(|&i| i > 0xFFFF);
|
||||
(&scratch.info[..], &scratch.packed[..], emits)
|
||||
}
|
||||
None => (&tables.info[..], &tables.packed[..], true),
|
||||
};
|
||||
if emits {
|
||||
section
|
||||
.blocks
|
||||
.unpack_ids_mapped(&mut scratch.stage32[..], info);
|
||||
for ly in 0..16 {
|
||||
let mut p = at.row(ly << 4);
|
||||
for lz in 0..16 {
|
||||
let i = (ly << 8) | (lz << 4);
|
||||
for x in 0..16 {
|
||||
let info = scratch.stage32[i + x];
|
||||
states[p + x] = info as u16;
|
||||
if info > 0xFFFF {
|
||||
block[p + x] = (info >> 16) as u8;
|
||||
queue.push((p + x) as u32);
|
||||
}
|
||||
}
|
||||
p += W;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
section
|
||||
.blocks
|
||||
.unpack_ids_mapped(&mut scratch.stage[..], packed);
|
||||
for ly in 0..16 {
|
||||
let mut p = at.row(ly << 4);
|
||||
let (rows, _) = scratch.stage[(ly << 8)..(ly << 8) + 256].as_chunks::<16>();
|
||||
for row in rows {
|
||||
states[p..p + 16].copy_from_slice(row);
|
||||
p += W;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `region[dz][dx]` holds the chunks around the centre (index 1,1).
|
||||
/// `sky` is false in dimensions without skylight (the Nether).
|
||||
pub fn light_chunk(
|
||||
@@ -356,7 +487,7 @@ fn light_chunk_with_scratch(
|
||||
let block = &mut scratch.block[..total];
|
||||
let queue = &mut scratch.queue;
|
||||
let sky_queue = &mut scratch.sky_queue;
|
||||
let ids = &mut scratch.ids;
|
||||
let section_scratch = &mut scratch.section;
|
||||
|
||||
for (dz, row) in region.iter().enumerate() {
|
||||
let z_base = dz * 16;
|
||||
@@ -367,31 +498,12 @@ fn light_chunk_with_scratch(
|
||||
if section.is_uniform(0) {
|
||||
continue;
|
||||
}
|
||||
section.unpack(ids);
|
||||
let y_base = si * 16;
|
||||
for ly in 0..16 {
|
||||
let y = y_base + ly;
|
||||
let y_offset = y * (W * W);
|
||||
for lz in 0..16 {
|
||||
let z = z_base + lz;
|
||||
let p_row = y_offset + z * W + x_base;
|
||||
let i_row = (ly << 8) | (lz << 4);
|
||||
for lx in 0..16 {
|
||||
let id = ids[i_row + lx];
|
||||
if id == 0 {
|
||||
continue;
|
||||
}
|
||||
let p = p_row + lx;
|
||||
let info = tables.info[id as usize];
|
||||
states[p] = info as u16;
|
||||
let emission = info >> 16;
|
||||
if emission > 0 {
|
||||
block[p] = emission as u8;
|
||||
queue.push(p as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let at = At {
|
||||
x: x_base,
|
||||
y: si * 16,
|
||||
z: z_base,
|
||||
};
|
||||
states_of(section, tables, at, states, block, queue, section_scratch);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2195,6 +2307,88 @@ mod tests {
|
||||
pack_lighting(sections, h, &sky, &block, sky_light)
|
||||
}
|
||||
|
||||
/// 600 states of every dampening, the last 300 of them light sources (a
|
||||
/// section's palette must hold emitting values too, which take
|
||||
/// `states_of`'s other pass).
|
||||
fn info_tables() -> LightTables {
|
||||
let cells = (0..600u32)
|
||||
.map(|id| Cell {
|
||||
dampening: (id % 16) as u8,
|
||||
emission: if id >= 300 { 14 } else { 0 },
|
||||
shaped: false,
|
||||
faces: [EMPTY; 6],
|
||||
})
|
||||
.collect();
|
||||
LightTables::from_parts(cells, vec![false, true, true, true], 2)
|
||||
}
|
||||
|
||||
/// `states_of` writes what walking the section's `unpack`ed state ids
|
||||
/// writes, for every palette the collector can hold: one value, a list of
|
||||
/// up to 256 (four to eight bits wide) and the global palette beyond it,
|
||||
/// with and without a light source in the palette.
|
||||
#[test]
|
||||
fn section_states_match_the_unpacked_ids() {
|
||||
let tables = info_tables();
|
||||
let at = At { x: 0, y: 0, z: 0 };
|
||||
let mut ids = Box::new([0u32; 4096]);
|
||||
// (distinct values, the bits the collector stores them in)
|
||||
let cases = [
|
||||
(1, 0),
|
||||
(3, 4),
|
||||
(20, 5),
|
||||
(40, 6),
|
||||
(80, 7),
|
||||
(200, 8),
|
||||
(300, 16),
|
||||
];
|
||||
for (distinct, bits) in cases {
|
||||
for emitting in [false, true] {
|
||||
let base = if emitting { 300 } else { 0 };
|
||||
let values: Vec<u32> = (0..4096)
|
||||
// A pattern that repeats each value the section holds.
|
||||
.map(|i| base + ((i * 7 + i / 16) % distinct) as u32)
|
||||
.collect();
|
||||
let section = Section {
|
||||
blocks: Section::from_blocks(values.as_slice().try_into().unwrap()),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
section.blocks.bits(),
|
||||
bits,
|
||||
"{distinct} values should be stored in {bits} bits"
|
||||
);
|
||||
let mut states = vec![0u16; W * W * 16];
|
||||
let mut block = vec![0u8; W * W * 16];
|
||||
let mut queue = Vec::new();
|
||||
states_of(
|
||||
§ion,
|
||||
&tables,
|
||||
at,
|
||||
&mut states,
|
||||
&mut block,
|
||||
&mut queue,
|
||||
&mut SectionScratch::new(),
|
||||
);
|
||||
|
||||
section.unpack(&mut ids);
|
||||
let mut want_queue = Vec::new();
|
||||
for (i, &id) in ids.iter().enumerate() {
|
||||
let info = tables.info[id as usize];
|
||||
let p = at.row(i >> 4) + (i & 15);
|
||||
assert_eq!(
|
||||
states[p], info as u16,
|
||||
"{distinct} values, emitting {emitting}: state at {i}"
|
||||
);
|
||||
if info > 0xFFFF {
|
||||
assert_eq!(block[p], (info >> 16) as u8, "emission at {i}");
|
||||
want_queue.push(p as u32);
|
||||
}
|
||||
}
|
||||
assert_eq!(queue, want_queue, "{distinct} values, emitting {emitting}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_chunk_matches_old_algorithm() {
|
||||
let t = tables();
|
||||
|
||||
@@ -401,6 +401,7 @@ fn visibility(pad: &Padded, info: &[Info]) -> u64 {
|
||||
}
|
||||
/// The section's cells that are not `isSolidRender`, by row (`y << 4 | z`, bit
|
||||
/// x), from the padded rows of `MeshScratch::solid_rows`.
|
||||
#[cfg(test)]
|
||||
fn open_rows(solid: &[u32]) -> [u16; 256] {
|
||||
std::array::from_fn(|r| {
|
||||
let (y, z) = ((r >> 4) as i32, (r & 15) as i32);
|
||||
@@ -977,10 +978,11 @@ impl Padded {
|
||||
// The section itself is unpacked in one pass and copied row by row;
|
||||
// the border cells of the neighbouring sections are read in runs, from
|
||||
// sections found once per layer.
|
||||
own.fill(0);
|
||||
let center = region[1][1];
|
||||
if let Some(c) = center {
|
||||
c.sections[sy as usize].unpack(own);
|
||||
} else {
|
||||
own.fill(0);
|
||||
}
|
||||
for y in -B..16 + B {
|
||||
let wy = base_y + y;
|
||||
@@ -1065,6 +1067,18 @@ fn fill_run(
|
||||
},
|
||||
}
|
||||
}
|
||||
// Direction order is the same as Direction::ALL; all values are evaluated at compile time.
|
||||
const DIRECTIONS: [(Direction, [i32; 3], u8); 6] = [
|
||||
direction_info(Direction::Down),
|
||||
direction_info(Direction::Up),
|
||||
direction_info(Direction::North),
|
||||
direction_info(Direction::South),
|
||||
direction_info(Direction::West),
|
||||
direction_info(Direction::East),
|
||||
];
|
||||
const fn direction_info(d: Direction) -> (Direction, [i32; 3], u8) {
|
||||
(d, d.normal(), d.opposite() as u8)
|
||||
}
|
||||
/// Per-worker temporary arrays. They never hold resource-pack references or
|
||||
/// geometry still owned by the renderer, and total about 150 KiB per worker
|
||||
/// (the leaf mask adds 8,000 cells, depths and a queue).
|
||||
@@ -1080,21 +1094,63 @@ pub struct MeshScratch {
|
||||
/// Fills `rows` with the padded section's `isSolidRender` blocks, one bit each (`MeshScratch::solid_rows`).
|
||||
fn solid_rows(pad: &Padded, solid: &[bool], rows: &mut Vec<u32>) {
|
||||
rows.clear();
|
||||
rows.extend(pad.blocks.as_chunks::<P>().0.iter().map(|row| {
|
||||
row.iter().enumerate().fold(0u32, |bits, (x, &id)| {
|
||||
bits | u32::from(solid[id as usize]) << x
|
||||
})
|
||||
}));
|
||||
for row in pad.blocks.as_chunks::<P>().0 {
|
||||
let mut bits = 0;
|
||||
for (x, &id) in row.iter().enumerate() {
|
||||
bits |= u32::from(solid[id as usize]) << x;
|
||||
}
|
||||
rows.push(bits);
|
||||
}
|
||||
}
|
||||
/// Whether the block at `(x, y, z)` (section-local) and its six neighbours
|
||||
/// are all `isSolidRender`: none of its faces can show.
|
||||
#[inline]
|
||||
fn hidden(rows: &[u32], x: i32, y: i32, z: i32) -> bool {
|
||||
let row = |y: i32, z: i32| rows[((y + B) as usize) * P + (z + B) as usize];
|
||||
let r = row(y, z);
|
||||
let around =
|
||||
r & r << 1 & r >> 1 & row(y - 1, z) & row(y + 1, z) & row(y, z - 1) & row(y, z + 1);
|
||||
around >> (x + B) & 1 != 0
|
||||
/// Open and fully surrounded cells in each inner row.
|
||||
fn row_masks(rows: &[u32]) -> ([u16; 256], [u16; 256]) {
|
||||
let mut open = [0; 256];
|
||||
let mut hidden = [0; 256];
|
||||
for y in 0..16 {
|
||||
for z in 0..16 {
|
||||
let i = (y + B as usize) * P + z + B as usize;
|
||||
let r = rows[i];
|
||||
let around =
|
||||
r & r << 1 & r >> 1 & rows[i - P] & rows[i + P] & rows[i - 1] & rows[i + 1];
|
||||
open[y * 16 + z] = !(r >> B) as u16;
|
||||
hidden[y * 16 + z] = (around >> B) as u16;
|
||||
}
|
||||
}
|
||||
(open, hidden)
|
||||
}
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn row_masks_match_cell_definition() {
|
||||
let mut seed = 42u32;
|
||||
for _ in 0..100 {
|
||||
let rows: Vec<u32> = (0..P * P)
|
||||
.map(|_| {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 17;
|
||||
seed ^= seed << 5;
|
||||
seed & ((1 << P) - 1)
|
||||
})
|
||||
.collect();
|
||||
let (open, hidden) = row_masks(&rows);
|
||||
for y in 0..16 {
|
||||
for z in 0..16 {
|
||||
let cell = |x: usize, y: usize, z: usize| (rows[y * P + z] >> x) & 1 != 0;
|
||||
for x in 0..16 {
|
||||
let (x, py, pz) = (x + B as usize, y + B as usize, z + B as usize);
|
||||
let occupied = cell(x, py, pz);
|
||||
let enclosed = occupied
|
||||
&& cell(x - 1, py, pz)
|
||||
&& cell(x + 1, py, pz)
|
||||
&& cell(x, py - 1, pz)
|
||||
&& cell(x, py + 1, pz)
|
||||
&& cell(x, py, pz - 1)
|
||||
&& cell(x, py, pz + 1);
|
||||
assert_eq!((open[y * 16 + z] >> (x - B as usize)) & 1 != 0, !occupied);
|
||||
assert_eq!((hidden[y * 16 + z] >> (x - B as usize)) & 1 != 0, enclosed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod leaf_culling_tests;
|
||||
@@ -1463,7 +1519,8 @@ impl Mesher<'_> {
|
||||
let pad = &scratch.padded;
|
||||
solid_rows(pad, &self.tables.solid, &mut scratch.solid_rows);
|
||||
let opaque = &scratch.solid_rows;
|
||||
out.visibility = visibility_rows(&open_rows(opaque));
|
||||
let (open, hidden) = row_masks(opaque);
|
||||
out.visibility = visibility_rows(&open);
|
||||
let base_y = center.min_y() + sy * 16;
|
||||
if self.leaf_culling.is_interior() {
|
||||
scratch
|
||||
@@ -1497,7 +1554,7 @@ impl Mesher<'_> {
|
||||
out.lights
|
||||
.push(([center.x * 16 + x, base_y + y, center.z * 16 + z], id));
|
||||
}
|
||||
if hidden(opaque, x, y, z) {
|
||||
if hidden[(y * 16 + z) as usize] >> x & 1 != 0 {
|
||||
continue;
|
||||
}
|
||||
if state.fluid.kind() != FluidKind::Empty {
|
||||
@@ -1538,12 +1595,11 @@ impl Mesher<'_> {
|
||||
let interior_leaf = state.leaves
|
||||
&& self.leaf_culling.is_interior()
|
||||
&& scratch.leaf.interior(self.leaf_culling, x, y, z);
|
||||
for d in Direction::ALL {
|
||||
let [dx, dy, dz] = d.normal();
|
||||
for (d, [dx, dy, dz], opposite) in DIRECTIONS {
|
||||
let neighbor = pad.blocks[pi(x + dx, y + dy, z + dz)];
|
||||
// `should_render_face`'s first test, without its lookups: a
|
||||
// full occlusion face toward this block hides the face.
|
||||
if full_faces[neighbor as usize] >> d.opposite() as u8 & 1 != 0 {
|
||||
if full_faces[neighbor as usize] >> opposite & 1 != 0 {
|
||||
continue;
|
||||
}
|
||||
let other = &info[neighbor as usize];
|
||||
|
||||
@@ -229,7 +229,7 @@ pub(super) fn tesselate(
|
||||
let kind = me.fluid.kind();
|
||||
let same = |s: &Info| s.fluid.kind().same_family(kind);
|
||||
let neighbor = |d: Direction| {
|
||||
let [dx, dy, dz] = d.normal();
|
||||
let [dx, dy, dz] = super::DIRECTIONS[d as usize].1;
|
||||
at([x + dx, y + dy, z + dz])
|
||||
};
|
||||
let (down, up) = (neighbor(Direction::Down), neighbor(Direction::Up));
|
||||
@@ -238,12 +238,11 @@ pub(super) fn tesselate(
|
||||
let render_down =
|
||||
should_render(Direction::Down) && !occluded_by(Direction::Down, MAX_HEIGHT, down);
|
||||
let sides = [
|
||||
Direction::North,
|
||||
Direction::South,
|
||||
Direction::West,
|
||||
Direction::East,
|
||||
]
|
||||
.map(should_render);
|
||||
should_render(Direction::North),
|
||||
should_render(Direction::South),
|
||||
should_render(Direction::West),
|
||||
should_render(Direction::East),
|
||||
];
|
||||
if !render_up && !render_down && !sides.iter().any(|&s| s) {
|
||||
return;
|
||||
}
|
||||
@@ -356,9 +355,9 @@ pub(super) fn tesselate(
|
||||
fluid_face(color, face)
|
||||
};
|
||||
let vertex = |(pos, uv)| TerrainVertex::new(pos, uv, layer, color, light);
|
||||
target.extend(v.map(vertex));
|
||||
target.extend([vertex(v[0]), vertex(v[1]), vertex(v[2]), vertex(v[3])]);
|
||||
if back {
|
||||
target.extend([v[0], v[3], v[2], v[1]].map(vertex));
|
||||
target.extend([vertex(v[0]), vertex(v[3]), vertex(v[2]), vertex(v[1])]);
|
||||
}
|
||||
};
|
||||
let (fx, fy, fz) = (x as f32, y as f32, z as f32);
|
||||
|
||||
@@ -23,6 +23,7 @@ use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
fs,
|
||||
path::Path,
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
pub struct BakedFace {
|
||||
@@ -51,8 +52,11 @@ type VariantKey = (String, [u8; 3], bool);
|
||||
pub struct StateModels {
|
||||
/// Each definition's choices, a variant standing as its index in `baked`.
|
||||
choices: Vec<CompiledChoices<u32>>,
|
||||
/// All baked alternatives per definition, invalidated before mutable model access.
|
||||
bounds: OnceLock<Vec<Option<[[f32; 3]; 2]>>>,
|
||||
/// By state id: its definition in `choices` and the models it selects.
|
||||
states: Vec<Option<(usize, SelectedModels)>>,
|
||||
single_models: Vec<Option<u32>>,
|
||||
/// Every transformed model once, with the variant it was baked from.
|
||||
baked: Vec<(Variant, BakedModel)>,
|
||||
/// Standalone models by id: their index in `baked`.
|
||||
@@ -354,10 +358,19 @@ impl StateModels {
|
||||
baked.push((variant, model));
|
||||
}
|
||||
// Every variant of every definition was baked above.
|
||||
let choices = choices.iter().map(|c| c.map(|v| index[&key(v)])).collect();
|
||||
let choices: Vec<_> = choices.iter().map(|c| c.map(|v| index[&key(v)])).collect();
|
||||
let single_models = states
|
||||
.iter()
|
||||
.map(|state| {
|
||||
let (choice, selected) = state.as_ref()?;
|
||||
choices[*choice].single_variant(selected).copied()
|
||||
})
|
||||
.collect();
|
||||
Ok(Self {
|
||||
choices,
|
||||
bounds: OnceLock::new(),
|
||||
states,
|
||||
single_models,
|
||||
baked,
|
||||
standalone: BTreeMap::new(),
|
||||
})
|
||||
@@ -408,6 +421,7 @@ impl StateModels {
|
||||
}
|
||||
/// Every baked model with the variant it was baked from, to change its faces.
|
||||
pub fn models_mut(&mut self) -> impl Iterator<Item = (&Variant, &mut BakedModel)> {
|
||||
self.bounds.take();
|
||||
self.baked
|
||||
.iter_mut()
|
||||
.map(|(variant, model)| (&*variant, model))
|
||||
@@ -432,6 +446,37 @@ impl StateModels {
|
||||
let model = self.choices[*choice].particle_variant(selected)?;
|
||||
self.baked[*model as usize].1.particle
|
||||
}
|
||||
/// Envelope of every weighted/multipart alternative in the state's definition.
|
||||
/// Includes transformed resource-pack faces outside the unit cube. Empty or
|
||||
/// non-finite geometry has no usable envelope and keeps the original draw path.
|
||||
pub fn render_bounds(&self, state: BlockStateId) -> Option<[[f32; 3]; 2]> {
|
||||
let (definition, _) = self.state(state)?;
|
||||
let bounds = self.bounds.get_or_init(|| {
|
||||
self.choices
|
||||
.iter()
|
||||
.map(|choices| {
|
||||
let mut lo = glam::Vec3::splat(f32::INFINITY);
|
||||
let mut hi = glam::Vec3::splat(f32::NEG_INFINITY);
|
||||
let mut valid = true;
|
||||
// `map` visits every choice, without consuming the render RNG.
|
||||
choices.map(|&index| {
|
||||
for face in &self.baked[index as usize].1.faces {
|
||||
for p in face.quad.positions {
|
||||
let p = glam::Vec3::from_array(p);
|
||||
valid &= p.is_finite();
|
||||
lo = lo.min(p);
|
||||
hi = hi.max(p);
|
||||
}
|
||||
}
|
||||
});
|
||||
(valid && lo.is_finite() && hi.is_finite())
|
||||
.then_some([lo.to_array(), hi.to_array()])
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
bounds.get(*definition).copied().flatten()
|
||||
}
|
||||
|
||||
pub fn collect_seeded(
|
||||
&self,
|
||||
state: BlockStateId,
|
||||
@@ -452,6 +497,10 @@ impl StateModels {
|
||||
out: &mut Vec<&'a BakedModel>,
|
||||
) -> Result<()> {
|
||||
out.clear();
|
||||
if let Some(Some(model)) = self.single_models.get(state.0 as usize) {
|
||||
out.push(&self.baked[*model as usize].1);
|
||||
return Ok(());
|
||||
}
|
||||
self.each_seeded(state, position_seed(position), |(_, model)| out.push(model))
|
||||
}
|
||||
fn each_seeded<'a>(
|
||||
@@ -619,3 +668,7 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "catalog/bounds_tests.rs"]
|
||||
mod bounds_tests;
|
||||
@@ -0,0 +1,115 @@
|
||||
use super::*;
|
||||
|
||||
fn catalog(definition: &str) -> StateModels {
|
||||
let definition = BlockStateDefinition::from_json(definition).unwrap();
|
||||
let choices = CompiledChoices::new(&definition)
|
||||
.unwrap()
|
||||
.map(|v| if v.model == "horizon:a" { 0 } else { 1 });
|
||||
let baked = [
|
||||
([-16, 0, 0], [16, 16, 16], [0, 0, 0]),
|
||||
([0, -8, -16], [32, 32, 16], [0, 90, 0]),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, (from, to, turns))| {
|
||||
let faces: serde_json::Map<String, Value> =
|
||||
["up", "down", "north", "south", "east", "west"]
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
(
|
||||
name.to_owned(),
|
||||
serde_json::json!({"texture":"#all"}),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let json =
|
||||
serde_json::json!({"textures":{"all":"horizon:block/test"},"elements":[{"from":from,"to":to,"faces":faces}]}).to_string();
|
||||
let model = horizon_assets::model::CuboidModel::from_json(&json).unwrap();
|
||||
let name = format!("horizon:{}", if i == 0 { "a" } else { "b" });
|
||||
let library =
|
||||
horizon_assets::resolve::ModelLibrary::new(BTreeMap::from([(name.clone(), model)]))
|
||||
.unwrap();
|
||||
let baked = bake_resolved(
|
||||
Path::new("/nonexistent-horizon-bounds-fixture"),
|
||||
&mut Sprites::default(),
|
||||
&library.resolve(&name).unwrap(),
|
||||
turns,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
(
|
||||
Variant {
|
||||
model: name,
|
||||
rotations: turns,
|
||||
uv_lock: false,
|
||||
},
|
||||
baked,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
StateModels {
|
||||
choices: vec![choices],
|
||||
states: vec![Some((0, SelectedModels::Variant(0)))],
|
||||
baked,
|
||||
..StateModels::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelopes_cover_weighted_and_multipart_geometry_and_mutation_invalidates_them() {
|
||||
for (definition, selected) in [
|
||||
(
|
||||
r#"{"variants":{"": [{"model":"horizon:a","weight":1},{"model":"horizon:b","weight":3}]}}"#,
|
||||
SelectedModels::Variant(0),
|
||||
),
|
||||
(
|
||||
r#"{"multipart":[{"apply":{"model":"horizon:a"}},{"apply":{"model":"horizon:b"}}]}"#,
|
||||
SelectedModels::Multipart(vec![0, 1]),
|
||||
),
|
||||
] {
|
||||
let mut models = catalog(definition);
|
||||
models.states[0] = Some((0, selected));
|
||||
let [lo, hi] = models.render_bounds(BlockStateId(0)).unwrap();
|
||||
assert!(
|
||||
lo[0] <= -1.0 && hi[1] >= 2.0,
|
||||
"pack geometry exceeds a unit cube"
|
||||
);
|
||||
let mut variants = BTreeSet::new();
|
||||
for i in 0..128 {
|
||||
let seed = position_seed(BlockPos::new(i * 13, i % 31, -i * 23));
|
||||
for (variant, model) in models.collect_seeded(BlockStateId(0), seed).unwrap() {
|
||||
variants.insert(variant.model.clone());
|
||||
for point in model.faces.iter().flat_map(|f| f.quad.positions) {
|
||||
assert!((0..3).all(|axis| point[axis] >= lo[axis] && point[axis] <= hi[axis]));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(variants.len(), 2);
|
||||
models.models_mut().next().unwrap().1.faces[0]
|
||||
.quad
|
||||
.positions[0] = [100.0, 2.0, -5.0];
|
||||
let [lo, hi] = models.render_bounds(BlockStateId(0)).unwrap();
|
||||
assert_eq!(hi[0], 100.0);
|
||||
assert_eq!(lo[2], -5.0);
|
||||
// A resource replacement owns a fresh catalog rather than the previous cache.
|
||||
let replacement = catalog(r#"{"variants":{"":{"model":"horizon:a"}}}"#);
|
||||
assert_eq!(
|
||||
replacement.render_bounds(BlockStateId(0)),
|
||||
Some([[-1.0, 0.0, 0.0], [1.0, 1.0, 1.0]])
|
||||
);
|
||||
models.models_mut().next().unwrap().1.faces[0]
|
||||
.quad
|
||||
.positions[0][0] = f32::NAN;
|
||||
assert!(models.render_bounds(BlockStateId(0)).is_none());
|
||||
}
|
||||
assert!(
|
||||
StateModels::default()
|
||||
.render_bounds(BlockStateId(0))
|
||||
.is_none()
|
||||
);
|
||||
let mut empty = catalog(r#"{"variants":{"":{"model":"horizon:a"}}}"#);
|
||||
for (_, model) in empty.models_mut() {
|
||||
model.faces.clear();
|
||||
}
|
||||
assert!(empty.render_bounds(BlockStateId(0)).is_none());
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -899,9 +899,24 @@ fn merge(kind: Merge, layers: &[Vec<u8>]) -> Option<Vec<u8>> {
|
||||
.iter()
|
||||
// SoundManager skips a sounds.json Gson rejects, whole.
|
||||
.filter(|b| kind != Merge::Sounds || crate::sound_events::is_valid_file(b))
|
||||
.filter_map(|b| match serde_json::from_slice(b) {
|
||||
Ok(Value::Object(m)) => Some(m),
|
||||
_ => None,
|
||||
.filter_map(|b| {
|
||||
match serde_json::from_slice(b) {
|
||||
Ok(Value::Object(m)) => Some(m),
|
||||
// `Language.loadFromJson` reads with Gson's lenient reader, so
|
||||
// a lang file with `#` comments still counts; a file that
|
||||
// strict JSON accepts keeps its bytes as they are.
|
||||
_ if kind == Merge::Lang => {
|
||||
let mut strings = std::collections::HashMap::new();
|
||||
horizon_client_core::lang::read_lang_file(b, &mut strings);
|
||||
Some(
|
||||
strings
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, Value::String(v)))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let list = |key: &str, layers: &mut dyn Iterator<Item = &Map<String, Value>>| {
|
||||
@@ -1424,6 +1439,18 @@ mod tests {
|
||||
parse(lang.unwrap()),
|
||||
serde_json::json!({"a":"vanilla a","b":"pack b"})
|
||||
);
|
||||
// A layer with `#` comments is read leniently, not dropped.
|
||||
let lenient = merge(
|
||||
Merge::Lang,
|
||||
&[
|
||||
json(r#"{"a":"vanilla a"}"#),
|
||||
json("# a pack comment\n{\"b\":\"pack b\", # trailing\n\"a\":\"pack a\"}"),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
parse(lenient.unwrap()),
|
||||
serde_json::json!({"a":"pack a","b":"pack b"})
|
||||
);
|
||||
// SoundManager: sounds add up; `replace` starts the event over.
|
||||
let sounds = merge(
|
||||
Merge::Sounds,
|
||||
|
||||
@@ -975,11 +975,32 @@ impl TextField {
|
||||
}
|
||||
}
|
||||
|
||||
/// `ScreenRectangle.intersection`: the part of both rectangles, or
|
||||
/// `ScreenRectangle.empty()` (`0, 0, 0, 0`, which clips everything) when they
|
||||
/// do not overlap.
|
||||
fn intersect(a: [i32; 4], b: [i32; 4]) -> [i32; 4] {
|
||||
let rect = [
|
||||
a[0].max(b[0]),
|
||||
a[1].max(b[1]),
|
||||
a[2].min(b[2]),
|
||||
a[3].min(b[3]),
|
||||
];
|
||||
if rect[0] < rect[2] && rect[1] < rect[3] {
|
||||
rect
|
||||
} else {
|
||||
[0, 0, 0, 0]
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the calls with the HUD batch: sprites nine-sliced, textures from
|
||||
/// the atlas (`textures/gui/x.png` is `gui/x`; tiled textures from their
|
||||
/// corner), scissor clipping, and the tooltip at the mouse.
|
||||
pub fn paint(batch: &mut HudBatch, atlas: &HudAtlas, draws: &[Draw]) {
|
||||
let mut clip: Option<(usize, [i32; 4])> = None;
|
||||
// `GuiGraphicsExtractor.ScissorStack`: a scissor clips what is drawn since
|
||||
// it opened, cut to the screen while none is open and to the one around it
|
||||
// otherwise; closing it puts the enclosing one back.
|
||||
let screen = [0, 0, batch.width as i32, batch.height as i32];
|
||||
let mut clips: Vec<(usize, [i32; 4])> = Vec::new();
|
||||
let mut tooltip = None;
|
||||
for d in draws {
|
||||
match d {
|
||||
@@ -1083,10 +1104,11 @@ pub fn paint(batch: &mut HudBatch, atlas: &HudAtlas, draws: &[Draw]) {
|
||||
);
|
||||
}
|
||||
Draw::Scissor { x0, y0, x1, y1 } => {
|
||||
clip = Some((batch.vertices.len(), [*x0, *y0, *x1, *y1]))
|
||||
let outer = clips.last().map_or(screen, |&(_, rect)| rect);
|
||||
clips.push((batch.vertices.len(), intersect(outer, [*x0, *y0, *x1, *y1])));
|
||||
}
|
||||
Draw::NoScissor => {
|
||||
if let Some((start, [x0, y0, x1, y1])) = clip.take() {
|
||||
if let Some((start, [x0, y0, x1, y1])) = clips.pop() {
|
||||
batch.clip_since(start, x0 as f32, y0 as f32, x1 as f32, y1 as f32);
|
||||
}
|
||||
}
|
||||
@@ -1282,3 +1304,126 @@ mod button_label_tests {
|
||||
batch.vertices.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod scissor_tests {
|
||||
use super::*;
|
||||
|
||||
fn atlas() -> HudAtlas {
|
||||
HudAtlas::build(std::path::Path::new("/nonexistent/horizon-test-assets")).unwrap()
|
||||
}
|
||||
|
||||
/// The bounds of every quad's six vertices, in draw order.
|
||||
fn quads(batch: &HudBatch) -> Vec<[f32; 4]> {
|
||||
batch
|
||||
.vertices
|
||||
.as_chunks::<6>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|q| {
|
||||
let (mut x0, mut y0) = (f32::MAX, f32::MAX);
|
||||
let (mut x1, mut y1) = (f32::MIN, f32::MIN);
|
||||
for v in q {
|
||||
x0 = x0.min(v.pos[0]);
|
||||
y0 = y0.min(v.pos[1]);
|
||||
x1 = x1.max(v.pos[0]);
|
||||
y1 = y1.max(v.pos[1]);
|
||||
}
|
||||
[x0, y0, x1, y1]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn fill(x0: i32, y0: i32, x1: i32, y1: i32) -> Draw {
|
||||
Draw::Fill {
|
||||
x0,
|
||||
y0,
|
||||
x1,
|
||||
y1,
|
||||
color: -1,
|
||||
}
|
||||
}
|
||||
|
||||
fn scissor(x0: i32, y0: i32, x1: i32, y1: i32) -> Draw {
|
||||
Draw::Scissor { x0, y0, x1, y1 }
|
||||
}
|
||||
|
||||
/// `GuiGraphicsExtractor.ScissorStack.push`: a scissor inside another is
|
||||
/// intersected with it (`ScreenRectangle.intersection`), and the outer one
|
||||
/// still clips what is drawn after the inner one closes.
|
||||
#[test]
|
||||
fn a_nested_scissor_is_intersected_with_the_one_around_it() {
|
||||
let atlas = atlas();
|
||||
let mut batch = HudBatch::new([320, 240]);
|
||||
paint(
|
||||
&mut batch,
|
||||
&atlas,
|
||||
&[
|
||||
scissor(10, 10, 100, 100),
|
||||
fill(0, 0, 200, 200),
|
||||
scissor(50, 50, 300, 300),
|
||||
fill(0, 0, 400, 400),
|
||||
Draw::NoScissor,
|
||||
fill(0, 0, 200, 200),
|
||||
Draw::NoScissor,
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
quads(&batch),
|
||||
[
|
||||
[10.0, 10.0, 100.0, 100.0],
|
||||
[50.0, 50.0, 100.0, 100.0],
|
||||
[10.0, 10.0, 100.0, 100.0],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// A scissor with no scissor around it is cut to the screen, as
|
||||
/// `ScissorStack.push` intersects with the screen while the stack is empty.
|
||||
#[test]
|
||||
fn a_scissor_wider_than_the_screen_is_cut_to_it() {
|
||||
let atlas = atlas();
|
||||
let mut batch = HudBatch::new([320, 240]);
|
||||
paint(
|
||||
&mut batch,
|
||||
&atlas,
|
||||
&[
|
||||
scissor(-50, -50, 500, 500),
|
||||
fill(-100, -100, 600, 600),
|
||||
Draw::NoScissor,
|
||||
],
|
||||
);
|
||||
assert_eq!(quads(&batch), [[0.0, 0.0, 320.0, 240.0]]);
|
||||
}
|
||||
|
||||
/// A scissor that misses the one around it intersects to
|
||||
/// `ScreenRectangle.empty()`: nothing inside it is drawn, and the outer
|
||||
/// scissor takes over again when it closes.
|
||||
#[test]
|
||||
fn a_scissor_outside_the_one_around_it_draws_nothing() {
|
||||
let atlas = atlas();
|
||||
let mut batch = HudBatch::new([320, 240]);
|
||||
paint(
|
||||
&mut batch,
|
||||
&atlas,
|
||||
&[
|
||||
scissor(10, 10, 50, 50),
|
||||
fill(0, 0, 200, 200),
|
||||
scissor(60, 60, 90, 90),
|
||||
fill(0, 0, 200, 200),
|
||||
Draw::NoScissor,
|
||||
fill(0, 0, 200, 200),
|
||||
Draw::NoScissor,
|
||||
fill(0, 0, 200, 200),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
quads(&batch),
|
||||
[
|
||||
[10.0, 10.0, 50.0, 50.0],
|
||||
[10.0, 10.0, 50.0, 50.0],
|
||||
[0.0, 0.0, 200.0, 200.0],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,11 @@ impl<P: Copy, S: Copy> Lines<P, S> {
|
||||
}
|
||||
self.y += style::GROUP_GAP;
|
||||
}
|
||||
/// A group of rows under its page's own name (`list`'s `page_name`), for pages named at run
|
||||
/// time, such as a shader pack's screens.
|
||||
pub(crate) fn titled(&mut self, page: P, rows: Vec<S>) {
|
||||
self.group(page, "", rows);
|
||||
}
|
||||
/// The lines and the content's height.
|
||||
pub(crate) fn finish(self) -> (Vec<Line<P, S>>, f32) {
|
||||
(self.lines, (self.y - style::GROUP_GAP).max(0.0))
|
||||
@@ -292,14 +297,15 @@ pub(crate) fn plate(batch: &mut HudBatch, atlas: &HudAtlas, cols: &Columns, head
|
||||
}
|
||||
|
||||
/// The list, clipped to its view: each group's panel, then its names and rows on it. `row`
|
||||
/// draws a setting at its `y`.
|
||||
/// draws a setting at its `y`; `page_name` names a page, and a group without a name of its own
|
||||
/// (`Lines::titled`), in the player's language.
|
||||
pub(crate) fn list<P: Copy, S: Copy>(
|
||||
batch: &mut HudBatch,
|
||||
atlas: &HudAtlas,
|
||||
cols: &Columns,
|
||||
lines: &[Line<P, S>],
|
||||
scroll: f32,
|
||||
page_name: impl Fn(P) -> &'static str,
|
||||
page_name: impl Fn(P) -> String,
|
||||
mut row: impl FnMut(&mut HudBatch, S, f32),
|
||||
) {
|
||||
let start = batch.vertices.len();
|
||||
@@ -325,10 +331,11 @@ pub(crate) fn list<P: Copy, S: Copy>(
|
||||
}
|
||||
let Some(setting) = line.setting else {
|
||||
let (name, color, dy) = match line.group {
|
||||
Some((name, _)) => (name, style::MUTED, 3.0),
|
||||
Some(("", _)) => (page_name(line.page), style::MUTED, 3.0),
|
||||
Some((name, _)) => (lang::get(name), style::MUTED, 3.0),
|
||||
None => (page_name(line.page), style::ACCENT, 4.0),
|
||||
};
|
||||
let name = Text::literal(lang::get(name));
|
||||
let name = Text::literal(name);
|
||||
batch.text_styled(atlas, &name, cols.list_x + 2.0, y + dy, color, true);
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -191,49 +191,6 @@ impl ShaderOptions {
|
||||
.map(|c| Text::legacy(&c.replace("\\n", "\n")))
|
||||
}
|
||||
|
||||
/// `option_label` within `width` as `measure` sizes it: the name shortened (`Bloom Up...: 2`)
|
||||
/// when the whole doesn't fit, and whether it was.
|
||||
pub fn option_label_within(
|
||||
&self,
|
||||
name: &str,
|
||||
width: f32,
|
||||
measure: impl Fn(&Text) -> f32,
|
||||
) -> (Text, bool) {
|
||||
let full = self.option_label(name);
|
||||
if measure(&full) <= width {
|
||||
return (full, false);
|
||||
}
|
||||
// The pack's text, cut a character at a time with the `§` codes it holds kept.
|
||||
let key = format!("option.{name}");
|
||||
let caption = self.lang.get(&key).unwrap_or(name);
|
||||
let value = self.value_text(name);
|
||||
let mut chars: Vec<char> = caption.chars().collect();
|
||||
loop {
|
||||
while chars.len() >= 2 && chars[chars.len() - 2] == '§' {
|
||||
chars.truncate(chars.len() - 2);
|
||||
}
|
||||
if chars.pop().is_none() {
|
||||
return (full, true);
|
||||
}
|
||||
// Without the spaces and codes it ends with.
|
||||
let mut end = chars.len();
|
||||
loop {
|
||||
if end >= 2 && chars[end - 2] == '§' {
|
||||
end -= 2;
|
||||
} else if end > 0 && chars[end - 1].is_whitespace() {
|
||||
end -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let cut: String = chars[..end].iter().collect();
|
||||
let label = Text::option_name_value(Text::legacy(&format!("{cut}...")), value.clone());
|
||||
if measure(&label) <= width {
|
||||
return (label, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `Name: value`, in the pack's words, the value yellow when it is not the default.
|
||||
pub fn option_label(&self, name: &str) -> Text {
|
||||
Text::option_name_value(
|
||||
@@ -242,8 +199,13 @@ impl ShaderOptions {
|
||||
)
|
||||
}
|
||||
|
||||
/// Option `name`'s caption in the pack's words.
|
||||
pub fn option_name(&self, name: &str) -> Text {
|
||||
self.named(&format!("option.{name}"), name)
|
||||
}
|
||||
|
||||
/// The value of option `name` as the pack names it, yellow when it is not the default.
|
||||
fn value_text(&self, name: &str) -> Text {
|
||||
pub fn value_text(&self, name: &str) -> Text {
|
||||
let Some(option) = self.options.get(name) else {
|
||||
return Text::literal(String::new());
|
||||
};
|
||||
@@ -350,7 +312,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_resets_comments_and_fitted_labels() {
|
||||
fn single_resets_and_comments() {
|
||||
let mut o = options();
|
||||
o.lang = Lang::parse(
|
||||
"option.LEVEL=Level of detail\noption.LEVEL.comment=How much detail.\\nMore costs more.\n",
|
||||
@@ -364,18 +326,6 @@ mod tests {
|
||||
Some("How much detail.\nMore costs more.")
|
||||
);
|
||||
assert!(o.comment("option.AO").is_none());
|
||||
// One unit a character.
|
||||
let measure = |t: &Text| t.plain().chars().count() as f32;
|
||||
assert!(!o.option_label_within("LEVEL", 40.0, measure).1);
|
||||
let (fitted, cut) = o.option_label_within("LEVEL", 12.0, measure);
|
||||
assert!(cut);
|
||||
assert_eq!(fitted.plain(), "Level...: 2");
|
||||
o.lang = Lang::parse("option.LEVEL=§aLevel §lof detail§r\n");
|
||||
let (fitted, _) = o.option_label_within("LEVEL", 12.0, measure);
|
||||
assert_eq!(fitted.plain(), "Level...: 2");
|
||||
assert_eq!(fitted.spans[0].style, Text::legacy("§aL").spans[0].style);
|
||||
let (fitted, _) = o.option_label_within("LEVEL", 14.0, measure);
|
||||
assert_eq!(fitted.plain(), "Level of...: 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -258,19 +258,10 @@ fn root_style(component: &Tag) -> Vec<(&'static str, Tag)> {
|
||||
.filter_map(|key| Some((*key, root?.get(key)?.clone())))
|
||||
.collect()
|
||||
}
|
||||
/// Plain text of a stored text component (string, `{text: ...}` or JSON string).
|
||||
/// `ComponentSerialization.CODEC`: modern NBT strings are literal text, not JSON.
|
||||
fn component_text(tag: &Tag) -> String {
|
||||
match tag {
|
||||
Tag::String(s) => {
|
||||
let s = s.to_string_lossy();
|
||||
match serde_json::from_str::<serde_json::Value>(&s) {
|
||||
Ok(serde_json::Value::String(text)) => text,
|
||||
Ok(v) if v.get("text").is_some() => {
|
||||
v["text"].as_str().unwrap_or_default().to_owned()
|
||||
}
|
||||
_ => s,
|
||||
}
|
||||
}
|
||||
Tag::String(s) => s.to_string_lossy(),
|
||||
// Component.getString: translations and key binds resolved.
|
||||
Tag::Compound(_) | Tag::List(_) => {
|
||||
scoreboard::comp_text(&scoreboard::tag_to_json(tag)).plain()
|
||||
@@ -323,6 +314,35 @@ mod tests {
|
||||
decoded.decode(&tag);
|
||||
assert_eq!(decoded, data);
|
||||
}
|
||||
#[test]
|
||||
fn modern_string_messages_keep_literal_quotes_and_json_text() {
|
||||
let lines = [
|
||||
"plain text",
|
||||
"\"quoted text\"",
|
||||
r#"{"text":"literal object","bold":true}"#,
|
||||
"",
|
||||
];
|
||||
let mut saved = styled_sign();
|
||||
let messages = Tag::List(lines.map(|line| Tag::String(line.into())).to_vec());
|
||||
if let Tag::Compound(front) = saved.get_mut("front_text").unwrap() {
|
||||
front.insert("messages".into(), messages.clone());
|
||||
}
|
||||
let mut data = SignData::default();
|
||||
data.decode(&saved);
|
||||
assert_eq!(data.front.lines, lines.map(str::to_owned));
|
||||
let encoded = Tag::compound(data.encode());
|
||||
assert_eq!(
|
||||
encoded.get("front_text").unwrap().get("messages"),
|
||||
Some(&messages)
|
||||
);
|
||||
let mut copied = SignData::default();
|
||||
copied.decode(&encoded);
|
||||
assert_eq!(copied.front.lines, data.front.lines);
|
||||
assert_eq!(
|
||||
front_messages(&Tag::compound(copied.encode())),
|
||||
front_messages(&encoded)
|
||||
);
|
||||
}
|
||||
/// A sign as a server sends it: a green "Online" and a plain line, the
|
||||
/// way `ComponentSerialization.CODEC` writes them to NBT.
|
||||
fn styled_sign() -> Tag {
|
||||
|
||||
@@ -237,6 +237,7 @@ pub struct Resolver {
|
||||
done: std::sync::mpsc::Receiver<(NameOrId, Option<ResolvedProfile>)>,
|
||||
send: std::sync::mpsc::Sender<(NameOrId, Option<ResolvedProfile>)>,
|
||||
last_sweep: std::time::Instant,
|
||||
heads: HeadProfiles,
|
||||
}
|
||||
struct Entry {
|
||||
touched: std::time::Instant,
|
||||
@@ -304,6 +305,7 @@ impl Resolver {
|
||||
done,
|
||||
send,
|
||||
last_sweep: std::time::Instant::now(),
|
||||
heads: HeadProfiles::default(),
|
||||
}
|
||||
}
|
||||
/// `CacheBuilder.expireAfterAccess` with `maximumSize`.
|
||||
@@ -465,6 +467,64 @@ pub fn dynamic_nbt(profile: &horizon_world::nbt::Tag) -> Option<NameOrId> {
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_HEAD_PROFILES: usize = 4096;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct HeadProfile {
|
||||
info: ProfileInfo,
|
||||
skin: Option<(String, String)>,
|
||||
}
|
||||
|
||||
fn prepare_head(profile: &serde_json::Value) -> HeadProfile {
|
||||
let info = parse_profile(profile);
|
||||
let skin = info.textures.as_ref().and_then(|(value, signature)| {
|
||||
let url = unpack(value, *signature, &TEXTURE_URIS).skin?;
|
||||
let location = skin_location(&texture_hash(&url)?);
|
||||
Some((url, location))
|
||||
});
|
||||
HeadProfile { info, skin }
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct HeadProfiles {
|
||||
entries: std::collections::HashMap<serde_json::Value, std::sync::Arc<HeadProfile>>,
|
||||
nbt: std::collections::HashMap<horizon_world::nbt::Tag, std::sync::Arc<serde_json::Value>>,
|
||||
resolved: [std::collections::HashMap<String, std::sync::Arc<HeadProfile>>; 2],
|
||||
}
|
||||
|
||||
impl HeadProfiles {
|
||||
fn get(&mut self, profile: &serde_json::Value) -> std::sync::Arc<HeadProfile> {
|
||||
if let Some(parsed) = self.entries.get(profile) {
|
||||
return parsed.clone();
|
||||
}
|
||||
if self.entries.len() >= MAX_HEAD_PROFILES {
|
||||
self.entries.clear();
|
||||
}
|
||||
let parsed = std::sync::Arc::new(prepare_head(profile));
|
||||
self.entries.insert(profile.clone(), parsed.clone());
|
||||
parsed
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolver {
|
||||
/// Cache by NBT value rather than address: equipment can be replaced or
|
||||
/// mutated in place, and neither must leave a stale profile behind.
|
||||
pub fn head_profile_nbt(
|
||||
&mut self,
|
||||
profile: &horizon_world::nbt::Tag,
|
||||
) -> std::sync::Arc<serde_json::Value> {
|
||||
if let Some(json) = self.heads.nbt.get(profile) {
|
||||
return json.clone();
|
||||
}
|
||||
if self.heads.nbt.len() >= MAX_HEAD_PROFILES {
|
||||
self.heads.nbt.clear();
|
||||
}
|
||||
let json = std::sync::Arc::new(crate::scoreboard::tag_to_json(profile));
|
||||
self.heads.nbt.insert(profile.clone(), json.clone());
|
||||
json
|
||||
}
|
||||
}
|
||||
|
||||
/// `PlayerSkinRenderCache.getOrDefault` for a head: the resolved skin once
|
||||
/// its texture is in the atlas (`atlas`), else `partialProfile`'s default.
|
||||
pub fn head_skin(
|
||||
@@ -478,32 +538,54 @@ pub fn head_skin(
|
||||
{
|
||||
return sprite;
|
||||
}
|
||||
let p = parse_profile(profile);
|
||||
let property = p.textures.or_else(|| {
|
||||
let key = p.dynamic.as_ref()?;
|
||||
let p = resolver.heads.get(profile);
|
||||
// Dynamic resolution is deliberately not cached: pending answers and the
|
||||
// resolver's expiring service results must still advance on every draw.
|
||||
let resolved = p.info.dynamic.as_ref().and_then(|key| {
|
||||
if p.info.textures.is_some() {
|
||||
return None;
|
||||
}
|
||||
match resolver.state(key) {
|
||||
Resolve::Done(Some(profile)) => profile.textures(),
|
||||
Resolve::Done(Some(profile)) => {
|
||||
let (_, value, signature) = profile
|
||||
.properties
|
||||
.iter()
|
||||
.find(|(n, _, _)| n == "textures")?;
|
||||
let cache = &mut resolver.heads.resolved[usize::from(signature.is_some())];
|
||||
if let Some(parsed) = cache.get(value) {
|
||||
return Some(parsed.clone());
|
||||
}
|
||||
if cache.len() >= MAX_HEAD_PROFILES {
|
||||
cache.clear();
|
||||
}
|
||||
let property = if signature.is_some() {
|
||||
serde_json::json!({"name": "textures", "value": value, "signature": ""})
|
||||
} else {
|
||||
serde_json::json!({"name": "textures", "value": value})
|
||||
};
|
||||
let parsed = std::sync::Arc::new(prepare_head(
|
||||
&serde_json::json!({"properties": [property]}),
|
||||
));
|
||||
cache.insert(value.clone(), parsed.clone());
|
||||
Some(parsed)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
if let Some((value, signature)) = property
|
||||
&& let Some(url) = unpack(&value, signature, &TEXTURE_URIS).skin
|
||||
&& let Some(hash) = texture_hash(&url)
|
||||
{
|
||||
let location = skin_location(&hash);
|
||||
match downloads.state(Kind::Skin, &url) {
|
||||
Download::Ready if atlas(&location) => {
|
||||
downloads.drawn(&location);
|
||||
return location;
|
||||
if let Some((url, location)) = resolved.as_deref().unwrap_or(&p).skin.as_ref() {
|
||||
match downloads.state(Kind::Skin, url) {
|
||||
Download::Ready if atlas(location) => {
|
||||
downloads.drawn(location);
|
||||
return location.clone();
|
||||
}
|
||||
// Its slot was evicted: offer the image again.
|
||||
Download::Ready => {
|
||||
downloads.again(&location);
|
||||
downloads.again(location);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
crate::tab_list::default_skin(p.id).to_owned()
|
||||
crate::tab_list::default_skin(p.info.id).to_owned()
|
||||
}
|
||||
|
||||
/// `PlayerSkin.Patch.apply` for a head: the sprite of a profile's `texture`
|
||||
@@ -1137,6 +1219,80 @@ pub fn sha1(data: &[u8]) -> [u8; 20] {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn head_profile_cache_matches_uncached_and_is_bounded() {
|
||||
use base64::Engine;
|
||||
let mut cache = super::HeadProfiles::default();
|
||||
for (url, slim) in [
|
||||
("https://textures.minecraft.net/texture/abc", false),
|
||||
("https://textures.minecraft.net/texture/abc", true),
|
||||
("https://evil.example/texture/abc", false),
|
||||
] {
|
||||
let payload = serde_json::json!({"textures": {
|
||||
"SKIN": {"url": url, "metadata": {"model": if slim { "slim" } else { "classic" }}},
|
||||
"CAPE": {"url": "http://textures.minecraft.net/texture/cape"}
|
||||
}});
|
||||
let value = base64::engine::general_purpose::STANDARD.encode(payload.to_string());
|
||||
for signed in [false, true] {
|
||||
let mut property = serde_json::json!({"name": "textures", "value": value});
|
||||
if signed {
|
||||
property["signature"] = serde_json::Value::Null;
|
||||
}
|
||||
let profile = serde_json::json!({"properties": [property]});
|
||||
let parsed = cache.get(&profile);
|
||||
assert_eq!(*parsed, super::prepare_head(&profile));
|
||||
let textures = super::unpack(
|
||||
&value,
|
||||
if signed {
|
||||
super::Signature::Signed
|
||||
} else {
|
||||
super::Signature::Unsigned
|
||||
},
|
||||
&super::TEXTURE_URIS,
|
||||
);
|
||||
assert_eq!(textures.slim, slim && !url.contains("evil"));
|
||||
assert_eq!(textures.cape.is_some(), !url.contains("evil"));
|
||||
assert_eq!(parsed.skin.is_some(), !url.contains("evil"));
|
||||
assert!(std::sync::Arc::ptr_eq(&parsed, &cache.get(&profile)));
|
||||
}
|
||||
}
|
||||
for profile in [
|
||||
serde_json::json!({"properties": [{"name": "textures", "value": "!bad"}]}),
|
||||
serde_json::json!({"name": "Notch"}),
|
||||
serde_json::json!({}),
|
||||
] {
|
||||
let parsed = cache.get(&profile);
|
||||
assert_eq!(*parsed, super::prepare_head(&profile));
|
||||
assert!(parsed.skin.is_none());
|
||||
assert!(std::sync::Arc::ptr_eq(&parsed, &cache.get(&profile)));
|
||||
}
|
||||
cache.entries.clear();
|
||||
for i in 0..super::MAX_HEAD_PROFILES {
|
||||
cache.get(&serde_json::json!({"id": i}));
|
||||
}
|
||||
assert_eq!(cache.entries.len(), super::MAX_HEAD_PROFILES);
|
||||
cache.get(&serde_json::json!({}));
|
||||
assert_eq!(cache.entries.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn head_profile_nbt_cache_tracks_value_changes() {
|
||||
let mut resolver = super::Resolver::new();
|
||||
let mut tag = horizon_world::nbt::Tag::String("Notch".into());
|
||||
let first = resolver.head_profile_nbt(&tag);
|
||||
assert_eq!(*first, crate::scoreboard::tag_to_json(&tag));
|
||||
assert!(std::sync::Arc::ptr_eq(
|
||||
&first,
|
||||
&resolver.head_profile_nbt(&tag)
|
||||
));
|
||||
tag = horizon_world::nbt::Tag::String("Alex".into());
|
||||
assert_eq!(*resolver.head_profile_nbt(&tag), serde_json::json!("Alex"));
|
||||
for i in 0..super::MAX_HEAD_PROFILES {
|
||||
resolver.head_profile_nbt(&horizon_world::nbt::Tag::Int(i as i32));
|
||||
}
|
||||
assert!(resolver.heads.nbt.len() < super::MAX_HEAD_PROFILES);
|
||||
}
|
||||
|
||||
/// A profile's `texture` names a pack texture, never a file outside the
|
||||
/// packs: bad characters and `..` segments draw nothing.
|
||||
#[test]
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::sound_events::{INTENTIONALLY_EMPTY, Sentinel, Sound, SoundRegistry};
|
||||
use horizon_core::random::LegacyRandom;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, RwLock},
|
||||
sync::{Arc, OnceLock, RwLock},
|
||||
};
|
||||
|
||||
/// `SoundSource` indices (vanilla order).
|
||||
@@ -593,19 +593,25 @@ impl SoundInstance {
|
||||
}
|
||||
/// `AbstractSoundInstance.getVolume` (after `getOrResolve`).
|
||||
pub fn get_volume(&mut self) -> f32 {
|
||||
self.volume_with(self.volume)
|
||||
}
|
||||
fn volume_with(&mut self, volume: f32) -> f32 {
|
||||
let sample = match &self.sound {
|
||||
Some(s) => s.volume.sample(&mut self.random),
|
||||
None => 1.0,
|
||||
};
|
||||
self.volume * sample
|
||||
volume * sample
|
||||
}
|
||||
/// `AbstractSoundInstance.getPitch`.
|
||||
pub fn get_pitch(&mut self) -> f32 {
|
||||
self.pitch_with(self.pitch)
|
||||
}
|
||||
fn pitch_with(&mut self, pitch: f32) -> f32 {
|
||||
let sample = match &self.sound {
|
||||
Some(s) => s.pitch.sample(&mut self.random),
|
||||
None => 1.0,
|
||||
};
|
||||
self.pitch * sample
|
||||
pitch * sample
|
||||
}
|
||||
/// `AbstractTickableSoundInstance.stop`.
|
||||
fn stop(&mut self) {
|
||||
@@ -872,9 +878,36 @@ struct Handle {
|
||||
channel: Option<ChannelId>,
|
||||
streaming: bool,
|
||||
stopped: bool,
|
||||
/// Restore the unedited pitch when a later refresh no longer changes it.
|
||||
pitch_edited: bool,
|
||||
}
|
||||
/// One operation about to apply a sound's parameters on the game thread.
|
||||
/// `starting` is true for initial/delayed/loop-replay starts, false for live
|
||||
/// tickable or category-volume updates. The identifier is the sound event,
|
||||
/// not its selected file; source is the vanilla SoundSource index.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct SoundPlayback<'a> {
|
||||
/// Engine-local handle; not unique across split-screen engines.
|
||||
pub instance: InstanceId,
|
||||
pub identifier: &'a str,
|
||||
pub source: usize,
|
||||
pub position: [f64; 3],
|
||||
pub relative: bool,
|
||||
pub starting: bool,
|
||||
}
|
||||
pub type PlaybackHook = fn(&SoundPlayback<'_>, &mut f32, &mut f32, &mut bool);
|
||||
static PLAYBACK_HOOK: OnceLock<PlaybackHook> = OnceLock::new();
|
||||
|
||||
/// Install native playback dispatch before constructing client audio engines.
|
||||
/// No audio/device thread invokes this callback.
|
||||
pub fn install_playback_hook(hook: PlaybackHook) -> Result<(), PlaybackHook> {
|
||||
PLAYBACK_HOOK.set(hook)
|
||||
}
|
||||
|
||||
pub struct SoundEngine {
|
||||
pub registry: SoundRegistry,
|
||||
playback_hook: Option<PlaybackHook>,
|
||||
/// `loaded`: the library opened.
|
||||
pub loaded: bool,
|
||||
tick_count: i32,
|
||||
@@ -912,6 +945,7 @@ impl SoundEngine {
|
||||
pub fn new(registry: SoundRegistry) -> Self {
|
||||
Self {
|
||||
registry,
|
||||
playback_hook: PLAYBACK_HOOK.get().copied(),
|
||||
loaded: true,
|
||||
tick_count: 0,
|
||||
instances: HashMap::new(),
|
||||
@@ -934,6 +968,42 @@ impl SoundEngine {
|
||||
notifications: Vec::new(),
|
||||
}
|
||||
}
|
||||
/// Isolate a fixture's dispatch from process-wide native installation.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn set_test_playback_hook(&mut self, hook: PlaybackHook) {
|
||||
self.playback_hook = Some(hook);
|
||||
}
|
||||
fn playback_parameters(&self, id: InstanceId, starting: bool) -> Option<(f32, f32)> {
|
||||
let instance = self.instances.get(&id)?;
|
||||
let (mut volume, mut pitch) = (instance.volume, instance.pitch);
|
||||
if let Some(hook) = self.playback_hook {
|
||||
let sound = SoundPlayback {
|
||||
instance: id,
|
||||
identifier: &instance.identifier,
|
||||
source: instance.source,
|
||||
position: instance.position,
|
||||
relative: instance.relative,
|
||||
starting,
|
||||
};
|
||||
let mut cancel = false;
|
||||
hook(&sound, &mut volume, &mut pitch, &mut cancel);
|
||||
let volume_edited = volume.to_bits() != instance.volume.to_bits();
|
||||
let pitch_edited = pitch.to_bits() != instance.pitch.to_bits();
|
||||
if cancel
|
||||
|| (volume_edited && (!volume.is_finite() || volume < 0.0))
|
||||
|| (pitch_edited && (!pitch.is_finite() || pitch <= 0.0))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some((volume, pitch))
|
||||
}
|
||||
fn cancel_playback(&mut self, id: InstanceId, backend: &mut impl Backend) {
|
||||
if let Some(instance) = self.instances.get_mut(&id) {
|
||||
instance.stop();
|
||||
}
|
||||
self.stop(id, backend);
|
||||
}
|
||||
/// Hand an instance to the engine. A retained instance stays until
|
||||
/// `release`; others go once the engine no longer refers to them.
|
||||
pub fn add(&mut self, instance: SoundInstance, retain: bool) -> InstanceId {
|
||||
@@ -1027,10 +1097,27 @@ impl SoundEngine {
|
||||
continue;
|
||||
};
|
||||
if source == instance.source || source == MASTER {
|
||||
let (v, s) = (instance.get_volume(), instance.source);
|
||||
let original_pitch = instance.pitch;
|
||||
let Some((base_volume, base_pitch)) = self.playback_parameters(id, false) else {
|
||||
self.cancel_playback(id, backend);
|
||||
continue;
|
||||
};
|
||||
let instance = self.instances.get_mut(&id).expect("present");
|
||||
let (v, s) = (instance.volume_with(base_volume), instance.source);
|
||||
let pitch_edited = base_pitch.to_bits() != original_pitch.to_bits();
|
||||
// Restore a prior edit, but an always-no-op hook samples exactly as before.
|
||||
let restore_pitch = self.handles.get(&handle).is_some_and(|h| h.pitch_edited);
|
||||
let pitch = (pitch_edited || restore_pitch)
|
||||
.then(|| Self::calculate_pitch(instance.pitch_with(base_pitch)));
|
||||
let volume = self.calculate_volume(v, s);
|
||||
if let Some(h) = self.handles.get_mut(&handle) {
|
||||
h.pitch_edited = pitch_edited;
|
||||
}
|
||||
if let Some(c) = self.handles.get(&handle).and_then(handle_channel) {
|
||||
backend.set_volume(c, volume);
|
||||
if let Some(pitch) = pitch {
|
||||
backend.set_pitch(c, pitch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1207,8 +1294,11 @@ impl SoundEngine {
|
||||
if sound.sentinel != Sentinel::None {
|
||||
return PlayResult::NotStarted;
|
||||
}
|
||||
let Some((base_volume, base_pitch)) = self.playback_parameters(id, true) else {
|
||||
return PlayResult::NotStarted;
|
||||
};
|
||||
let instance = self.instances.get_mut(&id).expect("present");
|
||||
let instance_volume = instance.get_volume();
|
||||
let instance_volume = instance.volume_with(base_volume);
|
||||
let attenuation_distance = sound.attenuation_distance(instance_volume);
|
||||
if self.listening {
|
||||
self.notify(id, subtitle, attenuation_distance);
|
||||
@@ -1252,6 +1342,7 @@ impl SoundEngine {
|
||||
channel: Some(channel),
|
||||
streaming,
|
||||
stopped: false,
|
||||
pitch_edited: base_pitch.to_bits() != self.instances[&id].pitch.to_bits(),
|
||||
},
|
||||
);
|
||||
self.delete_time.insert(id, self.tick_count + 20);
|
||||
@@ -1264,7 +1355,7 @@ impl SoundEngine {
|
||||
self.instance_by_source.push((source, id));
|
||||
}
|
||||
let instance = self.instances.get_mut(&id).expect("present");
|
||||
let pitch = Self::calculate_pitch(instance.get_pitch());
|
||||
let pitch = Self::calculate_pitch(instance.pitch_with(base_pitch));
|
||||
let (attenuation, relative, tickable) =
|
||||
(instance.attenuation, instance.relative, instance.tickable());
|
||||
backend.set_pitch(channel, pitch);
|
||||
@@ -1350,13 +1441,22 @@ impl SoundEngine {
|
||||
self.stop(id, backend);
|
||||
continue;
|
||||
}
|
||||
let (v, source) = (instance.get_volume(), instance.source);
|
||||
let pitch = Self::calculate_pitch(instance.get_pitch());
|
||||
let Some((base_volume, base_pitch)) = self.playback_parameters(id, false) else {
|
||||
self.cancel_playback(id, backend);
|
||||
continue;
|
||||
};
|
||||
let instance = self.instances.get_mut(&id).expect("present");
|
||||
let pitch_edited = base_pitch.to_bits() != instance.pitch.to_bits();
|
||||
let (v, source) = (instance.volume_with(base_volume), instance.source);
|
||||
let pitch = Self::calculate_pitch(instance.pitch_with(base_pitch));
|
||||
let position = instance.position;
|
||||
let volume = self.calculate_volume(v, source);
|
||||
let Some(handle) = self.channel_of(id) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(h) = self.handles.get_mut(&handle) {
|
||||
h.pitch_edited = pitch_edited;
|
||||
}
|
||||
if let Some(c) = self.handles.get(&handle).and_then(handle_channel) {
|
||||
backend.set_volume(c, volume);
|
||||
backend.set_pitch(c, pitch);
|
||||
@@ -1370,7 +1470,7 @@ impl SoundEngine {
|
||||
&& let (Some(sound), Some(subtitle)) =
|
||||
(instance.sound.clone(), instance.event.clone())
|
||||
{
|
||||
let distance = sound.attenuation_distance(instance.get_volume());
|
||||
let distance = sound.attenuation_distance(instance.volume_with(base_volume));
|
||||
self.notify(id, subtitle, distance);
|
||||
}
|
||||
}
|
||||
@@ -1995,3 +2095,7 @@ mod entity_bound_tests {
|
||||
assert_ne!(sound.position, replacement.state().position);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sound_engine/playback_tests.rs"]
|
||||
mod playback_tests;
|
||||
@@ -0,0 +1,188 @@
|
||||
use super::*;
|
||||
use horizon_core::random::RandomSource;
|
||||
|
||||
fn setup() -> (SoundEngine, test_backend::Null) {
|
||||
let mut registry = SoundRegistry::default();
|
||||
registry.add_file(
|
||||
"test",
|
||||
br#"{"beep":{"subtitle":"test.beep","sounds":[{"name":"beep","volume":0.5,"pitch":0.5}]}}"#,
|
||||
&|_, _| true,
|
||||
);
|
||||
let mut engine = SoundEngine::new(registry);
|
||||
engine.playback_hook = None;
|
||||
engine.listening = true;
|
||||
(engine, test_backend::Null::default())
|
||||
}
|
||||
fn instance() -> SoundInstance {
|
||||
SoundInstance::at(
|
||||
"test:beep",
|
||||
4,
|
||||
0.8,
|
||||
1.2,
|
||||
LegacyRandom::new(1),
|
||||
[2.0, 3.0, 4.0],
|
||||
)
|
||||
}
|
||||
fn adjust(sound: &SoundPlayback<'_>, volume: &mut f32, pitch: &mut f32, _: &mut bool) {
|
||||
assert_eq!(sound.identifier, "test:beep");
|
||||
assert_eq!(sound.source, 4);
|
||||
assert!(!sound.relative);
|
||||
*volume *= 0.5;
|
||||
*pitch = 2.0;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn played_parameters_reach_channels_without_changing_the_instance() {
|
||||
let (mut engine, mut backend) = setup();
|
||||
engine.set_test_playback_hook(adjust);
|
||||
let (id, result) = engine.play_new(instance(), true, &mut backend);
|
||||
assert_eq!(result, PlayResult::Started);
|
||||
let channel = engine.channel(id).unwrap();
|
||||
assert_eq!(backend.volume[&channel], 0.2);
|
||||
assert_eq!(backend.pitch[&channel], 1.0);
|
||||
assert_eq!(backend.position[&channel], [2.0, 3.0, 4.0]);
|
||||
assert_eq!(engine.instances[&id].volume, 0.8);
|
||||
assert_eq!(engine.instances[&id].pitch, 1.2);
|
||||
assert_eq!(engine.notifications.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delayed_and_manually_looped_plays_do_not_compound_edits() {
|
||||
let (mut engine, mut backend) = setup();
|
||||
engine.set_test_playback_hook(adjust);
|
||||
let mut sound = instance();
|
||||
sound.looping = true;
|
||||
sound.delay = 2;
|
||||
let id = engine.add(sound, true);
|
||||
engine.play_delayed(id, 2);
|
||||
engine.tick(false, &TickContext::default(), &mut backend);
|
||||
assert!(backend.volume.is_empty() && engine.notifications.is_empty());
|
||||
engine.tick(false, &TickContext::default(), &mut backend);
|
||||
let first = engine.channel(id).unwrap();
|
||||
assert_eq!(backend.volume[&first], 0.2);
|
||||
backend.stopped.insert(first, true);
|
||||
for _ in 0..22 {
|
||||
engine.tick(false, &TickContext::default(), &mut backend);
|
||||
}
|
||||
let replay = engine.channel(id).unwrap();
|
||||
assert_ne!(first, replay);
|
||||
assert_eq!(backend.volume[&replay], 0.2);
|
||||
assert_eq!(engine.instances[&id].volume, 0.8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tickable_and_category_updates_keep_transient_edits_and_can_cancel() {
|
||||
let (mut engine, mut backend) = setup();
|
||||
engine.set_test_playback_hook(adjust);
|
||||
let mut sound = instance();
|
||||
sound.ticking = Ticking::Native;
|
||||
let (id, _) = engine.play_new(sound, true, &mut backend);
|
||||
let channel = engine.channel(id).unwrap();
|
||||
let sound = engine.instance_mut(id).unwrap();
|
||||
sound.volume = 0.4;
|
||||
sound.position = [8.0, 9.0, 10.0];
|
||||
engine.tick(false, &TickContext::default(), &mut backend);
|
||||
assert_eq!(backend.volume[&channel], 0.1);
|
||||
assert_eq!(backend.position[&channel], [8.0, 9.0, 10.0]);
|
||||
engine.set_test_playback_hook(|_, volume, _, _| *volume *= 0.5);
|
||||
engine.update_category_volume(4, 0.5, &mut backend);
|
||||
assert_eq!(backend.volume[&channel], 0.05);
|
||||
assert_eq!(
|
||||
backend.pitch[&channel], 0.6,
|
||||
"a later no-pitch edit restores the base pitch"
|
||||
);
|
||||
engine.notifications.clear();
|
||||
engine.set_test_playback_hook(|sound, _, _, cancel| {
|
||||
assert!(!sound.starting);
|
||||
*cancel = true;
|
||||
});
|
||||
engine.tick(false, &TickContext::default(), &mut backend);
|
||||
assert!(backend.stopped(channel));
|
||||
assert!(engine.notifications.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancellation_and_invalid_edits_never_allocate_or_notify() {
|
||||
let edits: [PlaybackHook; 6] = [
|
||||
|_, _, _, cancel| *cancel = true,
|
||||
|_, volume, _, _| *volume = f32::NAN,
|
||||
|_, volume, _, _| *volume = f32::INFINITY,
|
||||
|_, volume, _, _| *volume = -1.0,
|
||||
|_, _, pitch, _| *pitch = 0.0,
|
||||
|_, _, pitch, _| *pitch = f32::NEG_INFINITY,
|
||||
];
|
||||
for hook in edits {
|
||||
let (mut engine, mut backend) = setup();
|
||||
engine.set_test_playback_hook(hook);
|
||||
let (_, result) = engine.play_new(instance(), false, &mut backend);
|
||||
assert_eq!(result, PlayResult::NotStarted);
|
||||
assert!(backend.stopped.is_empty() && engine.notifications.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_op_dispatch_preserves_random_sampling_and_failed_pool_behavior() {
|
||||
let (mut plain, mut plain_backend) = setup();
|
||||
let (mut hooked, mut hooked_backend) = setup();
|
||||
hooked.set_test_playback_hook(|_, _, _, _| {});
|
||||
for (engine, backend) in [
|
||||
(&mut plain, &mut plain_backend),
|
||||
(&mut hooked, &mut hooked_backend),
|
||||
] {
|
||||
engine.limits = [0, 0];
|
||||
let id = engine.add(instance(), true);
|
||||
assert_eq!(engine.play(id, backend), PlayResult::NotStarted);
|
||||
engine.limits = [247, 8];
|
||||
assert_eq!(engine.play(id, backend), PlayResult::Started);
|
||||
}
|
||||
assert_eq!(plain_backend.volume, hooked_backend.volume);
|
||||
assert_eq!(plain_backend.pitch, hooked_backend.pitch);
|
||||
assert_eq!(plain.notifications, hooked.notifications);
|
||||
assert_eq!(
|
||||
plain.instances.get_mut(&1).unwrap().random.next_long(),
|
||||
hooked.instances.get_mut(&1).unwrap().random.next_long()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entity_bound_updates_are_intercepted_at_the_new_position() {
|
||||
let (mut engine, mut backend) = setup();
|
||||
let entity = SoundEntity::new(42, [2.0, 3.0, 4.0], false);
|
||||
engine.set_test_playback_hook(|sound, volume, _, _| {
|
||||
if !sound.starting {
|
||||
assert_eq!(sound.position, [8.0, 9.0, 10.0]);
|
||||
}
|
||||
*volume *= 0.5;
|
||||
});
|
||||
let sound = SoundInstance::entity_bound(
|
||||
"test:beep",
|
||||
4,
|
||||
0.8,
|
||||
1.0,
|
||||
LegacyRandom::new(1),
|
||||
entity.clone(),
|
||||
);
|
||||
let (id, _) = engine.play_new(sound, true, &mut backend);
|
||||
entity.update(SoundEntityState {
|
||||
position: [8.0, 9.0, 10.0],
|
||||
silent: false,
|
||||
removed: false,
|
||||
});
|
||||
engine.tick(false, &TickContext::default(), &mut backend);
|
||||
let channel = engine.channel(id).unwrap();
|
||||
assert_eq!(backend.position[&channel], [8.0, 9.0, 10.0]);
|
||||
assert_eq!(backend.volume[&channel], 0.2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_volume_only_edit_preserves_legacy_zero_pitch_clamping() {
|
||||
let (mut engine, mut backend) = setup();
|
||||
engine.set_test_playback_hook(|_, volume, _, _| *volume *= 0.5);
|
||||
let mut sound = instance();
|
||||
sound.pitch = 0.0;
|
||||
let (id, result) = engine.play_new(sound, true, &mut backend);
|
||||
assert_eq!(result, PlayResult::Started);
|
||||
let channel = engine.channel(id).unwrap();
|
||||
assert_eq!(backend.volume[&channel], 0.2);
|
||||
assert_eq!(backend.pitch[&channel], 0.5);
|
||||
}
|
||||
@@ -544,6 +544,12 @@ pub struct Streaming {
|
||||
/// Chunks come from a server (`insert_remote`/`forget`): nothing is
|
||||
/// generated and columns are only unloaded when the server says so.
|
||||
pub remote: bool,
|
||||
/// This server sends bulk block edits as whole columns whose light is
|
||||
/// from before its relight (a Horizon server): a resent resident column
|
||||
/// is lit here. Any other server's packet carries the light vanilla
|
||||
/// applies, so it is not lit again (a proxy's next server resends its
|
||||
/// whole world at the same coordinates).
|
||||
pub relight_resent: bool,
|
||||
/// More areas to keep loaded (centre chunk, radius), e.g. around the
|
||||
/// players of a server. Each is a square, like vanilla's ticket levels:
|
||||
/// chunks finish out to `radius + 1` (Chebyshev) and generation reaches
|
||||
@@ -1108,6 +1114,7 @@ impl Streaming {
|
||||
generated_total: 0,
|
||||
meshed_total: 0,
|
||||
remote: false,
|
||||
relight_resent: false,
|
||||
extra: Vec::new(),
|
||||
cover: None,
|
||||
mesh: true,
|
||||
@@ -1154,14 +1161,15 @@ impl Streaming {
|
||||
}
|
||||
/// A resident column a server sent again whole (it changed it in bulk):
|
||||
/// its new blocks taken, the sections that did not change kept as they
|
||||
/// were, and its light and its neighbours' worked out again here, off
|
||||
/// the game thread, as light is for the blocks a server changes one at a
|
||||
/// time; the sections that changed remesh (with theirs) once lit. Until
|
||||
/// then the sections that changed show the light the packet carried (a
|
||||
/// Horizon server sends the light from before its relight, then the
|
||||
/// new light; another server's is already right, which matters where
|
||||
/// the neighbours this light needs aren't all here).
|
||||
pub fn replace_remote(&mut self, world: &mut World, mut chunk: Chunk) {
|
||||
/// were, and (for a Horizon server's bulk edit) its light and its
|
||||
/// neighbours' worked out again here, off the game thread, as light is for
|
||||
/// the blocks a server changes one at a time; the sections that changed
|
||||
/// remesh (with theirs) once lit. Until then the sections that changed
|
||||
/// show the light the packet carried (a Horizon server sends the light
|
||||
/// from before its relight, then the new light; another server's is already
|
||||
/// right, which matters where the neighbours this light needs aren't all
|
||||
/// here).
|
||||
pub fn replace_remote(&mut self, world: &mut World, chunk: Chunk) {
|
||||
let key = (chunk.x, chunk.z);
|
||||
let Some(old) = world.chunks.get(&key).cloned() else {
|
||||
return self.insert_remote(world, chunk);
|
||||
@@ -1169,34 +1177,7 @@ impl Streaming {
|
||||
if !self.lit.contains(&key) {
|
||||
return self.insert_remote(world, chunk);
|
||||
}
|
||||
// The cells whose blocks changed, by section: they remesh with the neighbours within a block of them (as
|
||||
// vanilla's `setBlocksDirty`), not every section round each section changed (which, for the few blocks a
|
||||
// section often changes in a bulk edit, is mostly sections that did not).
|
||||
let mut cells = crate::light::LightChanges::default();
|
||||
cells.keyed(world.layout());
|
||||
let kept = keep_unchanged_sections(&old, &mut chunk, &mut cells);
|
||||
chunk.light_loaded = old.light_loaded;
|
||||
chunk.lit = true;
|
||||
world.chunks.insert(key, Arc::new(chunk));
|
||||
world.revision = world.revision.wrapping_add(1);
|
||||
// A section with new biomes and the same blocks remeshes now (its tint), with its neighbours, whose tint
|
||||
// blends across the border: above and below too, as a block's tint can sample the biome quart of the
|
||||
// section next to it (`BiomeManager.getBiome`'s zoom reaches a quart beyond the block).
|
||||
Self::light_updated(world, key.0, key.1, &kept.biomes);
|
||||
// A section with the same blocks and new light takes the packet's light (vanilla's
|
||||
// `handleLevelChunkWithLight` always applies it) and remeshes as a light update would.
|
||||
Self::light_updated(world, key.0, key.1, &kept.light);
|
||||
if !kept.blocks {
|
||||
return;
|
||||
}
|
||||
for dz in -1..=1 {
|
||||
for dx in -1..=1 {
|
||||
world.dirty_light.insert((key.0 + dx, key.1 + dz));
|
||||
}
|
||||
}
|
||||
cells.dirty_sections(|section| {
|
||||
world.dirty_sections.insert(section);
|
||||
});
|
||||
take_resent_column(world, key, &old, chunk, self.relight_resent);
|
||||
}
|
||||
/// A server's light update for a resident column: the sections whose
|
||||
/// light changed and their neighbours remesh
|
||||
@@ -3046,6 +3027,51 @@ fn keep_unchanged_sections(
|
||||
}
|
||||
out
|
||||
}
|
||||
/// Takes the server's new `chunk` over the resident column `old`: the sections
|
||||
/// whose blocks did not change keep `old`'s (with `chunk`'s biomes and light),
|
||||
/// and the sections whose blocks changed remesh. `relight` says the packet's
|
||||
/// light is from before the server's relight (a Horizon server's bulk edit), so
|
||||
/// the column and its neighbours are lit here; another server's packet already
|
||||
/// carries the light vanilla applies, so no light job is queued for it.
|
||||
fn take_resent_column(
|
||||
world: &mut World,
|
||||
key: (i32, i32),
|
||||
old: &Chunk,
|
||||
mut chunk: Chunk,
|
||||
relight: bool,
|
||||
) {
|
||||
// The cells whose blocks changed, by section: they remesh with the neighbours within a block of them (as
|
||||
// vanilla's `setBlocksDirty`), not every section round each section changed (which, for the few blocks a
|
||||
// section often changes in a bulk edit, is mostly sections that did not).
|
||||
let mut cells = crate::light::LightChanges::default();
|
||||
cells.keyed(world.layout());
|
||||
let kept = keep_unchanged_sections(old, &mut chunk, &mut cells);
|
||||
chunk.light_loaded = old.light_loaded;
|
||||
chunk.lit = true;
|
||||
world.chunks.insert(key, Arc::new(chunk));
|
||||
world.revision = world.revision.wrapping_add(1);
|
||||
// A section with new biomes and the same blocks remeshes now (its tint), with its neighbours, whose tint
|
||||
// blends across the border: above and below too, as a block's tint can sample the biome quart of the
|
||||
// section next to it (`BiomeManager.getBiome`'s zoom reaches a quart beyond the block).
|
||||
Streaming::light_updated(world, key.0, key.1, &kept.biomes);
|
||||
// A section with the same blocks and new light takes the packet's light (vanilla's
|
||||
// `handleLevelChunkWithLight` always applies it) and remeshes as a light update would.
|
||||
Streaming::light_updated(world, key.0, key.1, &kept.light);
|
||||
if !kept.blocks {
|
||||
return;
|
||||
}
|
||||
if relight {
|
||||
for dz in -1..=1 {
|
||||
for dx in -1..=1 {
|
||||
world.dirty_light.insert((key.0 + dx, key.1 + dz));
|
||||
}
|
||||
}
|
||||
}
|
||||
cells.dirty_sections(|section| {
|
||||
world.dirty_sections.insert(section);
|
||||
});
|
||||
}
|
||||
|
||||
/// Whether two sections' packed light (4,096 cells) holds the same values.
|
||||
fn same_light(a: &crate::chunk::Light, b: &crate::chunk::Light) -> bool {
|
||||
a.same_as(b)
|
||||
@@ -4215,6 +4241,42 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn resident_column(world: &mut World, light: u8) -> Chunk {
|
||||
let mut chunk = Chunk::empty(2, -3);
|
||||
chunk.set(1, 10, 1, 1);
|
||||
Arc::make_mut(&mut chunk.sections[0]).light = crate::chunk::Light::Uniform(light);
|
||||
chunk.lit = true;
|
||||
world.chunks.insert((2, -3), Arc::new(chunk.clone()));
|
||||
chunk
|
||||
}
|
||||
|
||||
/// A server's resent column uses the light in its packet, as vanilla's
|
||||
/// `handleLevelChunkWithLight` does: only a Horizon server's bulk edit,
|
||||
/// whose packet light is from before its relight, is relit here. A proxy's
|
||||
/// next server resends its whole world at the same coordinates, and
|
||||
/// relighting every column and its neighbours is work that world never
|
||||
/// asked for (#10273).
|
||||
#[test]
|
||||
fn a_resent_column_is_relit_only_for_a_horizon_servers_bulk_edit() {
|
||||
for relight in [false, true] {
|
||||
let mut world = World::new(horizon_world::registry::BlockStateId(0), 0);
|
||||
let old = resident_column(&mut world, 0x50);
|
||||
let mut new = old.clone();
|
||||
new.set(1, 10, 1, 2);
|
||||
Arc::make_mut(&mut new.sections[0]).light = crate::chunk::Light::Uniform(0xF0);
|
||||
take_resent_column(&mut world, (2, -3), &old, new, relight);
|
||||
let chunk = &world.chunks[&(2, -3)];
|
||||
assert_eq!(chunk.block(1, 10, 1).0, 2);
|
||||
// The packet's light is what the column shows.
|
||||
assert_eq!(chunk.sections[0].light.get(0), 0xF0);
|
||||
// The changed section remeshes either way.
|
||||
assert!(world.dirty_sections.contains(&(2, 0, -3)));
|
||||
// Only a Horizon bulk edit queues light work, for the column and
|
||||
// its eight neighbours.
|
||||
assert_eq!(world.dirty_light.len(), if relight { 9 } else { 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -12,7 +12,11 @@ use crate::{
|
||||
scoreboard::{Comp, NumberFormat, Objective, RenderType, Scoreboard, comp_text},
|
||||
text::Text,
|
||||
};
|
||||
use std::{borrow::Cow, cmp::Ordering, collections::HashMap};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::Ordering,
|
||||
collections::{HashMap, HashSet},
|
||||
};
|
||||
|
||||
/// `GameType.SPECTATOR` id.
|
||||
pub const SPECTATOR: i32 = 3;
|
||||
@@ -124,10 +128,18 @@ pub fn face(
|
||||
}
|
||||
}
|
||||
|
||||
/// `Hud.extractTabList`: whether the list shows. A single-player world
|
||||
/// shows it only with another listed player or a `list` objective.
|
||||
pub fn visible(key_down: bool, local_server: bool, listed: usize, objective: bool) -> bool {
|
||||
key_down && !(local_server && listed <= 1 && !objective)
|
||||
/// `Hud.extractTabList`: whether the list shows. `Hud.extractRenderState`
|
||||
/// only calls it inside `if (!isHidden)`, so F1 takes the list away whatever
|
||||
/// else holds it up. A single-player world shows it only with another listed
|
||||
/// player or a `list` objective.
|
||||
pub fn visible(
|
||||
hud_shown: bool,
|
||||
key_down: bool,
|
||||
local_server: bool,
|
||||
listed: usize,
|
||||
objective: bool,
|
||||
) -> bool {
|
||||
hud_shown && key_down && !(local_server && listed <= 1 && !objective)
|
||||
}
|
||||
|
||||
/// `Options.getBackgroundColor(553648127)`: the row background.
|
||||
@@ -146,12 +158,14 @@ fn compare_utf16(a: &str, b: &str) -> Ordering {
|
||||
/// `String.compareToIgnoreCase` (per code point: upper then lower case).
|
||||
fn compare_ignore_case(a: &str, b: &str) -> Ordering {
|
||||
fn simple(c: char, upper: bool) -> char {
|
||||
let mut it: Box<dyn Iterator<Item = char>> = if upper {
|
||||
Box::new(c.to_uppercase())
|
||||
let pair = if upper {
|
||||
let mut it = c.to_uppercase();
|
||||
(it.next(), it.next())
|
||||
} else {
|
||||
Box::new(c.to_lowercase())
|
||||
let mut it = c.to_lowercase();
|
||||
(it.next(), it.next())
|
||||
};
|
||||
match (it.next(), it.next()) {
|
||||
match pair {
|
||||
(Some(m), None) => m,
|
||||
_ => c,
|
||||
}
|
||||
@@ -181,23 +195,25 @@ fn compare_ignore_case(a: &str, b: &str) -> Ordering {
|
||||
/// list order first, spectators last, team name, profile name ignoring
|
||||
/// case), at most 80 players.
|
||||
pub fn sorted_players<'a>(players: &'a [TabPlayer], board: &Scoreboard) -> Vec<&'a TabPlayer> {
|
||||
let team = |p: &TabPlayer| {
|
||||
board
|
||||
.players_team(&p.name)
|
||||
.map_or("", |t| t.name.as_str())
|
||||
.to_owned()
|
||||
};
|
||||
let mut out: Vec<&TabPlayer> = players.iter().collect();
|
||||
out.sort_by(|a, b| {
|
||||
let mut out: Vec<_> = players
|
||||
.iter()
|
||||
.map(|p| {
|
||||
(
|
||||
p,
|
||||
board.players_team(&p.name).map_or("", |t| t.name.as_str()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
out.sort_by(|(a, team_a), (b, team_b)| {
|
||||
a.list_order
|
||||
.wrapping_neg()
|
||||
.cmp(&b.list_order.wrapping_neg())
|
||||
.then_with(|| (a.game_mode == SPECTATOR).cmp(&(b.game_mode == SPECTATOR)))
|
||||
.then_with(|| compare_utf16(&team(a), &team(b)))
|
||||
.then_with(|| compare_utf16(team_a, team_b))
|
||||
.then_with(|| compare_ignore_case(&a.name, &b.name))
|
||||
});
|
||||
out.truncate(80);
|
||||
out
|
||||
out.into_iter().map(|(p, _)| p).collect()
|
||||
}
|
||||
|
||||
/// What `setVisible(true)` has the narrator say:
|
||||
@@ -339,6 +355,26 @@ pub struct TabList {
|
||||
pub footer: Option<Text>,
|
||||
visible: bool,
|
||||
health: HashMap<u128, HealthState>,
|
||||
cache: Option<LayoutCache>,
|
||||
order: Option<(Vec<TabPlayer>, Scoreboard, Vec<u128>)>,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
struct LayoutCache {
|
||||
players: Vec<TabPlayer>,
|
||||
board: Scoreboard,
|
||||
objective: Option<Objective>,
|
||||
header: Option<Text>,
|
||||
footer: Option<Text>,
|
||||
language: Option<std::sync::Arc<crate::lang::Language>>,
|
||||
dimensions: (i32, bool, u32, u64),
|
||||
flags: Vec<(bool, bool)>,
|
||||
draws: Vec<GuiDraw>,
|
||||
hearts: Vec<(usize, i32, i32, i32, u128, i32)>,
|
||||
}
|
||||
impl std::fmt::Debug for LayoutCache {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("LayoutCache").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
struct Entry {
|
||||
name: Text,
|
||||
@@ -363,10 +399,87 @@ impl TabList {
|
||||
self.header = None;
|
||||
self.footer = None;
|
||||
}
|
||||
/// `extractRenderState`.
|
||||
/// Share the cached display order with skin residency selection.
|
||||
pub fn displayed_ids(&mut self, players: &[TabPlayer], board: &Scoreboard) -> &[u128] {
|
||||
if self.order.as_ref().is_none_or(|(old, previous, _)| {
|
||||
old.len() != players.len()
|
||||
|| old.iter().zip(players).any(|(a, b)| {
|
||||
a.uuid != b.uuid
|
||||
|| a.name != b.name
|
||||
|| a.list_order != b.list_order
|
||||
|| a.game_mode != b.game_mode
|
||||
})
|
||||
|| previous != board
|
||||
}) {
|
||||
let ids = sorted_players(players, board)
|
||||
.iter()
|
||||
.map(|p| p.uuid)
|
||||
.collect();
|
||||
self.order = Some((players.to_vec(), board.clone(), ids));
|
||||
}
|
||||
&self.order.as_ref().unwrap().2
|
||||
}
|
||||
/// `extractRenderState`: retain text, measurements and geometry, not animated hearts.
|
||||
pub fn layout(&mut self, i: &TabInputs, font: &Font) -> Vec<GuiDraw> {
|
||||
let language = crate::lang::current();
|
||||
let dimensions = (i.screen_width, i.online_mode, i.background, font.epoch());
|
||||
let flags: Vec<_> = i
|
||||
.players
|
||||
.iter()
|
||||
.map(|p| ((i.upside_down)(p.uuid), (i.horizon)(p.uuid)))
|
||||
.collect();
|
||||
let valid = self.cache.as_ref().is_some_and(|c| {
|
||||
c.players == i.players
|
||||
&& c.board == *i.board
|
||||
&& c.objective.as_ref() == i.objective
|
||||
&& c.header == self.header
|
||||
&& c.footer == self.footer
|
||||
&& c.dimensions == dimensions
|
||||
&& c.flags == flags
|
||||
&& match (&c.language, &language) {
|
||||
(Some(a), Some(b)) => std::sync::Arc::ptr_eq(a, b),
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
if !valid {
|
||||
let mut hearts = Vec::new();
|
||||
let draws = self.build_layout(i, font, &mut hearts);
|
||||
self.cache = Some(LayoutCache {
|
||||
players: i.players.to_vec(),
|
||||
board: i.board.clone(),
|
||||
objective: i.objective.cloned(),
|
||||
header: self.header.clone(),
|
||||
footer: self.footer.clone(),
|
||||
language,
|
||||
dimensions,
|
||||
flags,
|
||||
draws,
|
||||
hearts,
|
||||
});
|
||||
}
|
||||
let cache = self.cache.take().unwrap();
|
||||
let mut out = Vec::with_capacity(cache.draws.len());
|
||||
let mut start = 0;
|
||||
for &(at, y, left, right, id, score) in &cache.hearts {
|
||||
out.extend_from_slice(&cache.draws[start..at]);
|
||||
self.hearts(y, left, right, id, score, i.gui_ticks, font, &mut out);
|
||||
start = at;
|
||||
}
|
||||
out.extend_from_slice(&cache.draws[start..]);
|
||||
self.cache = Some(cache);
|
||||
out
|
||||
}
|
||||
fn build_layout(
|
||||
&mut self,
|
||||
i: &TabInputs,
|
||||
font: &Font,
|
||||
hearts: &mut Vec<(usize, i32, i32, i32, u128, i32)>,
|
||||
) -> Vec<GuiDraw> {
|
||||
let width = |t: &Text| font.width_text(t) as i32;
|
||||
let players = sorted_players(i.players, i.board);
|
||||
let ids = self.displayed_ids(i.players, i.board);
|
||||
let by_id: HashMap<_, _> = i.players.iter().map(|p| (p.uuid, p)).collect();
|
||||
let players: Vec<_> = ids.iter().map(|id| by_id[id]).collect();
|
||||
let spacer = font.width(" ") as i32;
|
||||
let mut max_name = 0;
|
||||
let mut max_score = 0;
|
||||
@@ -409,8 +522,8 @@ impl TabList {
|
||||
entries.push(entry);
|
||||
}
|
||||
if !self.health.is_empty() {
|
||||
self.health
|
||||
.retain(|id, _| players.iter().any(|p| p.uuid == *id));
|
||||
let displayed: HashSet<_> = players.iter().map(|p| p.uuid).collect();
|
||||
self.health.retain(|id, _| displayed.contains(id));
|
||||
}
|
||||
let slots = players.len() as i32;
|
||||
let mut rows = slots;
|
||||
@@ -524,16 +637,7 @@ impl TabList {
|
||||
let right = left + width_for_score;
|
||||
if right - left > 5 {
|
||||
if objective.render_type == RenderType::Hearts {
|
||||
self.hearts(
|
||||
yo,
|
||||
left,
|
||||
right,
|
||||
p.uuid,
|
||||
entry.score,
|
||||
i.gui_ticks,
|
||||
font,
|
||||
&mut out,
|
||||
);
|
||||
hearts.push((out.len(), yo, left, right, p.uuid, entry.score));
|
||||
} else if let Some(text) = &entry.formatted {
|
||||
out.push(GuiDraw::Text {
|
||||
text: text.clone(),
|
||||
@@ -683,6 +787,60 @@ mod tests {
|
||||
assert!(list.set_visible(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_tab_layout_matches_rebuilt_layout_and_animated_health() {
|
||||
let font = Font::metrics(std::path::Path::new("missing-tab-test-resources"));
|
||||
let mut board = Scoreboard::default();
|
||||
board.add_objective(
|
||||
"hp",
|
||||
"dummy",
|
||||
serde_json::json!("hp"),
|
||||
RenderType::Hearts,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
board.add_team("red");
|
||||
board.add_to_team("Alice", "red");
|
||||
board.score_mut("Alice", "hp").value = 20;
|
||||
let mut players = vec![TabPlayer::new(1, "Alice"), TabPlayer::new(2, "Bob")];
|
||||
let mut cached = TabList::default();
|
||||
let mut fresh = TabList::default();
|
||||
cached.header = Some(Text::literal("Header"));
|
||||
fresh.header = cached.header.clone();
|
||||
for frame in 0..40 {
|
||||
match frame {
|
||||
2 => {
|
||||
players[0].display_name =
|
||||
Some(serde_json::json!({"text": "Changed", "obfuscated": true}))
|
||||
}
|
||||
3 => board.team_mut("red").unwrap().prefix = serde_json::json!("prefix"),
|
||||
4 => board.score_mut("Alice", "hp").value = 9,
|
||||
5 => players.push(TabPlayer::new(3, "Charlie")),
|
||||
6 => {
|
||||
players.pop();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let inputs = TabInputs {
|
||||
players: &players,
|
||||
board: &board,
|
||||
objective: board.objective("hp"),
|
||||
screen_width: if frame < 7 { 400 } else { 300 },
|
||||
online_mode: true,
|
||||
gui_ticks: frame,
|
||||
background: 0x20ffffff,
|
||||
upside_down: &|_| false,
|
||||
horizon: &|_| false,
|
||||
};
|
||||
fresh.cache = None;
|
||||
assert_eq!(
|
||||
cached.layout(&inputs, &font),
|
||||
fresh.layout(&inputs, &font),
|
||||
"frame {frame}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn order_ping_and_skins() {
|
||||
let board = Scoreboard::default();
|
||||
@@ -702,8 +860,11 @@ mod tests {
|
||||
assert_eq!(ping_sprite(1000), "icon/ping_1");
|
||||
// UUID.hashCode of 0 is 0: the first default skin.
|
||||
assert_eq!(default_skin(0), "entity/player/slim/alex");
|
||||
assert!(!visible(true, true, 1, false));
|
||||
assert!(visible(true, true, 1, true));
|
||||
assert!(visible(true, false, 1, false));
|
||||
assert!(!visible(true, true, true, 1, false));
|
||||
assert!(visible(true, true, true, 1, true));
|
||||
assert!(visible(true, true, false, 1, false));
|
||||
// F1 (`Hud.isHidden`) takes the list away however it was held up.
|
||||
assert!(!visible(false, true, false, 2, false));
|
||||
assert!(!visible(false, true, true, 1, true));
|
||||
}
|
||||
}
|
||||
@@ -72,10 +72,7 @@
|
||||
"horizon.mods.settings": "Instellings",
|
||||
"horizon.mods.in_world": " (los die wêreld om te verander)",
|
||||
"horizon.mods.open_folder": "Maak Mod-lêergids oop",
|
||||
"horizon.shader_packs.empty": "Geen shader packs: plaas dit in die shaderpacks-lêergids",
|
||||
"horizon.shader_options.open": "Shader-opsies...",
|
||||
"horizon.shader_options.profile": "Profiel",
|
||||
"horizon.shader_options.loading": "Lees tans die shader-pakket se opsies...",
|
||||
"horizon.agent.copy_details": "Kopieer verbindingbesonderhede",
|
||||
"horizon.multiplayer.join": "Sluit aan",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Kanselleer Herkoppel",
|
||||
@@ -475,5 +472,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Ander spelers se spore en auras bly versteek; hul ander kosmetiese items wys steeds. By verstek af.",
|
||||
"horizon.extras.cosmetics.shown": "Getoon",
|
||||
"horizon.extras.cosmetics.hidden": "Versteek",
|
||||
"horizon.extras.cosmetics.name_tags": "Slegs naamplaatjies"
|
||||
"horizon.extras.cosmetics.name_tags": "Slegs naamplaatjies",
|
||||
"Installed": "Geïnstalleer",
|
||||
"Apply": "Pas toe",
|
||||
"Reset all options": "Stel alle opsies terug",
|
||||
"A Horizon shader pack.": "'n Horizon-skadupakket.",
|
||||
"An OptiFine or Iris shader pack.": "'n OptiFine- of Iris-skadupakket.",
|
||||
"In use: press to turn shaders off.": "In gebruik: druk om skadus af te skakel.",
|
||||
"Press to use it.": "Druk om dit te gebruik.",
|
||||
"Put shader pack .zip files or folders in it.": "Sit skadupakket-.zip-lêers of -vouers daarin.",
|
||||
"Puts every option of this pack back to its default.": "Stel elke opsie van hierdie pakket terug na sy verstek.",
|
||||
"Sets many options at once, as the pack groups them.": "Stel baie opsies op een slag, soos die pakket hulle groepeer.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Nog geen skadupakkette nie: maak die pakketvouer oop en sit 'n paar daarin.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Kies 'n skadupakket. As jy die een in gebruik kies, word skadus afgeskakel.",
|
||||
"Changed options apply with Apply, or when you leave.": "Gewysigde opsies word toegepas met Pas toe, of wanneer jy verlaat.",
|
||||
"Pack settings": "Pakketopsies",
|
||||
"Built-in settings": "Ingeboude opsies",
|
||||
"%s settings": "%s-opsies",
|
||||
"Settings made by the pack's author: they change only this pack.": "Opsies deur die pakket se skepper: hulle verander net hierdie pakket."
|
||||
}
|
||||
@@ -77,10 +77,7 @@
|
||||
"horizon.mods.settings": "الإعدادات",
|
||||
"horizon.mods.in_world": " (اترك العالم ليتغير)",
|
||||
"horizon.mods.open_folder": "افتح مجلد التعديل",
|
||||
"horizon.shader_packs.empty": "لا توجد حزم تظليل: ضعها في مجلدshaderpacks",
|
||||
"horizon.shader_options.open": "خيارات التظليل...",
|
||||
"horizon.shader_options.profile": "الملف الشخصي",
|
||||
"horizon.shader_options.loading": "قراءة خيارات حزمة التظليل...",
|
||||
"horizon.agent.copy_details": "نسخ تفاصيل الاتصال",
|
||||
"horizon.multiplayer.join": "انضم",
|
||||
"horizon.multiplayer.cancel_reconnecting": "إلغاء إعادة الاتصال",
|
||||
@@ -494,5 +491,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "تبقى آثار وهالات اللاعبين الآخرين مخفية؛ وتظل مستحضراتهم الأخرى ظاهرة. معطّل افتراضيًا.",
|
||||
"horizon.extras.cosmetics.shown": "معروض",
|
||||
"horizon.extras.cosmetics.hidden": "مخفي",
|
||||
"horizon.extras.cosmetics.name_tags": "علامات الاسم فقط"
|
||||
"horizon.extras.cosmetics.name_tags": "علامات الاسم فقط",
|
||||
"Installed": "المثبّتة",
|
||||
"Apply": "تطبيق",
|
||||
"Reset all options": "إعادة ضبط كل الخيارات",
|
||||
"A Horizon shader pack.": "حزمة شيدر من Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "حزمة شيدر من OptiFine أو Iris.",
|
||||
"In use: press to turn shaders off.": "قيد الاستخدام: اضغط لإيقاف الشيدرات.",
|
||||
"Press to use it.": "اضغط لاستخدامها.",
|
||||
"Put shader pack .zip files or folders in it.": "ضع ملفات .zip أو مجلدات حزم الشيدر فيه.",
|
||||
"Puts every option of this pack back to its default.": "يعيد كل خيار في هذه الحزمة إلى قيمته الافتراضية.",
|
||||
"Sets many options at once, as the pack groups them.": "يضبط عدة خيارات دفعة واحدة، كما تجمعها الحزمة.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "لا توجد حزم شيدر بعد: افتح مجلد الحزم وضع بعضها فيه.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "اختر حزمة شيدر. اختيار الحزمة قيد الاستخدام يوقف الشيدرات.",
|
||||
"Changed options apply with Apply, or when you leave.": "تُطبَّق الخيارات المعدّلة بزر تطبيق، أو عند المغادرة.",
|
||||
"Pack settings": "إعدادات الحزمة",
|
||||
"Built-in settings": "الإعدادات المدمجة",
|
||||
"%s settings": "إعدادات %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "إعدادات وضعها مؤلف الحزمة: تؤثر على هذه الحزمة فقط."
|
||||
}
|
||||
@@ -71,10 +71,7 @@
|
||||
"horizon.mods.settings": "Parametrlər",
|
||||
"horizon.mods.in_world": " (Dəyişmək üçün dünyanı tərk et)",
|
||||
"horizon.mods.open_folder": "Mod Qovluğunu açın",
|
||||
"horizon.shader_packs.empty": "Şeyder paketləri yoxdur: onları shaderpacks qovluğuna qoyun",
|
||||
"horizon.shader_options.open": "Shader Seçimləri...",
|
||||
"horizon.shader_options.profile": "Profil",
|
||||
"horizon.shader_options.loading": "Şeyder paketinin seçimləri oxunur...",
|
||||
"horizon.agent.copy_details": "Bağlantı təfərrüatlarını kopyalayın",
|
||||
"horizon.multiplayer.join": "Qoşulun",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Yenidən qoşulmanı ləğv edin",
|
||||
@@ -466,5 +463,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Digər oyunçuların izləri və auraları gizli qalır; digər kosmetikaları görünməyə davam edir. Defolt olaraq söndürülüb.",
|
||||
"horizon.extras.cosmetics.shown": "Göstərildi",
|
||||
"horizon.extras.cosmetics.hidden": "Gizli",
|
||||
"horizon.extras.cosmetics.name_tags": "Yalnız ad etiketləri"
|
||||
"horizon.extras.cosmetics.name_tags": "Yalnız ad etiketləri",
|
||||
"Installed": "Quraşdırılmış",
|
||||
"Apply": "Tətbiq et",
|
||||
"Reset all options": "Bütün seçimləri sıfırla",
|
||||
"A Horizon shader pack.": "Horizon şeyder paketi.",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine və ya Iris şeyder paketi.",
|
||||
"In use: press to turn shaders off.": "İstifadədədir: şeyderləri söndürmək üçün basın.",
|
||||
"Press to use it.": "İstifadə etmək üçün basın.",
|
||||
"Put shader pack .zip files or folders in it.": "Şeyder paketlərinin .zip fayllarını və ya qovluqlarını bura qoyun.",
|
||||
"Puts every option of this pack back to its default.": "Bu paketin bütün seçimlərini ilkin dəyərinə qaytarır.",
|
||||
"Sets many options at once, as the pack groups them.": "Paketin qruplaşdırdığı kimi, bir çox seçimi birdən təyin edir.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Hələ şeyder paketi yoxdur: paket qovluğunu açıb bir neçəsini qoyun.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Şeyder paketi seçin. İstifadədə olanı seçmək şeyderləri söndürür.",
|
||||
"Changed options apply with Apply, or when you leave.": "Dəyişdirilmiş seçimlər Tətbiq et ilə və ya çıxdıqda tətbiq olunur.",
|
||||
"Pack settings": "Paket seçimləri",
|
||||
"Built-in settings": "Daxili seçimlər",
|
||||
"%s settings": "%s seçimləri",
|
||||
"Settings made by the pack's author: they change only this pack.": "Paketin müəllifinin yaratdığı seçimlər: yalnız bu paketə təsir edir."
|
||||
}
|
||||
@@ -74,10 +74,7 @@
|
||||
"horizon.mods.settings": "Көйләүҙәр",
|
||||
"horizon.mods.in_world": " (донъяны үҙгәртергә ҡалдырырға)",
|
||||
"horizon.mods.open_folder": "Мод папкаһын асыу",
|
||||
"horizon.shader_packs.empty": "Шейдер пакеттары юҡ: уларҙы шейдерпакеттар папкаһына һалығыҙ",
|
||||
"horizon.shader_options.open": "Шейдер варианттары...",
|
||||
"horizon.shader_options.profile": "Профиль",
|
||||
"horizon.shader_options.loading": "Шейдер пакетының варианттарын уҡыу...",
|
||||
"horizon.agent.copy_details": "Тоташыу тураһында мәғлүмәтте күсерергә",
|
||||
"horizon.multiplayer.join": "Ҡушылыу",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Яңынан тоташтырыуҙы юҡҡа сығарыу",
|
||||
@@ -492,5 +489,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Башҡа уйынсыларҙың эҙҙәре һәм ауралары йәшерен ҡала; уларҙың башҡа косметикаһы күренә. Ғәҙәттә һүндерелгән.",
|
||||
"horizon.extras.cosmetics.shown": "Күрһәтелгән",
|
||||
"horizon.extras.cosmetics.hidden": "Йәшерен",
|
||||
"horizon.extras.cosmetics.name_tags": "Исем билдәләре генә"
|
||||
"horizon.extras.cosmetics.name_tags": "Исем билдәләре генә",
|
||||
"Installed": "Урынлаштырылған",
|
||||
"Apply": "Ҡулланырға",
|
||||
"Reset all options": "Бөтә көйләүҙәрҙе ташларға",
|
||||
"A Horizon shader pack.": "Horizon шейдер пакеты.",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine йәки Iris шейдер пакеты.",
|
||||
"In use: press to turn shaders off.": "Ҡулланыла: шейдерҙарҙы һүндереү өсөн баҫығыҙ.",
|
||||
"Press to use it.": "Ҡулланыу өсөн баҫығыҙ.",
|
||||
"Put shader pack .zip files or folders in it.": "Шейдер пакеттарының .zip файлдарын йәки папкаларын шунда һалығыҙ.",
|
||||
"Puts every option of this pack back to its default.": "Был пакеттың һәр көйләүен ғәҙәти хәленә ҡайтара.",
|
||||
"Sets many options at once, as the pack groups them.": "Пакет төркөмләгәнсә, күп көйләүҙе бер юлы урынлаштыра.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Шейдер пакеттары әлегә юҡ: пакеттар папкаһын асып, берәй нисәһен һалығыҙ.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Шейдер пакетын һайлағыҙ. Ҡулланылғанын һайлау шейдерҙарҙы һүндерә.",
|
||||
"Changed options apply with Apply, or when you leave.": "Үҙгәртелгән көйләүҙәр «Ҡулланырға» менән йәки сыҡҡанда ҡулланыла.",
|
||||
"Pack settings": "Пакет көйләүҙәре",
|
||||
"Built-in settings": "Эске көйләүҙәр",
|
||||
"%s settings": "%s көйләүҙәре",
|
||||
"Settings made by the pack's author: they change only this pack.": "Пакет авторы төҙөгән көйләүҙәр: улар тик был пакетты үҙгәртә."
|
||||
}
|
||||
@@ -72,10 +72,7 @@
|
||||
"horizon.mods.settings": "Налады",
|
||||
"horizon.mods.in_world": " (пакінуць свет змяніцца)",
|
||||
"horizon.mods.open_folder": "Адкрыць тэчку Mod",
|
||||
"horizon.shader_packs.empty": "Пакетаў шэйдэраў няма: змесціце іх у тэчку шэйдэраў",
|
||||
"horizon.shader_options.open": "Параметры шэйдара...",
|
||||
"horizon.shader_options.profile": "Профіль",
|
||||
"horizon.shader_options.loading": "Чытанне параметраў пакета шэйдараў...",
|
||||
"horizon.agent.copy_details": "Скапіруйце звесткі аб падключэнні",
|
||||
"horizon.multiplayer.join": "Далучайцеся",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Адмяніць паўторнае падключэнне",
|
||||
@@ -478,5 +475,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Сляды і аўры іншых гульцоў схаваныя; іх астатняя касметыка бачная. Па змаўчанні выкл.",
|
||||
"horizon.extras.cosmetics.shown": "Паказана",
|
||||
"horizon.extras.cosmetics.hidden": "Схаваны",
|
||||
"horizon.extras.cosmetics.name_tags": "Толькі біркі з імёнамі"
|
||||
"horizon.extras.cosmetics.name_tags": "Толькі біркі з імёнамі",
|
||||
"Installed": "Усталяваныя",
|
||||
"Apply": "Ужыць",
|
||||
"Reset all options": "Скінуць усе налады",
|
||||
"A Horizon shader pack.": "Пакет шэйдараў Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Пакет шэйдараў OptiFine або Iris.",
|
||||
"In use: press to turn shaders off.": "Выкарыстоўваецца: націсніце, каб выключыць шэйдары.",
|
||||
"Press to use it.": "Націсніце, каб выкарыстаць.",
|
||||
"Put shader pack .zip files or folders in it.": "Пакладзіце ў яе .zip-файлы або папкі пакетаў шэйдараў.",
|
||||
"Puts every option of this pack back to its default.": "Вяртае ўсе налады гэтага пакета да прадвызначаных.",
|
||||
"Sets many options at once, as the pack groups them.": "Задае адразу многія налады так, як іх групуе пакет.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Пакетаў шэйдараў пакуль няма: адкрыйце папку пакетаў і пакладзіце туды некалькі.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Выберыце пакет шэйдараў. Выбар пакета, які выкарыстоўваецца, выключае шэйдары.",
|
||||
"Changed options apply with Apply, or when you leave.": "Змененыя налады ўжываюцца кнопкай «Ужыць» або пры выхадзе.",
|
||||
"Pack settings": "Налады пакета",
|
||||
"Built-in settings": "Убудаваныя налады",
|
||||
"%s settings": "Налады %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Налады, зробленыя аўтарам пакета: яны змяняюць толькі гэты пакет."
|
||||
}
|
||||
@@ -70,10 +70,7 @@
|
||||
"horizon.mods.settings": "Настройки",
|
||||
"horizon.mods.in_world": " (оставете света да се промени)",
|
||||
"horizon.mods.open_folder": "Отворете папката на Mod",
|
||||
"horizon.shader_packs.empty": "Без шейдър пакети: поставете ги в папката shaderpacks",
|
||||
"horizon.shader_options.open": "Опции за шейдър...",
|
||||
"horizon.shader_options.profile": "Профил",
|
||||
"horizon.shader_options.loading": "Четене на опциите на шейдър пакета...",
|
||||
"horizon.agent.copy_details": "Копиране на подробности за връзката",
|
||||
"horizon.multiplayer.join": "Присъединете се",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Отказ Повторно свързване",
|
||||
@@ -474,5 +471,22 @@
|
||||
"horizon.extras.cosmetics.shown": "Показано",
|
||||
"horizon.extras.cosmetics.hidden": "Скрити",
|
||||
"horizon.extras.cosmetics.name_tags": "Само етикети с имена",
|
||||
"Shader Packs...": "Шейдър пакети..."
|
||||
"Shader Packs...": "Шейдър пакети...",
|
||||
"Installed": "Инсталирани",
|
||||
"Apply": "Приложи",
|
||||
"Reset all options": "Нулирай всички опции",
|
||||
"A Horizon shader pack.": "Пакет шейдъри на Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Пакет шейдъри на OptiFine или Iris.",
|
||||
"In use: press to turn shaders off.": "В употреба: натиснете, за да изключите шейдърите.",
|
||||
"Press to use it.": "Натиснете, за да го използвате.",
|
||||
"Put shader pack .zip files or folders in it.": "Сложете в нея .zip файлове или папки с пакети шейдъри.",
|
||||
"Puts every option of this pack back to its default.": "Връща всяка опция на този пакет към стойността по подразбиране.",
|
||||
"Sets many options at once, as the pack groups them.": "Задава много опции наведнъж, както ги групира пакетът.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Още няма пакети шейдъри: отворете папката с пакети и сложете няколко.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Изберете пакет шейдъри. Изборът на използвания го изключва.",
|
||||
"Changed options apply with Apply, or when you leave.": "Променените опции се прилагат с Приложи или при излизане.",
|
||||
"Pack settings": "Настройки на пакета",
|
||||
"Built-in settings": "Вградени настройки",
|
||||
"%s settings": "Настройки на %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Настройки, направени от автора на пакета: променят само този пакет."
|
||||
}
|
||||
@@ -72,10 +72,7 @@
|
||||
"horizon.mods.settings": "Arventennoù",
|
||||
"horizon.mods.in_world": " (lezel ar bed da cheñch)",
|
||||
"horizon.mods.open_folder": "Digeriñ an teuliad mod",
|
||||
"horizon.shader_packs.empty": "Pakad skeudennoù ebet : lakait anezho e teuliad ar pakadoù skeudennoù",
|
||||
"horizon.shader_options.open": "Dibarzhioù skeudenniñ...",
|
||||
"horizon.shader_options.profile": "Profil",
|
||||
"horizon.shader_options.loading": "O lenn dibaboù ar pakad skeudennoù...",
|
||||
"horizon.agent.copy_details": "Eilañ munudoù ar c'hennask",
|
||||
"horizon.multiplayer.join": "Kemer perzh",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Nullañ an adkennaskañ",
|
||||
@@ -477,5 +474,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Kuzhet e chom roudoù hag aouraoù ar c'hoarierien all; diskouezet e vez o c'hinkladurioù all. Diweredekaet dre ziouer.",
|
||||
"horizon.extras.cosmetics.shown": "Diskouezet",
|
||||
"horizon.extras.cosmetics.hidden": "Kuzhet",
|
||||
"horizon.extras.cosmetics.name_tags": "Tikedennoù anvioù hepken"
|
||||
"horizon.extras.cosmetics.name_tags": "Tikedennoù anvioù hepken",
|
||||
"Installed": "Staliet",
|
||||
"Apply": "Arloañ",
|
||||
"Reset all options": "Adderaouekaat an holl zibarzhioù",
|
||||
"A Horizon shader pack.": "Ur pakad skeudennerioù Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Ur pakad skeudennerioù OptiFine pe Iris.",
|
||||
"In use: press to turn shaders off.": "Implijet: pouezit evit lazhañ ar skeudennerioù.",
|
||||
"Press to use it.": "Pouezit evit e implijout.",
|
||||
"Put shader pack .zip files or folders in it.": "Lakait e-barzh restroù .zip pe teuliadoù pakadoù skeudennerioù.",
|
||||
"Puts every option of this pack back to its default.": "Adlakaat a ra pep dibarzh eus ar pakad-mañ d'e dalvoudegezh dre ziouer.",
|
||||
"Sets many options at once, as the pack groups them.": "Lakaat a ra kalz dibarzhioù war un dro, evel ma vez strollet gant ar pakad.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Pakad skeudennerioù ebet c'hoazh: digorit teuliad ar pakadoù ha lakait un nebeut e-barzh.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Dibabit ur pakad skeudennerioù. Dibab an hini implijet a laz ar skeudennerioù.",
|
||||
"Changed options apply with Apply, or when you leave.": "An dibarzhioù cheñchet a vez arloet gant Arloañ, pe pa guitait.",
|
||||
"Pack settings": "Arventennoù ar pakad",
|
||||
"Built-in settings": "Arventennoù enframmet",
|
||||
"%s settings": "Arventennoù %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Arventennoù graet gant oberer ar pakad: cheñch a reont nemet ar pakad-mañ."
|
||||
}
|
||||
@@ -58,10 +58,7 @@
|
||||
"horizon.mods.settings": "Postavke",
|
||||
"horizon.mods.in_world": " (ostavi svijet da se promijeni)",
|
||||
"horizon.mods.open_folder": "Otvorite Mod folder",
|
||||
"horizon.shader_packs.empty": "Nema paketa shadera: stavite ih u mapu shaderpacks",
|
||||
"horizon.shader_options.open": "Opcije shadera...",
|
||||
"horizon.shader_options.profile": "Profil",
|
||||
"horizon.shader_options.loading": "Čitanje opcija paketa shadera...",
|
||||
"horizon.agent.copy_details": "Kopiraj detalje veze",
|
||||
"horizon.multiplayer.join": "Pridružite se",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Otkaži ponovno povezivanje",
|
||||
@@ -419,5 +416,22 @@
|
||||
"horizon.extras.cosmetics.shown": "Prikazano",
|
||||
"horizon.extras.cosmetics.hidden": "Skriveno",
|
||||
"horizon.extras.cosmetics.name_tags": "Samo oznake sa imenom",
|
||||
"Shader Packs...": "Shader paketi..."
|
||||
"Shader Packs...": "Shader paketi...",
|
||||
"Installed": "Instalirani",
|
||||
"Apply": "Primijeni",
|
||||
"Reset all options": "Resetuj sve opcije",
|
||||
"A Horizon shader pack.": "Horizon paket šejdera.",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine ili Iris paket šejdera.",
|
||||
"In use: press to turn shaders off.": "U upotrebi: pritisni da isključiš šejdere.",
|
||||
"Press to use it.": "Pritisni da ga koristiš.",
|
||||
"Put shader pack .zip files or folders in it.": "Stavi u njega .zip datoteke ili mape paketa šejdera.",
|
||||
"Puts every option of this pack back to its default.": "Vraća svaku opciju ovog paketa na zadanu vrijednost.",
|
||||
"Sets many options at once, as the pack groups them.": "Postavlja mnogo opcija odjednom, kako ih paket grupiše.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Još nema paketa šejdera: otvori mapu paketa i stavi nekoliko u nju.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Odaberi paket šejdera. Odabir paketa koji se koristi isključuje šejdere.",
|
||||
"Changed options apply with Apply, or when you leave.": "Promijenjene opcije se primjenjuju pomoću Primijeni ili kada izađeš.",
|
||||
"Pack settings": "Postavke paketa",
|
||||
"Built-in settings": "Ugrađene postavke",
|
||||
"%s settings": "Postavke za %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Postavke koje je napravio autor paketa: mijenjaju samo ovaj paket."
|
||||
}
|
||||
@@ -69,10 +69,7 @@
|
||||
"horizon.mods.settings": "Configuració",
|
||||
"horizon.mods.in_world": " (deixar el món per canviar)",
|
||||
"horizon.mods.open_folder": "Obriu la carpeta de modificacions",
|
||||
"horizon.shader_packs.empty": "Sense paquets d'ombres: col·loqueu-los a la carpeta shaderpacks",
|
||||
"horizon.shader_options.open": "Opcions d'ombrejat...",
|
||||
"horizon.shader_options.profile": "Perfil",
|
||||
"horizon.shader_options.loading": "Llegint les opcions del paquet shader...",
|
||||
"horizon.agent.copy_details": "Copia els detalls de la connexió",
|
||||
"horizon.multiplayer.join": "Uneix-te",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Cancel·la la tornada a connectar",
|
||||
@@ -467,5 +464,22 @@
|
||||
"horizon.extras.cosmetics.shown": "Es mostra",
|
||||
"horizon.extras.cosmetics.hidden": "Ocult",
|
||||
"horizon.extras.cosmetics.name_tags": "Només etiquetes de nom",
|
||||
"Shader Packs...": "Paquets d'ombres..."
|
||||
"Shader Packs...": "Paquets d'ombres...",
|
||||
"Installed": "Instal·lats",
|
||||
"Apply": "Aplica",
|
||||
"Reset all options": "Restableix totes les opcions",
|
||||
"A Horizon shader pack.": "Un paquet de shaders d'Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Un paquet de shaders d'OptiFine o Iris.",
|
||||
"In use: press to turn shaders off.": "En ús: prem per desactivar els shaders.",
|
||||
"Press to use it.": "Prem per fer-lo servir.",
|
||||
"Put shader pack .zip files or folders in it.": "Hi pots posar fitxers .zip o carpetes de paquets de shaders.",
|
||||
"Puts every option of this pack back to its default.": "Torna totes les opcions d'aquest paquet al valor per defecte.",
|
||||
"Sets many options at once, as the pack groups them.": "Defineix moltes opcions alhora, tal com les agrupa el paquet.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Encara no hi ha cap paquet de shaders: obre la carpeta de paquets i hi poses algun.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Tria un paquet de shaders. Triar el que està en ús desactiva els shaders.",
|
||||
"Changed options apply with Apply, or when you leave.": "Les opcions canviades s'apliquen amb Aplica, o quan surts.",
|
||||
"Pack settings": "Configuració del paquet",
|
||||
"Built-in settings": "Configuració integrada",
|
||||
"%s settings": "Configuració de %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Configuració feta per l'autor del paquet: només canvia aquest paquet."
|
||||
}
|
||||
@@ -67,11 +67,7 @@
|
||||
"horizon.mods.settings": "Nastavení",
|
||||
"horizon.mods.in_world": " (nechat svět změnit)",
|
||||
"horizon.mods.open_folder": "Otevřete složku Mod",
|
||||
"horizon.shader_packs.empty": "Žádné balíčky shaderů: vložte je do složky shaderpacks",
|
||||
"horizon.shader_packs.kind_horizon": " (Horizont)",
|
||||
"horizon.shader_options.open": "Možnosti stínování...",
|
||||
"horizon.shader_options.profile": "Profil",
|
||||
"horizon.shader_options.loading": "Čtení možností sady shaderů...",
|
||||
"horizon.agent.copy_details": "Kopírovat podrobnosti o připojení",
|
||||
"horizon.multiplayer.join": "Připojte se",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Zrušit opětovné připojení",
|
||||
@@ -456,5 +452,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Stopy a aury ostatních hráčů zůstanou skryté; jejich ostatní kosmetika je dál vidět. Ve výchozím stavu vypnuto.",
|
||||
"horizon.extras.cosmetics.shown": "Zobrazeno",
|
||||
"horizon.extras.cosmetics.hidden": "Skrytá",
|
||||
"horizon.extras.cosmetics.name_tags": "Pouze jmenovky"
|
||||
"horizon.extras.cosmetics.name_tags": "Pouze jmenovky",
|
||||
"Installed": "Nainstalované",
|
||||
"Apply": "Použít",
|
||||
"Reset all options": "Obnovit všechny možnosti",
|
||||
"A Horizon shader pack.": "Balíček shaderů Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Balíček shaderů OptiFine nebo Iris.",
|
||||
"In use: press to turn shaders off.": "Používá se: stisknutím vypnete shadery.",
|
||||
"Press to use it.": "Stisknutím jej použijete.",
|
||||
"Put shader pack .zip files or folders in it.": "Vložte do ní soubory .zip nebo složky balíčků shaderů.",
|
||||
"Puts every option of this pack back to its default.": "Vrátí všechny možnosti tohoto balíčku na výchozí hodnoty.",
|
||||
"Sets many options at once, as the pack groups them.": "Nastaví mnoho možností najednou, tak jak je balíček seskupuje.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Zatím žádné balíčky shaderů: otevřete složku balíčků a vložte do ní nějaké.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Vyberte balíček shaderů. Výběr používaného balíčku shadery vypne.",
|
||||
"Changed options apply with Apply, or when you leave.": "Změněné možnosti se použijí tlačítkem Použít nebo při odchodu.",
|
||||
"Pack settings": "Nastavení balíčku",
|
||||
"Built-in settings": "Vestavěná nastavení",
|
||||
"%s settings": "Nastavení %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Nastavení vytvořená autorem balíčku: mění jen tento balíček."
|
||||
}
|
||||
@@ -73,10 +73,7 @@
|
||||
"horizon.mods.settings": "Настройкӑсем",
|
||||
"horizon.mods.in_world": " (тӗнчене улӑштарма хӑвар)",
|
||||
"horizon.mods.open_folder": "Мод папкине уҫӑр",
|
||||
"horizon.shader_packs.empty": "Шейдер пакетӗсем ҫук: вӗсене шейдер пакетӗсем папкӑна лартӑр",
|
||||
"horizon.shader_options.open": "Шейдер опцийӗсем...",
|
||||
"horizon.shader_options.profile": "Профиль",
|
||||
"horizon.shader_options.loading": "Шейдер пакетӗн опцийӗсене вулатӑп...",
|
||||
"horizon.agent.copy_details": "Ҫыхӑну даннӑйӗсене копилесе илӗр",
|
||||
"horizon.multiplayer.join": "Пӗрлешӗр",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Ҫӗнӗрен ҫыхӑнассине пӑрахӑҫла",
|
||||
@@ -488,5 +485,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Ытти вӑййисен йӗрӗсем тата аурисем пытарӑнса юлаҫҫӗ; вӗсен ытти косметики курӑнать. Йӑлана кура сӳнтернӗ.",
|
||||
"horizon.extras.cosmetics.shown": "Кӑтартнӑ",
|
||||
"horizon.extras.cosmetics.hidden": "Пытарнӑ",
|
||||
"horizon.extras.cosmetics.name_tags": "Ят паллисем кӑна"
|
||||
"horizon.extras.cosmetics.name_tags": "Ят паллисем кӑна",
|
||||
"Installed": "Лартнă",
|
||||
"Apply": "Усă курасси",
|
||||
"Reset all options": "Пур ĕнерлевсене таçтан лартасси",
|
||||
"A Horizon shader pack.": "Horizon шейдер пакечĕ.",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine е Iris шейдер пакечĕ.",
|
||||
"In use: press to turn shaders off.": "Усă курăнать: шейдерсене сӳнтермелли пусăр.",
|
||||
"Press to use it.": "Усă курма пусăр.",
|
||||
"Put shader pack .zip files or folders in it.": "Шейдер пакечĕсен .zip файлĕсене е папкисене унта хурăр.",
|
||||
"Puts every option of this pack back to its default.": "Ку пакетăн пур ĕнерлевне те пуçламăш хакне таврăнтарать.",
|
||||
"Sets many options at once, as the pack groups them.": "Пакет ушкăнланă евĕр нумай ĕнерлеве пĕр харăсах лартать.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Шейдер пакечĕсем халлĕхе çук: пакетсен папкине уçса темиçе хурăр.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Шейдер пакечĕ суйлăр. Усă куракана суйласан шейдерсем сӳнеççĕ.",
|
||||
"Changed options apply with Apply, or when you leave.": "Улшăннă ĕнерлевсем «Усă курасси» пусăмпа е тухнă чухне усă курăнаççĕ.",
|
||||
"Pack settings": "Пакет ĕнерлевĕсем",
|
||||
"Built-in settings": "Йĕркеленĕ ĕнерлевсем",
|
||||
"%s settings": "%s ĕнерлевĕсем",
|
||||
"Settings made by the pack's author: they change only this pack.": "Пакет авторĕ тунă ĕнерлевсем: вĕсем ҫак пакета çеç улăштараççĕ."
|
||||
}
|
||||
@@ -67,10 +67,7 @@
|
||||
"horizon.mods.settings": "Gosodiadau",
|
||||
"horizon.mods.in_world": " (gadewch y byd i newid)",
|
||||
"horizon.mods.open_folder": "Agor Ffolder Mod",
|
||||
"horizon.shader_packs.empty": "Dim pecynnau lliwiwr: rhowch nhw yn y ffolder shaderpacks",
|
||||
"horizon.shader_options.open": "Opsiynau Shader...",
|
||||
"horizon.shader_options.profile": "Proffil",
|
||||
"horizon.shader_options.loading": "Darllen opsiynau'r pecyn lliwiwr...",
|
||||
"horizon.agent.copy_details": "Copïo Manylion Cysylltiad",
|
||||
"horizon.multiplayer.join": "Ymuno",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Canslo Ailgysylltu",
|
||||
@@ -463,5 +460,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Mae llwybrau ac awrâu chwaraewyr eraill yn aros yn gudd; mae eu colur arall yn dal i ddangos. I ffwrdd yn ddiofyn.",
|
||||
"horizon.extras.cosmetics.shown": "Dangosir",
|
||||
"horizon.extras.cosmetics.hidden": "Cudd",
|
||||
"horizon.extras.cosmetics.name_tags": "Tagiau enw yn unig"
|
||||
"horizon.extras.cosmetics.name_tags": "Tagiau enw yn unig",
|
||||
"Installed": "Wedi'u gosod",
|
||||
"Apply": "Gweithredu",
|
||||
"Reset all options": "Ailosod pob dewis",
|
||||
"A Horizon shader pack.": "Pecyn shaders Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Pecyn shaders OptiFine neu Iris.",
|
||||
"In use: press to turn shaders off.": "Yn cael ei ddefnyddio: pwyswch i ddiffodd shaders.",
|
||||
"Press to use it.": "Pwyswch i'w ddefnyddio.",
|
||||
"Put shader pack .zip files or folders in it.": "Rhowch ffeiliau .zip neu ffolderi pecynnau shaders ynddo.",
|
||||
"Puts every option of this pack back to its default.": "Yn rhoi pob dewis o'r pecyn hwn yn ôl i'w ragosodiad.",
|
||||
"Sets many options at once, as the pack groups them.": "Yn gosod llawer o ddewisiadau ar unwaith, fel mae'r pecyn yn eu grwpio.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Dim pecynnau shaders eto: agorwch ffolder y pecynnau a rhowch rai ynddo.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Dewiswch becyn shaders. Mae dewis yr un sy'n cael ei ddefnyddio yn diffodd shaders.",
|
||||
"Changed options apply with Apply, or when you leave.": "Mae dewisiadau a newidiwyd yn cael eu gweithredu gyda Gweithredu, neu pan fyddwch yn gadael.",
|
||||
"Pack settings": "Gosodiadau'r pecyn",
|
||||
"Built-in settings": "Gosodiadau adeiledig",
|
||||
"%s settings": "Gosodiadau %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Gosodiadau a wnaed gan awdur y pecyn: maent yn newid y pecyn hwn yn unig."
|
||||
}
|
||||
@@ -60,10 +60,7 @@
|
||||
"horizon.mods.settings": "Indstillinger",
|
||||
"horizon.mods.in_world": " (lad verden ændre sig)",
|
||||
"horizon.mods.open_folder": "Åbn Mod-mappe",
|
||||
"horizon.shader_packs.empty": "Ingen shader-pakker: læg dem i shaderpacks-mappen",
|
||||
"horizon.shader_options.open": "Indstillinger for skygge...",
|
||||
"horizon.shader_options.profile": "Profil",
|
||||
"horizon.shader_options.loading": "Læser shader-pakkens muligheder...",
|
||||
"horizon.agent.copy_details": "Kopier forbindelsesoplysninger",
|
||||
"horizon.multiplayer.join": "Deltag",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Annuller Gentilslutning",
|
||||
@@ -444,5 +441,22 @@
|
||||
"horizon.extras.cosmetics.shown": "Vist",
|
||||
"horizon.extras.cosmetics.hidden": "Skjult",
|
||||
"horizon.extras.cosmetics.name_tags": "Kun navneskilte",
|
||||
"Shader Packs...": "Shader-pakker..."
|
||||
"Shader Packs...": "Shader-pakker...",
|
||||
"Installed": "Installeret",
|
||||
"Apply": "Anvend",
|
||||
"Reset all options": "Nulstil alle indstillinger",
|
||||
"A Horizon shader pack.": "En Horizon-shaderpakke.",
|
||||
"An OptiFine or Iris shader pack.": "En OptiFine- eller Iris-shaderpakke.",
|
||||
"In use: press to turn shaders off.": "I brug: tryk for at slå shaders fra.",
|
||||
"Press to use it.": "Tryk for at bruge den.",
|
||||
"Put shader pack .zip files or folders in it.": "Læg .zip-filer eller mapper med shaderpakker i den.",
|
||||
"Puts every option of this pack back to its default.": "Sætter alle denne pakkes indstillinger tilbage til standard.",
|
||||
"Sets many options at once, as the pack groups them.": "Sætter mange indstillinger på én gang, som pakken grupperer dem.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Ingen shaderpakker endnu: åbn pakkemappen og læg nogle i den.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Vælg en shaderpakke. Vælger du den, der er i brug, slås shaders fra.",
|
||||
"Changed options apply with Apply, or when you leave.": "Ændrede indstillinger træder i kraft med Anvend, eller når du forlader skærmen.",
|
||||
"Pack settings": "Pakkeindstillinger",
|
||||
"Built-in settings": "Indbyggede indstillinger",
|
||||
"%s settings": "Indstillinger for %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Indstillinger lavet af pakkens forfatter: de ændrer kun denne pakke."
|
||||
}
|
||||
@@ -65,11 +65,7 @@
|
||||
"horizon.mods.dev": " (Entwicklung)",
|
||||
"horizon.mods.settings": "Einstellungen",
|
||||
"horizon.mods.open_folder": "Modordner öffnen",
|
||||
"horizon.shader_packs.empty": "Keine Shader-Pakete: Lege sie in den Ordner shaderpacks",
|
||||
"horizon.shader_packs.kind_horizon": " (Horizon)",
|
||||
"horizon.shader_options.open": "Shader-Optionen …",
|
||||
"horizon.shader_options.profile": "Profil",
|
||||
"horizon.shader_options.loading": "Optionen des Shader-Pakets werden gelesen …",
|
||||
"horizon.agent.copy_details": "Verbindungsdaten kopieren",
|
||||
"horizon.multiplayer.join": "Beitreten",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Neuverbindung abbrechen",
|
||||
@@ -222,6 +218,8 @@
|
||||
"horizon.extras.no_ai": "Keine KI",
|
||||
"horizon.extras.agent_control": "Agentensteuerung",
|
||||
"horizon.extras.discord_presence": "Discord Rich Presence",
|
||||
"horizon.extras.zoom": "Zoom",
|
||||
"horizon.extras.zoom_mod_install": "Zoom: über Mods installieren",
|
||||
"horizon.extras.fullbright.tooltip": "Zeigt Blöcke und Objekte bei vollem Licht, selbst an dunklen Orten.",
|
||||
"horizon.extras.dynamic_lights.tooltip": "Fackeln und andere Lichtquellen, die gehalten, fallen gelassen werden oder brennen, beleuchten die Blöcke um sich herum.",
|
||||
"horizon.extras.own_name_tag.tooltip": "Zeigt deinen Namen in der dritten Person über deinem Kopf an, so wie andere Spieler ihn sehen. Versteckt, wenn der Server die Namen deines Teams verbirgt oder du unsichtbar bist.",
|
||||
@@ -304,6 +302,23 @@
|
||||
"Shaders": "Shader",
|
||||
"Shader packs, Horizon's and shaders/ folder ones, and their options.": "Shader-Pakete, die von Horizon und die aus shaders/-Ordnern, und ihre Optionen.",
|
||||
"Shader packs": "Shader-Pakete",
|
||||
"Installed": "Installiert",
|
||||
"Apply": "Anwenden",
|
||||
"Reset all options": "Alle Optionen zurücksetzen",
|
||||
"A Horizon shader pack.": "Ein Horizon-Shader-Paket.",
|
||||
"An OptiFine or Iris shader pack.": "Ein OptiFine- oder Iris-Shader-Paket.",
|
||||
"In use: press to turn shaders off.": "In Verwendung: Drücken, um Shader auszuschalten.",
|
||||
"Press to use it.": "Drücken, um es zu verwenden.",
|
||||
"Put shader pack .zip files or folders in it.": "Lege Shader-Paket-.zip-Dateien oder -Ordner hinein.",
|
||||
"Puts every option of this pack back to its default.": "Setzt jede Option dieses Pakets auf den Standardwert zurück.",
|
||||
"Sets many options at once, as the pack groups them.": "Stellt viele Optionen auf einmal ein, so wie das Paket sie gruppiert.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Noch keine Shader-Pakete: Öffne den Paketordner und lege welche hinein.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Wähle ein Shader-Paket. Das verwendete erneut zu wählen schaltet Shader aus.",
|
||||
"Changed options apply with Apply, or when you leave.": "Geänderte Optionen gelten mit Anwenden oder beim Verlassen.",
|
||||
"Pack settings": "Paketeinstellungen",
|
||||
"Built-in settings": "Integrierte Einstellungen",
|
||||
"%s settings": "Einstellungen für %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Vom Autor des Pakets erstellte Einstellungen: Sie ändern nur dieses Paket.",
|
||||
"Vanilla's own preset: sets only vanilla's options, as Minecraft does.": "Das Preset von Vanilla: setzt nur die Optionen von Vanilla, wie Minecraft.",
|
||||
"Fullscreen takes the display over, at the mode below.": "Der Vollbildmodus übernimmt den Bildschirm, im Modus darunter.",
|
||||
"The display mode exclusive fullscreen uses. Applies when you leave.": "Der Anzeigemodus des exklusiven Vollbilds. Gilt, wenn du das Menü verlässt.",
|
||||
|
||||
@@ -67,10 +67,7 @@
|
||||
"horizon.mods.settings": "Ρυθμίσεις",
|
||||
"horizon.mods.in_world": " (άσε τον κόσμο να αλλάξει)",
|
||||
"horizon.mods.open_folder": "Ανοίξτε το φάκελο Mod",
|
||||
"horizon.shader_packs.empty": "Χωρίς πακέτα shader: τοποθετήστε τα στο φάκελο shaderpacks",
|
||||
"horizon.shader_options.open": "Επιλογές Shader...",
|
||||
"horizon.shader_options.profile": "Προφίλ",
|
||||
"horizon.shader_options.loading": "Διαβάζοντας τις επιλογές του πακέτου shader...",
|
||||
"horizon.agent.copy_details": "Αντιγραφή στοιχείων σύνδεσης",
|
||||
"horizon.multiplayer.join": "Εγγραφείτε",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Ακύρωση επανασύνδεσης",
|
||||
@@ -466,5 +463,22 @@
|
||||
"horizon.extras.cosmetics.shown": "Εμφανίζεται",
|
||||
"horizon.extras.cosmetics.hidden": "Κρυφό",
|
||||
"horizon.extras.cosmetics.name_tags": "Μόνο ετικέτες ονομάτων",
|
||||
"Shader Packs...": "Πακέτα Shader..."
|
||||
"Shader Packs...": "Πακέτα Shader...",
|
||||
"Installed": "Εγκατεστημένα",
|
||||
"Apply": "Εφαρμογή",
|
||||
"Reset all options": "Επαναφορά όλων των επιλογών",
|
||||
"A Horizon shader pack.": "Ένα πακέτο shader του Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Ένα πακέτο shader του OptiFine ή του Iris.",
|
||||
"In use: press to turn shaders off.": "Σε χρήση: πατήστε για απενεργοποίηση των shader.",
|
||||
"Press to use it.": "Πατήστε για χρήση.",
|
||||
"Put shader pack .zip files or folders in it.": "Βάλτε μέσα αρχεία .zip ή φακέλους πακέτων shader.",
|
||||
"Puts every option of this pack back to its default.": "Επαναφέρει κάθε επιλογή αυτού του πακέτου στην προεπιλογή της.",
|
||||
"Sets many options at once, as the pack groups them.": "Ορίζει πολλές επιλογές ταυτόχρονα, όπως τις ομαδοποιεί το πακέτο.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Δεν υπάρχουν ακόμη πακέτα shader: ανοίξτε το φάκελο πακέτων και βάλτε μερικά.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Επιλέξτε ένα πακέτο shader. Η επιλογή αυτού που χρησιμοποιείται απενεργοποιεί τα shader.",
|
||||
"Changed options apply with Apply, or when you leave.": "Οι αλλαγμένες επιλογές εφαρμόζονται με το Εφαρμογή ή όταν φύγετε.",
|
||||
"Pack settings": "Ρυθμίσεις πακέτου",
|
||||
"Built-in settings": "Ενσωματωμένες ρυθμίσεις",
|
||||
"%s settings": "Ρυθμίσεις %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Ρυθμίσεις από τον δημιουργό του πακέτου: αλλάζουν μόνο αυτό το πακέτο."
|
||||
}
|
||||
@@ -120,11 +120,7 @@
|
||||
"horizon.mods.switch.worldgen": "changes world generation",
|
||||
"horizon.mods.switch.registries": "adds blocks, items or other registry entries",
|
||||
"horizon.mods.switch.restart": "Restart the game to apply %s: they add blocks, items or world generation.",
|
||||
"horizon.shader_packs.empty": "No shader packs: put them in the shaderpacks folder",
|
||||
"horizon.shader_packs.kind_horizon": " (Horizon)",
|
||||
"horizon.shader_options.open": "Shader Options...",
|
||||
"horizon.shader_options.profile": "Profile",
|
||||
"horizon.shader_options.loading": "Reading the shader pack's options...",
|
||||
"horizon.agent.copy_details": "Copy Connection Details",
|
||||
"horizon.multiplayer.join": "Join",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Cancel Reconnecting",
|
||||
@@ -322,6 +318,8 @@
|
||||
"horizon.extras.no_ai": "No AI",
|
||||
"horizon.extras.agent_control": "Agent Control",
|
||||
"horizon.extras.discord_presence": "Discord Rich Presence",
|
||||
"horizon.extras.zoom": "Zoom",
|
||||
"horizon.extras.zoom_mod_install": "Zoom: install from Mods",
|
||||
"horizon.extras.splitscreen": "Splitscreen",
|
||||
"horizon.extras.chat_animation": "Smooth chat",
|
||||
"horizon.extras.chat_indicator": "Message indicator",
|
||||
@@ -495,6 +493,23 @@
|
||||
"Shaders": "Shaders",
|
||||
"Shader packs, Horizon's and shaders/ folder ones, and their options.": "Shader packs, Horizon's and shaders/ folder ones, and their options.",
|
||||
"Shader packs": "Shader packs",
|
||||
"Installed": "Installed",
|
||||
"Apply": "Apply",
|
||||
"Reset all options": "Reset all options",
|
||||
"A Horizon shader pack.": "A Horizon shader pack.",
|
||||
"An OptiFine or Iris shader pack.": "An OptiFine or Iris shader pack.",
|
||||
"In use: press to turn shaders off.": "In use: press to turn shaders off.",
|
||||
"Press to use it.": "Press to use it.",
|
||||
"Put shader pack .zip files or folders in it.": "Put shader pack .zip files or folders in it.",
|
||||
"Puts every option of this pack back to its default.": "Puts every option of this pack back to its default.",
|
||||
"Sets many options at once, as the pack groups them.": "Sets many options at once, as the pack groups them.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "No shader packs yet: open the pack folder and put some in it.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Pick a shader pack. Picking the one in use turns shaders off.",
|
||||
"Changed options apply with Apply, or when you leave.": "Changed options apply with Apply, or when you leave.",
|
||||
"Pack settings": "Pack settings",
|
||||
"Built-in settings": "Built-in settings",
|
||||
"%s settings": "%s settings",
|
||||
"Settings made by the pack's author: they change only this pack.": "Settings made by the pack's author: they change only this pack.",
|
||||
"Vanilla's own preset: sets only vanilla's options, as Minecraft does.": "Vanilla's own preset: sets only vanilla's options, as Minecraft does.",
|
||||
"Fullscreen takes the display over, at the mode below.": "Fullscreen takes the display over, at the mode below.",
|
||||
"The display mode exclusive fullscreen uses. Applies when you leave.": "The display mode exclusive fullscreen uses. Applies when you leave.",
|
||||
|
||||
@@ -72,10 +72,7 @@
|
||||
"horizon.mods.settings": "Agordoj",
|
||||
"horizon.mods.in_world": " (lasu la mondon ŝanĝi)",
|
||||
"horizon.mods.open_folder": "Malfermu Mod-dosierujon",
|
||||
"horizon.shader_packs.empty": "Neniuj ombraj pakoj: metu ilin en la dosierujon de ombraj pakoj",
|
||||
"horizon.shader_options.open": "Ombrilaj Opcioj...",
|
||||
"horizon.shader_options.profile": "Profilo",
|
||||
"horizon.shader_options.loading": "Legante la opciojn de la ombra pako...",
|
||||
"horizon.agent.copy_details": "Kopiu Detalojn pri Konekto",
|
||||
"horizon.multiplayer.join": "Aliĝu",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Nuligi Rekonekti",
|
||||
@@ -471,5 +468,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Spuroj kaj aŭroj de aliaj ludantoj restas kaŝitaj; iliaj aliaj kosmetikaĵoj ankoraŭ montriĝas. Defaŭlte malŝaltita.",
|
||||
"horizon.extras.cosmetics.shown": "Montrita",
|
||||
"horizon.extras.cosmetics.hidden": "Kaŝita",
|
||||
"horizon.extras.cosmetics.name_tags": "Nomaj etikedoj nur"
|
||||
"horizon.extras.cosmetics.name_tags": "Nomaj etikedoj nur",
|
||||
"Installed": "Instalitaj",
|
||||
"Apply": "Apliki",
|
||||
"Reset all options": "Restarigi ĉiujn agordojn",
|
||||
"A Horizon shader pack.": "Ombrilo-pakaĵo de Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Ombrilo-pakaĵo de OptiFine aŭ Iris.",
|
||||
"In use: press to turn shaders off.": "Uzata: premu por malŝalti ombrilojn.",
|
||||
"Press to use it.": "Premu por uzi ĝin.",
|
||||
"Put shader pack .zip files or folders in it.": "Metu en ĝin .zip-dosierojn aŭ dosierujojn de ombrilo-pakaĵoj.",
|
||||
"Puts every option of this pack back to its default.": "Restarigas ĉiun agordon de ĉi tiu pakaĵo al ĝia defaŭlto.",
|
||||
"Sets many options at once, as the pack groups them.": "Agordas multajn agordojn samtempe, kiel la pakaĵo grupigas ilin.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Ankoraŭ neniu ombrilo-pakaĵo: malfermu la pakaĵujon kaj metu kelkajn en ĝin.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Elektu ombrilo-pakaĵon. Elekti la uzatan malŝaltas ombrilojn.",
|
||||
"Changed options apply with Apply, or when you leave.": "Ŝanĝitaj agordoj efikas per Apliki, aŭ kiam vi foriras.",
|
||||
"Pack settings": "Agordoj de la pakaĵo",
|
||||
"Built-in settings": "Enkonstruitaj agordoj",
|
||||
"%s settings": "Agordoj de %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Agordoj faritaj de la aŭtoro de la pakaĵo: ili ŝanĝas nur ĉi tiun pakaĵon."
|
||||
}
|
||||
@@ -54,8 +54,6 @@
|
||||
"horizon.mods.dev": " (Desarrollo)",
|
||||
"horizon.mods.settings": "Ajustes",
|
||||
"horizon.mods.open_folder": "Abrir carpeta de mods",
|
||||
"horizon.shader_packs.empty": "No hay paquetes de shaders: ponlos en la carpeta shaderpacks",
|
||||
"horizon.shader_packs.kind_horizon": " (Horizon)",
|
||||
"horizon.agent.copy_details": "Copiar datos de conexión",
|
||||
"horizon.multiplayer.join": "Unirse",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Cancelar reconexión",
|
||||
@@ -194,9 +192,7 @@
|
||||
"horizon.pages.shader_options": "Opciones de sombreadores",
|
||||
"horizon.pages.split_profiles": "¿Quién juega?",
|
||||
"horizon.pages.split_profiles.tooltip": "Quién juega: un perfil guardado en esta carpeta del juego.",
|
||||
"horizon.shader_options.open": "Opciones de sombreadores…",
|
||||
"horizon.shader_options.profile": "Perfil",
|
||||
"horizon.shader_options.loading": "Leyendo las opciones del paquete de sombreadores…",
|
||||
"Particle Culling": "Particle Culling",
|
||||
"Draws every particle.": "Draws every particle.",
|
||||
"Skips offscreen, subpixel and terrain-hidden particles.": "Skips offscreen, subpixel and terrain-hidden particles.",
|
||||
@@ -256,5 +252,22 @@
|
||||
"horizon.tempban.list.empty": "No hay baneos temporales",
|
||||
"horizon.tempban.list.count": "Hay %s baneo(s) temporal(es):",
|
||||
"horizon.tempban.list.entry": "%1$s fue baneado por %2$s %3$s (hasta %4$s)",
|
||||
"Shader Packs...": "Paquetes de shaders..."
|
||||
"Shader Packs...": "Paquetes de shaders...",
|
||||
"Installed": "Instalados",
|
||||
"Apply": "Aplicar",
|
||||
"Reset all options": "Restablecer todas las opciones",
|
||||
"A Horizon shader pack.": "Un paquete de shaders de Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Un paquete de shaders de OptiFine o Iris.",
|
||||
"In use: press to turn shaders off.": "En uso: pulsa para desactivar los shaders.",
|
||||
"Press to use it.": "Pulsa para usarlo.",
|
||||
"Put shader pack .zip files or folders in it.": "Pon en ella archivos .zip o carpetas de paquetes de shaders.",
|
||||
"Puts every option of this pack back to its default.": "Devuelve todas las opciones de este paquete a sus valores por defecto.",
|
||||
"Sets many options at once, as the pack groups them.": "Ajusta muchas opciones a la vez, tal como las agrupa el paquete.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Aún no hay paquetes de shaders: abre la carpeta de paquetes y pon algunos en ella.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Elige un paquete de shaders. Elegir el que está en uso desactiva los shaders.",
|
||||
"Changed options apply with Apply, or when you leave.": "Las opciones cambiadas se aplican con Aplicar, o al salir.",
|
||||
"Pack settings": "Ajustes del paquete",
|
||||
"Built-in settings": "Ajustes integrados",
|
||||
"%s settings": "Ajustes de %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Ajustes creados por el autor del paquete: solo cambian este paquete."
|
||||
}
|
||||
@@ -70,10 +70,7 @@
|
||||
"horizon.mods.settings": "Seaded",
|
||||
"horizon.mods.in_world": " (jätke maailm muutuma)",
|
||||
"horizon.mods.open_folder": "Avage kaust Mod",
|
||||
"horizon.shader_packs.empty": "Varjupakkideta: pange need varjupakkide kausta",
|
||||
"horizon.shader_options.open": "Varjundi valikud...",
|
||||
"horizon.shader_options.profile": "Profiil",
|
||||
"horizon.shader_options.loading": "Varjutuspaketi valikute lugemine...",
|
||||
"horizon.agent.copy_details": "Kopeerige ühenduse üksikasjad",
|
||||
"horizon.multiplayer.join": "Liitu",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Tühista uuesti ühendamine",
|
||||
@@ -467,5 +464,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Teiste mängijate jäljed ja aurad jäävad peidetuks; nende muu kosmeetika on endiselt näha. Vaikimisi väljas.",
|
||||
"horizon.extras.cosmetics.shown": "Näidatud",
|
||||
"horizon.extras.cosmetics.hidden": "Peidetud",
|
||||
"horizon.extras.cosmetics.name_tags": "Ainult nimesildid"
|
||||
"horizon.extras.cosmetics.name_tags": "Ainult nimesildid",
|
||||
"Installed": "Paigaldatud",
|
||||
"Apply": "Rakenda",
|
||||
"Reset all options": "Lähtesta kõik valikud",
|
||||
"A Horizon shader pack.": "Horizoni varjutajapakett.",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine'i või Irise varjutajapakett.",
|
||||
"In use: press to turn shaders off.": "Kasutusel: vajuta, et varjutajad välja lülitada.",
|
||||
"Press to use it.": "Vajuta, et seda kasutada.",
|
||||
"Put shader pack .zip files or folders in it.": "Pane sinna varjutajapakettide .zip-failid või kaustad.",
|
||||
"Puts every option of this pack back to its default.": "Seab selle paketi kõik valikud tagasi vaikeväärtustele.",
|
||||
"Sets many options at once, as the pack groups them.": "Seab paljud valikud korraga, nii nagu pakett neid rühmitab.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Varjutajapakette veel pole: ava paketikaust ja pane sinna mõned.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Vali varjutajapakett. Kasutusel oleva valimine lülitab varjutajad välja.",
|
||||
"Changed options apply with Apply, or when you leave.": "Muudetud valikud rakenduvad nupuga Rakenda või väljumisel.",
|
||||
"Pack settings": "Paketi sätted",
|
||||
"Built-in settings": "Sisseehitatud sätted",
|
||||
"%s settings": "%s sätted",
|
||||
"Settings made by the pack's author: they change only this pack.": "Paketi autori tehtud sätted: need muudavad ainult seda paketti."
|
||||
}
|
||||
@@ -72,10 +72,7 @@
|
||||
"horizon.mods.settings": "Ezarpenak",
|
||||
"horizon.mods.in_world": " (utzi mundua aldatzen)",
|
||||
"horizon.mods.open_folder": "Ireki Mod karpeta",
|
||||
"horizon.shader_packs.empty": "Ez dago itzal-paketerik: jarri itzazu itzal-paketeen karpetan",
|
||||
"horizon.shader_options.open": "Itzalaren aukerak...",
|
||||
"horizon.shader_options.profile": "Profila",
|
||||
"horizon.shader_options.loading": "Shader paketearen aukerak irakurtzen...",
|
||||
"horizon.agent.copy_details": "Kopiatu konexioaren xehetasunak",
|
||||
"horizon.multiplayer.join": "Sartu",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Utzi berriro konektatzea",
|
||||
@@ -472,5 +469,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Beste jokalarien arrastoak eta aurak ezkutuan geratzen dira; haien beste kosmetikoak ikusten jarraitzen dute. Desaktibatuta lehenespenez.",
|
||||
"horizon.extras.cosmetics.shown": "Erakutsita",
|
||||
"horizon.extras.cosmetics.hidden": "Ezkutuan",
|
||||
"horizon.extras.cosmetics.name_tags": "Izena etiketak soilik"
|
||||
"horizon.extras.cosmetics.name_tags": "Izena etiketak soilik",
|
||||
"Installed": "Instalatuta",
|
||||
"Apply": "Aplikatu",
|
||||
"Reset all options": "Berrezarri aukera guztiak",
|
||||
"A Horizon shader pack.": "Horizon shader pakete bat.",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine edo Iris shader pakete bat.",
|
||||
"In use: press to turn shaders off.": "Erabileran: sakatu shaderrak desaktibatzeko.",
|
||||
"Press to use it.": "Sakatu erabiltzeko.",
|
||||
"Put shader pack .zip files or folders in it.": "Jarri shader pakete-en .zip fitxategiak edo karpetak bertan.",
|
||||
"Puts every option of this pack back to its default.": "Pakete honen aukera guztiak lehenetsira itzultzen ditu.",
|
||||
"Sets many options at once, as the pack groups them.": "Aukera asko batera ezartzen ditu, paketeak taldekatzen dituen moduan.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Ez dago shader paketerik oraindik: ireki paketeen karpeta eta jarri batzuk bertan.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Aukeratu shader pakete bat. Erabileran dagoena aukeratzeak shaderrak desaktibatzen ditu.",
|
||||
"Changed options apply with Apply, or when you leave.": "Aldatutako aukerak Aplikatu botoiarekin aplikatzen dira, edo irtetean.",
|
||||
"Pack settings": "Paketearen ezarpenak",
|
||||
"Built-in settings": "Ezarpen integratuak",
|
||||
"%s settings": "%s(r)en ezarpenak",
|
||||
"Settings made by the pack's author: they change only this pack.": "Paketearen egileak egindako ezarpenak: pakete hau soilik aldatzen dute."
|
||||
}
|
||||
@@ -69,10 +69,7 @@
|
||||
"horizon.mods.settings": "تنظیمات",
|
||||
"horizon.mods.in_world": " (دنیا را رها کن تا تغییر کند)",
|
||||
"horizon.mods.open_folder": "پوشه Mod را باز کنید",
|
||||
"horizon.shader_packs.empty": "بدون بسته های سایه زن: آنها را در پوشه shaderpacks قرار دهید",
|
||||
"horizon.shader_options.open": "گزینه های سایه زن...",
|
||||
"horizon.shader_options.profile": "نمایه",
|
||||
"horizon.shader_options.loading": "خواندن گزینه های بسته سایه زن...",
|
||||
"horizon.agent.copy_details": "کپی جزئیات اتصال",
|
||||
"horizon.multiplayer.join": "بپیوندید",
|
||||
"horizon.multiplayer.cancel_reconnecting": "لغو اتصال مجدد",
|
||||
@@ -477,5 +474,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "ردها و هالههای بازیکنان دیگر پنهان میمانند؛ دیگر آرایههایشان همچنان دیده میشوند. بهطور پیشفرض خاموش.",
|
||||
"horizon.extras.cosmetics.shown": "نشان داده شده است",
|
||||
"horizon.extras.cosmetics.hidden": "پنهان شده است",
|
||||
"horizon.extras.cosmetics.name_tags": "فقط برچسب های نام"
|
||||
"horizon.extras.cosmetics.name_tags": "فقط برچسب های نام",
|
||||
"Installed": "نصبشده",
|
||||
"Apply": "اعمال",
|
||||
"Reset all options": "بازنشانی همه گزینهها",
|
||||
"A Horizon shader pack.": "یک بسته شیدر Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "یک بسته شیدر OptiFine یا Iris.",
|
||||
"In use: press to turn shaders off.": "در حال استفاده: برای خاموش کردن شیدرها فشار دهید.",
|
||||
"Press to use it.": "برای استفاده فشار دهید.",
|
||||
"Put shader pack .zip files or folders in it.": "فایلهای .zip یا پوشههای بستههای شیدر را در آن بگذارید.",
|
||||
"Puts every option of this pack back to its default.": "همه گزینههای این بسته را به پیشفرض برمیگرداند.",
|
||||
"Sets many options at once, as the pack groups them.": "چندین گزینه را یکجا تنظیم میکند، همانطور که بسته آنها را گروهبندی کرده است.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "هنوز بسته شیدری نیست: پوشه بستهها را باز کنید و چند تا در آن بگذارید.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "یک بسته شیدر انتخاب کنید. انتخاب بسته در حال استفاده شیدرها را خاموش میکند.",
|
||||
"Changed options apply with Apply, or when you leave.": "گزینههای تغییرکرده با اعمال یا هنگام خروج اعمال میشوند.",
|
||||
"Pack settings": "تنظیمات بسته",
|
||||
"Built-in settings": "تنظیمات داخلی",
|
||||
"%s settings": "تنظیمات %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "تنظیماتی که سازنده بسته ساخته است: فقط همین بسته را تغییر میدهند."
|
||||
}
|
||||
@@ -67,10 +67,7 @@
|
||||
"horizon.mods.settings": "Asetukset",
|
||||
"horizon.mods.in_world": " (jätä maailma muuttumaan)",
|
||||
"horizon.mods.open_folder": "Avaa Mod-kansio",
|
||||
"horizon.shader_packs.empty": "Ei Shader-paketteja: laita ne Shaderpacks-kansioon",
|
||||
"horizon.shader_options.open": "Varjostimen asetukset...",
|
||||
"horizon.shader_options.profile": "Profiili",
|
||||
"horizon.shader_options.loading": "Shader-paketin vaihtoehtojen lukeminen...",
|
||||
"horizon.agent.copy_details": "Kopioi yhteystiedot",
|
||||
"horizon.multiplayer.join": "Liity",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Peruuta yhteyden muodostaminen",
|
||||
@@ -453,5 +450,22 @@
|
||||
"horizon.extras.cosmetics.shown": "Näytetään",
|
||||
"horizon.extras.cosmetics.hidden": "Piilotettu",
|
||||
"horizon.extras.cosmetics.name_tags": "Vain nimilaput",
|
||||
"Shader Packs...": "Shader Packit..."
|
||||
"Shader Packs...": "Shader Packit...",
|
||||
"Installed": "Asennetut",
|
||||
"Apply": "Käytä",
|
||||
"Reset all options": "Nollaa kaikki asetukset",
|
||||
"A Horizon shader pack.": "Horizon-varjostinpaketti.",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine- tai Iris-varjostinpaketti.",
|
||||
"In use: press to turn shaders off.": "Käytössä: paina, niin varjostimet poistuvat käytöstä.",
|
||||
"Press to use it.": "Paina käyttääksesi sitä.",
|
||||
"Put shader pack .zip files or folders in it.": "Laita siihen varjostinpakettien .zip-tiedostoja tai kansioita.",
|
||||
"Puts every option of this pack back to its default.": "Palauttaa kaikki tämän paketin asetukset oletusarvoihin.",
|
||||
"Sets many options at once, as the pack groups them.": "Asettaa monta asetusta kerralla sillä tavalla kuin paketti ryhmittelee ne.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Varjostinpaketteja ei vielä ole: avaa pakettikansio ja laita siihen joitakin.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Valitse varjostinpaketti. Käytössä olevan valitseminen poistaa varjostimet käytöstä.",
|
||||
"Changed options apply with Apply, or when you leave.": "Muutetut asetukset otetaan käyttöön Käytä-painikkeella tai poistuessasi.",
|
||||
"Pack settings": "Paketin asetukset",
|
||||
"Built-in settings": "Sisäänrakennetut asetukset",
|
||||
"%s settings": "%s: asetukset",
|
||||
"Settings made by the pack's author: they change only this pack.": "Paketin tekijän tekemät asetukset: ne muuttavat vain tätä pakettia."
|
||||
}
|
||||
@@ -62,9 +62,6 @@
|
||||
"horizon.mods.settings": "Mga setting",
|
||||
"horizon.mods.in_world": " (iwanan ang mundo upang magbago)",
|
||||
"horizon.mods.open_folder": "Buksan ang Mod Folder",
|
||||
"horizon.shader_packs.empty": "Walang shader pack: ilagay ang mga ito sa shaderpacks folder",
|
||||
"horizon.shader_options.open": "Mga Pagpipilian sa Shader...",
|
||||
"horizon.shader_options.loading": "Binabasa ang mga opsyon ng shader pack...",
|
||||
"horizon.agent.copy_details": "Kopyahin ang Mga Detalye ng Koneksyon",
|
||||
"horizon.multiplayer.join": "Sumali",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Kanselahin ang Muling Pagkonekta",
|
||||
@@ -425,5 +422,22 @@
|
||||
"horizon.extras.cosmetics.shown": "Ipinakita",
|
||||
"horizon.extras.cosmetics.hidden": "Nakatago",
|
||||
"horizon.extras.cosmetics.name_tags": "Mga name tag lang",
|
||||
"Shader Packs...": "Mga Shader Pack..."
|
||||
"Shader Packs...": "Mga Shader Pack...",
|
||||
"Installed": "Naka-install",
|
||||
"Apply": "Ilapat",
|
||||
"Reset all options": "I-reset ang lahat ng opsyon",
|
||||
"A Horizon shader pack.": "Isang Horizon shader pack.",
|
||||
"An OptiFine or Iris shader pack.": "Isang OptiFine o Iris shader pack.",
|
||||
"In use: press to turn shaders off.": "Ginagamit: pindutin para patayin ang mga shader.",
|
||||
"Press to use it.": "Pindutin para gamitin ito.",
|
||||
"Put shader pack .zip files or folders in it.": "Ilagay dito ang mga .zip file o folder ng shader pack.",
|
||||
"Puts every option of this pack back to its default.": "Ibinabalik sa default ang bawat opsyon ng pack na ito.",
|
||||
"Sets many options at once, as the pack groups them.": "Nagtatakda ng maraming opsyon nang sabay, ayon sa pagkakagrupo ng pack.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Wala pang shader pack: buksan ang pack folder at maglagay ng ilan.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Pumili ng shader pack. Ang pagpili sa ginagamit ay magpapatay ng mga shader.",
|
||||
"Changed options apply with Apply, or when you leave.": "Ang mga binagong opsyon ay ilalapat sa Ilapat, o kapag umalis ka.",
|
||||
"Pack settings": "Mga setting ng pack",
|
||||
"Built-in settings": "Mga built-in na setting",
|
||||
"%s settings": "Mga setting ng %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Mga setting na ginawa ng may-akda ng pack: ang pack na ito lang ang binabago nila."
|
||||
}
|
||||
@@ -70,10 +70,7 @@
|
||||
"horizon.mods.settings": "Innstillingar",
|
||||
"horizon.mods.in_world": " (lat heimin broytast)",
|
||||
"horizon.mods.open_folder": "Opna Mod mappuna",
|
||||
"horizon.shader_packs.empty": "Ongar skuggapakkar: set teir í mappuna skuggapakkar",
|
||||
"horizon.shader_options.open": "Skuggamøguleikar...",
|
||||
"horizon.shader_options.profile": "Vangamynd",
|
||||
"horizon.shader_options.loading": "Lesa møguleikarnar í skuggapakkanum...",
|
||||
"horizon.agent.copy_details": "Avrita sambandsupplýsingar",
|
||||
"horizon.multiplayer.join": "Ver við",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Avlýs aftursamband",
|
||||
@@ -474,5 +471,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Spor og aurur hjá øðrum leikarum verða fjald; teirra aðrir prýðislutir síggjast framvegis. Sløkt sum standard.",
|
||||
"horizon.extras.cosmetics.shown": "Víst",
|
||||
"horizon.extras.cosmetics.hidden": "Dult",
|
||||
"horizon.extras.cosmetics.name_tags": "Einans navnamerki"
|
||||
"horizon.extras.cosmetics.name_tags": "Einans navnamerki",
|
||||
"Installed": "Innstallað",
|
||||
"Apply": "Nýt",
|
||||
"Reset all options": "Nullstilla allar møguleikar",
|
||||
"A Horizon shader pack.": "Ein Horizon-skuggapakki.",
|
||||
"An OptiFine or Iris shader pack.": "Ein OptiFine- ella Iris-skuggapakki.",
|
||||
"In use: press to turn shaders off.": "Í brúki: trýst fyri at sláa skuggar frá.",
|
||||
"Press to use it.": "Trýst fyri at brúka hann.",
|
||||
"Put shader pack .zip files or folders in it.": "Legg .zip-fílur ella mappur við skuggapakkum í hana.",
|
||||
"Puts every option of this pack back to its default.": "Setur allar møguleikar hjá hesum pakka aftur til sjálvvirði.",
|
||||
"Sets many options at once, as the pack groups them.": "Setur nógvar møguleikar í senn, soleiðis sum pakkin bólkar teir.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Eingir skuggapakkar enn: opna pakkamappuna og legg nakrar í hana.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Vel ein skuggapakka. Um tú velur tann, sum er í brúki, verða skuggar slignir frá.",
|
||||
"Changed options apply with Apply, or when you leave.": "Broyttir møguleikar verða nýttir við Nýt, ella tá tú fer burtur.",
|
||||
"Pack settings": "Pakkaskipanir",
|
||||
"Built-in settings": "Innbygdar skipanir",
|
||||
"%s settings": "Skipanir hjá %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Skipanir gjørdar av høvundanum hjá pakkanum: teir broyta bert hendan pakkan."
|
||||
}
|
||||
@@ -68,10 +68,7 @@
|
||||
"horizon.mods.switch.worldgen": "change la génération du monde",
|
||||
"horizon.mods.switch.registries": "ajoute des blocs, des objets ou d'autres entrées de registre",
|
||||
"horizon.mods.switch.restart": "Redémarre le jeu pour appliquer %s : ils ajoutent des blocs, des objets ou de la génération de monde.",
|
||||
"horizon.shader_packs.empty": "Aucun pack de shaders : placez-les dans le dossier shaderpacks",
|
||||
"horizon.shader_options.open": "Options des shaders...",
|
||||
"horizon.shader_options.profile": "Profil",
|
||||
"horizon.shader_options.loading": "Lecture des options du pack de shaders...",
|
||||
"horizon.agent.copy_details": "Copier les détails de connexion",
|
||||
"horizon.multiplayer.join": "Rejoindre",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Annuler la reconnexion",
|
||||
@@ -359,6 +356,23 @@
|
||||
"Shaders": "Shaders",
|
||||
"Shader packs, Horizon's and shaders/ folder ones, and their options.": "Les packs de shaders, ceux de Horizon et ceux en dossier shaders/, et leurs options.",
|
||||
"Shader packs": "Packs de shaders",
|
||||
"Installed": "Installés",
|
||||
"Apply": "Appliquer",
|
||||
"Reset all options": "Réinitialiser toutes les options",
|
||||
"A Horizon shader pack.": "Un pack de shaders Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Un pack de shaders OptiFine ou Iris.",
|
||||
"In use: press to turn shaders off.": "En cours d'utilisation : appuyez pour désactiver les shaders.",
|
||||
"Press to use it.": "Appuyez pour l'utiliser.",
|
||||
"Put shader pack .zip files or folders in it.": "Mettez-y des fichiers .zip ou des dossiers de packs de shaders.",
|
||||
"Puts every option of this pack back to its default.": "Remet chaque option de ce pack à sa valeur par défaut.",
|
||||
"Sets many options at once, as the pack groups them.": "Règle de nombreuses options à la fois, selon le regroupement du pack.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Aucun pack de shaders pour l'instant : ouvrez le dossier des packs et ajoutez-en.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Choisissez un pack de shaders. Choisir celui en cours d'utilisation désactive les shaders.",
|
||||
"Changed options apply with Apply, or when you leave.": "Les options modifiées s'appliquent avec Appliquer, ou quand vous quittez.",
|
||||
"Pack settings": "Réglages du pack",
|
||||
"Built-in settings": "Réglages intégrés",
|
||||
"%s settings": "Réglages de %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Réglages créés par l'auteur du pack : ils ne modifient que ce pack.",
|
||||
"Vanilla's own preset: sets only vanilla's options, as Minecraft does.": "Le préréglage de vanilla : il ne règle que les options de vanilla, comme Minecraft.",
|
||||
"Fullscreen takes the display over, at the mode below.": "Le plein écran prend tout l'écran, au mode ci-dessous.",
|
||||
"The display mode exclusive fullscreen uses. Applies when you leave.": "Le mode d'affichage du plein écran exclusif. S'applique en quittant.",
|
||||
|
||||
@@ -66,10 +66,7 @@
|
||||
"horizon.mods.settings": "Impostazions",
|
||||
"horizon.mods.in_world": " (lassâ il mont par cambiâ)",
|
||||
"horizon.mods.open_folder": "Viert la cjarte Mod",
|
||||
"horizon.shader_packs.empty": "Nissun grup di ombre: metiju inte cjarte dai grups di ombre",
|
||||
"horizon.shader_options.open": "Opzions di ombrenadôr...",
|
||||
"horizon.shader_options.profile": "Profil",
|
||||
"horizon.shader_options.loading": "Leint lis opzions dal pachetin shader...",
|
||||
"horizon.agent.copy_details": "Copiâ i detais de conession",
|
||||
"horizon.multiplayer.join": "Iscrivitvi",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Annulâ la riconession",
|
||||
@@ -470,5 +467,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Lis olmis e lis aureis dai altris zuiadôrs a restin platadis; i lôr altris cosmetics si viodin ancjemò. Distudât di predefinît.",
|
||||
"horizon.extras.cosmetics.shown": "Mostrât",
|
||||
"horizon.extras.cosmetics.hidden": "Scuindût",
|
||||
"horizon.extras.cosmetics.name_tags": "Dome lis etichetis dai nons"
|
||||
"horizon.extras.cosmetics.name_tags": "Dome lis etichetis dai nons",
|
||||
"Installed": "Instalâts",
|
||||
"Apply": "Apliche",
|
||||
"Reset all options": "Ripristine ducj lis opzions",
|
||||
"A Horizon shader pack.": "Un pachet di shader di Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Un pachet di shader di OptiFine o Iris.",
|
||||
"In use: press to turn shaders off.": "In ûs: frache par distudâ i shader.",
|
||||
"Press to use it.": "Frache par doprâlu.",
|
||||
"Put shader pack .zip files or folders in it.": "Met dentri file .zip o cartelis di pachets di shader.",
|
||||
"Puts every option of this pack back to its default.": "Al torne ogni opzion di chest pachet al so valôr predefinît.",
|
||||
"Sets many options at once, as the pack groups them.": "Al met tantis opzions in t'un colp, come che il pachet lis gruparès.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Ancjemò nissun pachet di shader: vierç la cartele dai pachets e met-ne cualchidun.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Sielç un pachet di shader. Sielzi chel in ûs al distude i shader.",
|
||||
"Changed options apply with Apply, or when you leave.": "Lis opzions cambiadis a vegnin aplicadis cun Apliche, o cuant che tu jessis.",
|
||||
"Pack settings": "Impostazions dal pachet",
|
||||
"Built-in settings": "Impostazions integradis",
|
||||
"%s settings": "Impostazions di %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Impostazions fatis dal autôr dal pachet: a cambin dome chest pachet."
|
||||
}
|
||||
@@ -53,9 +53,6 @@
|
||||
"horizon.mods.settings": "Ynstellings",
|
||||
"horizon.mods.in_world": " (lit de wrâld te feroarjen)",
|
||||
"horizon.mods.open_folder": "Iepenje Mod-map",
|
||||
"horizon.shader_packs.empty": "Gjin shader-pakketten: set se yn 'e shaderpacks-map",
|
||||
"horizon.shader_options.open": "Shader-opsjes ...",
|
||||
"horizon.shader_options.loading": "De opsjes fan it shaderpakket lêze ...",
|
||||
"horizon.agent.copy_details": "Kopiearje Ferbiningsdetails",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Ofbrekke opnij ferbinen",
|
||||
"horizon.multiplayer.join_friend": "Doch mei oan in freon",
|
||||
@@ -402,5 +399,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Spoaren en aura's fan oare spilers bliuwe ferburgen; harren oare kosmetika bliuwt sichtber. Standert út.",
|
||||
"horizon.extras.cosmetics.shown": "Oanjûn",
|
||||
"horizon.extras.cosmetics.hidden": "Ferburgen",
|
||||
"horizon.extras.cosmetics.name_tags": "Allinnich nammelabels"
|
||||
"horizon.extras.cosmetics.name_tags": "Allinnich nammelabels",
|
||||
"Installed": "Ynstallearre",
|
||||
"Apply": "Tapasse",
|
||||
"Reset all options": "Alle opsjes weromsette",
|
||||
"A Horizon shader pack.": "In Horizon-shaderpakket.",
|
||||
"An OptiFine or Iris shader pack.": "In OptiFine- of Iris-shaderpakket.",
|
||||
"In use: press to turn shaders off.": "Yn gebrûk: druk om shaders út te setten.",
|
||||
"Press to use it.": "Druk om it te brûken.",
|
||||
"Put shader pack .zip files or folders in it.": "Set .zip-bestannen of mappen mei shaderpakketten deryn.",
|
||||
"Puts every option of this pack back to its default.": "Set alle opsjes fan dit pakket werom nei de standert.",
|
||||
"Sets many options at once, as the pack groups them.": "Stelt in protte opsjes tagelyk yn, sa't it pakket se groepearret.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Noch gjin shaderpakketten: iepenje de pakketmap en set der guon yn.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Kies in shaderpakket. Dejinge yn gebrûk kieze set shaders út.",
|
||||
"Changed options apply with Apply, or when you leave.": "Wizige opsjes wurde tapast mei Tapasse, of as jo fuortgeane.",
|
||||
"Pack settings": "Pakketynstellingen",
|
||||
"Built-in settings": "Ynboude ynstellingen",
|
||||
"%s settings": "Ynstellingen fan %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Ynstellingen makke troch de skriuwer fan it pakket: se feroarje allinnich dit pakket."
|
||||
}
|
||||
@@ -66,10 +66,7 @@
|
||||
"horizon.mods.settings": "Socruithe",
|
||||
"horizon.mods.in_world": " (fág an domhan a athrú)",
|
||||
"horizon.mods.open_folder": "Oscail Fillteán Mod",
|
||||
"horizon.shader_packs.empty": "Uimh pacáistí shader: iad a chur san fhillteán shaderpacks",
|
||||
"horizon.shader_options.open": "Roghanna Shader...",
|
||||
"horizon.shader_options.profile": "Próifíl",
|
||||
"horizon.shader_options.loading": "Roghanna an phacáiste shader a léamh...",
|
||||
"horizon.agent.copy_details": "Cóipeáil Sonraí Ceangail",
|
||||
"horizon.multiplayer.join": "Bígí",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Cealaigh Athnascadh",
|
||||
@@ -466,5 +463,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Fanann rianta agus aurataí imreoirí eile i bhfolach; feictear a gcuid maisithe eile fós. As de réir réamhshocraithe.",
|
||||
"horizon.extras.cosmetics.shown": "Taispeántar",
|
||||
"horizon.extras.cosmetics.hidden": "i bhfolach",
|
||||
"horizon.extras.cosmetics.name_tags": "Clibeanna ainm amháin"
|
||||
"horizon.extras.cosmetics.name_tags": "Clibeanna ainm amháin",
|
||||
"Installed": "Suiteáilte",
|
||||
"Apply": "Cuir i bhfeidhm",
|
||||
"Reset all options": "Athshocraigh gach rogha",
|
||||
"A Horizon shader pack.": "Pacáiste scáthaitheoirí Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Pacáiste scáthaitheoirí OptiFine nó Iris.",
|
||||
"In use: press to turn shaders off.": "In úsáid: brúigh chun scáthaitheoirí a mhúchadh.",
|
||||
"Press to use it.": "Brúigh chun é a úsáid.",
|
||||
"Put shader pack .zip files or folders in it.": "Cuir comhaid .zip nó fillteáin pacáistí scáthaitheoirí ann.",
|
||||
"Puts every option of this pack back to its default.": "Cuireann sé gach rogha den phacáiste seo ar ais ar an réamhshocrú.",
|
||||
"Sets many options at once, as the pack groups them.": "Socraíonn sé a lán roghanna ag an am céanna, mar a ghrúpálann an pacáiste iad.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Níl aon phacáiste scáthaitheoirí ann fós: oscail fillteán na bpacáistí agus cuir roinnt ann.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Roghnaigh pacáiste scáthaitheoirí. Má roghnaíonn tú an ceann atá in úsáid, múchtar na scáthaitheoirí.",
|
||||
"Changed options apply with Apply, or when you leave.": "Cuirtear roghanna athraithe i bhfeidhm le Cuir i bhfeidhm, nó nuair a fhágann tú.",
|
||||
"Pack settings": "Socruithe an phacáiste",
|
||||
"Built-in settings": "Socruithe ionsuite",
|
||||
"%s settings": "Socruithe %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Socruithe a rinne údar an phacáiste: ní athraíonn siad ach an pacáiste seo."
|
||||
}
|
||||
@@ -70,10 +70,7 @@
|
||||
"horizon.mods.settings": "Suidhichidhean",
|
||||
"horizon.mods.in_world": " (Fàg an saoghal airson atharrachadh)",
|
||||
"horizon.mods.open_folder": "Fosgail pasgan a’ Mhòid",
|
||||
"horizon.shader_packs.empty": "Gun phasgan shader: cuir iad ann am pasgan shaderpacks",
|
||||
"horizon.shader_options.open": "Roghainnean Shader...",
|
||||
"horizon.shader_options.profile": "Pròifil",
|
||||
"horizon.shader_options.loading": "A’ leughadh roghainnean pacaid shader...",
|
||||
"horizon.agent.copy_details": "Dèan lethbhreac den fhiosrachadh ceangail",
|
||||
"horizon.multiplayer.join": "Thig còmhla",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Sguir dhen ath-cheangal",
|
||||
@@ -467,5 +464,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Fanaidh slighean is aurathan chluicheadairean eile falaichte; chithear an cuid maise eile fhathast. Dheth a ghnàth.",
|
||||
"horizon.extras.cosmetics.shown": "Air a shealltainn",
|
||||
"horizon.extras.cosmetics.hidden": "Falaichte",
|
||||
"horizon.extras.cosmetics.name_tags": "Tagaichean ainmean a-mhàin"
|
||||
"horizon.extras.cosmetics.name_tags": "Tagaichean ainmean a-mhàin",
|
||||
"Installed": "Stàlaichte",
|
||||
"Apply": "Cuir an sàs",
|
||||
"Reset all options": "Ath-shuidhich a h-uile roghainn",
|
||||
"A Horizon shader pack.": "Pacaid sgàileadairean Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Pacaid sgàileadairean OptiFine no Iris.",
|
||||
"In use: press to turn shaders off.": "’Ga chleachdadh: brùth gus na sgàileadairean a chur dheth.",
|
||||
"Press to use it.": "Brùth gus a chleachdadh.",
|
||||
"Put shader pack .zip files or folders in it.": "Cuir faidhlichean .zip no pasganan de phacaidean sgàileadairean ann.",
|
||||
"Puts every option of this pack back to its default.": "Cuiridh e a h-uile roghainn dhen phacaid seo air ais dhan bhunait.",
|
||||
"Sets many options at once, as the pack groups them.": "Suidhichidh e iomadh roghainn aig an aon àm, mar a bhios a’ phacaid gan cuimseachadh.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Chan eil pacaid sgàileadairean ann fhathast: fosgail pasgan nam pacaidean agus cuir cuid ann.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Tagh pacaid sgàileadairean. Ma thaghas tu an tè a tha ’ga cleachdadh, thèid na sgàileadairean a chur dheth.",
|
||||
"Changed options apply with Apply, or when you leave.": "Thèid roghainnean atharraichte an sàs le Cuir an sàs, no nuair a dh’fhàgas tu.",
|
||||
"Pack settings": "Roghainnean na pacaid",
|
||||
"Built-in settings": "Roghainnean togte a-steach",
|
||||
"%s settings": "Roghainnean %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Roghainnean a rinn ùghdar na pacaid: chan atharraich iad ach a’ phacaid seo."
|
||||
}
|
||||
@@ -68,10 +68,7 @@
|
||||
"horizon.mods.settings": "Configuración",
|
||||
"horizon.mods.in_world": " (deixar o mundo para cambiar)",
|
||||
"horizon.mods.open_folder": "Abre o cartafol de mod",
|
||||
"horizon.shader_packs.empty": "Sen paquetes de sombreadores: colócaos no cartafol de paquetes de sombreadores",
|
||||
"horizon.shader_options.open": "Opcións de sombreado...",
|
||||
"horizon.shader_options.profile": "Perfil",
|
||||
"horizon.shader_options.loading": "Lendo as opcións do paquete de sombreadores...",
|
||||
"horizon.agent.copy_details": "Copiar os detalles da conexión",
|
||||
"horizon.multiplayer.join": "Únete",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Cancelar a conexión",
|
||||
@@ -466,5 +463,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "As estelas e auras doutros xogadores quedan agochadas; os seus outros cosméticos séguense vendo. Desactivado por defecto.",
|
||||
"horizon.extras.cosmetics.shown": "Amosado",
|
||||
"horizon.extras.cosmetics.hidden": "Oculto",
|
||||
"horizon.extras.cosmetics.name_tags": "Só etiquetas de nome"
|
||||
"horizon.extras.cosmetics.name_tags": "Só etiquetas de nome",
|
||||
"Installed": "Instalados",
|
||||
"Apply": "Aplicar",
|
||||
"Reset all options": "Restablecer todas as opcións",
|
||||
"A Horizon shader pack.": "Un paquete de shaders de Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Un paquete de shaders de OptiFine ou Iris.",
|
||||
"In use: press to turn shaders off.": "En uso: preme para desactivar os shaders.",
|
||||
"Press to use it.": "Preme para usalo.",
|
||||
"Put shader pack .zip files or folders in it.": "Pon nel ficheiros .zip ou cartafoles de paquetes de shaders.",
|
||||
"Puts every option of this pack back to its default.": "Devolve todas as opcións deste paquete ao seu valor por defecto.",
|
||||
"Sets many options at once, as the pack groups them.": "Axusta moitas opcións á vez, como as agrupa o paquete.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Aínda non hai paquetes de shaders: abre o cartafol de paquetes e pon algúns nel.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Escolle un paquete de shaders. Escoller o que está en uso desactiva os shaders.",
|
||||
"Changed options apply with Apply, or when you leave.": "As opcións cambiadas aplícanse con Aplicar, ou ao saír.",
|
||||
"Pack settings": "Axustes do paquete",
|
||||
"Built-in settings": "Axustes integrados",
|
||||
"%s settings": "Axustes de %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Axustes feitos polo autor do paquete: só cambian este paquete."
|
||||
}
|
||||
@@ -68,10 +68,7 @@
|
||||
"horizon.mods.settings": "Nā hoʻonohonoho",
|
||||
"horizon.mods.in_world": " (haʻalele i ka honua e hoʻololi)",
|
||||
"horizon.mods.open_folder": "E wehe i ka waihona mod",
|
||||
"horizon.shader_packs.empty": "ʻAʻohe pūʻolo shader: hoʻokomo iā lākou i loko o ka waihona shaderpacks",
|
||||
"horizon.shader_options.open": "Nā Koho Shader...",
|
||||
"horizon.shader_options.profile": "Pilikino",
|
||||
"horizon.shader_options.loading": "Heluhelu i nā koho o ka shader pack...",
|
||||
"horizon.agent.copy_details": "E kope i nā kikoʻī pili",
|
||||
"horizon.multiplayer.join": "Hui",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Hoʻopau i ka hoʻohui hou ʻana",
|
||||
@@ -461,5 +458,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Hūnā ʻia nā meheu a me nā aura o nā mea pāʻani ʻē aʻe; ʻike ʻia nō kā lākou mau mea hoʻonani ʻē aʻe. Pio ma ke ʻano maʻamau.",
|
||||
"horizon.extras.cosmetics.shown": "Hōʻike ʻia",
|
||||
"horizon.extras.cosmetics.hidden": "Huna",
|
||||
"horizon.extras.cosmetics.name_tags": "Nā inoa inoa wale nō"
|
||||
"horizon.extras.cosmetics.name_tags": "Nā inoa inoa wale nō",
|
||||
"Installed": "Kau ʻia",
|
||||
"Apply": "Hoʻohana",
|
||||
"Reset all options": "Hoʻomaka hou i nā koho a pau",
|
||||
"A Horizon shader pack.": "He pūʻolo malu Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "He pūʻolo malu OptiFine a i ʻole Iris.",
|
||||
"In use: press to turn shaders off.": "I ka hoʻohana ʻia: kaomi e pani i nā malu.",
|
||||
"Press to use it.": "Kaomi e hoʻohana iā ia.",
|
||||
"Put shader pack .zip files or folders in it.": "E waiho i nā faila .zip a i ʻole nā waihona pūʻolo malu i loko.",
|
||||
"Puts every option of this pack back to its default.": "Hoʻihoʻi i kēlā me kēia koho o kēia pūʻolo i kāna paʻamau.",
|
||||
"Sets many options at once, as the pack groups them.": "Hoʻonohonoho i nā koho he nui i ka manawa hoʻokahi, e like me ka hui ʻana o ka pūʻolo.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "ʻAʻohe pūʻolo malu: e wehe i ka waihona pūʻolo a e waiho i kekahi i loko.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "E koho i kekahi pūʻolo malu. Ke koho ʻoe i ka mea e hoʻohana ʻia nei, pani ʻia nā malu.",
|
||||
"Changed options apply with Apply, or when you leave.": "Hoʻohana ʻia nā koho i hoʻololi ʻia me Hoʻohana, a i ʻole ke haʻalele ʻoe.",
|
||||
"Pack settings": "Nā hoʻonohonoho o ka pūʻolo",
|
||||
"Built-in settings": "Nā hoʻonohonoho i kūkulu ʻia",
|
||||
"%s settings": "Nā hoʻonohonoho o %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Nā hoʻonohonoho a ka mea nāna i hana i ka pūʻolo: hoʻololi wale lākou i kēia pūʻolo."
|
||||
}
|
||||
@@ -69,10 +69,7 @@
|
||||
"horizon.mods.settings": "הגדרות",
|
||||
"horizon.mods.in_world": " (עזוב את העולם להשתנות)",
|
||||
"horizon.mods.open_folder": "פתח את תיקיית Mod",
|
||||
"horizon.shader_packs.empty": "ללא חבילות shader: הכניסו אותן לתיקיית shaderpacks",
|
||||
"horizon.shader_options.open": "אפשרויות הצללה...",
|
||||
"horizon.shader_options.profile": "פרופיל",
|
||||
"horizon.shader_options.loading": "קורא את האפשרויות של חבילת הצללה...",
|
||||
"horizon.agent.copy_details": "העתק את פרטי החיבור",
|
||||
"horizon.multiplayer.join": "הצטרף",
|
||||
"horizon.multiplayer.cancel_reconnecting": "בטל חיבור מחדש",
|
||||
@@ -469,5 +466,22 @@
|
||||
"horizon.extras.cosmetics.shown": "מוצג",
|
||||
"horizon.extras.cosmetics.hidden": "מוסתר",
|
||||
"horizon.extras.cosmetics.name_tags": "תגי שם בלבד",
|
||||
"Shader Packs...": "חבילות Shader..."
|
||||
"Shader Packs...": "חבילות Shader...",
|
||||
"Installed": "מותקנות",
|
||||
"Apply": "החל",
|
||||
"Reset all options": "איפוס כל האפשרויות",
|
||||
"A Horizon shader pack.": "חבילת שיידרים של Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "חבילת שיידרים של OptiFine או Iris.",
|
||||
"In use: press to turn shaders off.": "בשימוש: לחצו כדי לכבות שיידרים.",
|
||||
"Press to use it.": "לחצו כדי להשתמש בה.",
|
||||
"Put shader pack .zip files or folders in it.": "הניחו בה קובצי .zip או תיקיות של חבילות שיידרים.",
|
||||
"Puts every option of this pack back to its default.": "מחזיר כל אפשרות של חבילה זו לברירת המחדל.",
|
||||
"Sets many options at once, as the pack groups them.": "קובע הרבה אפשרויות בבת אחת, כפי שהחבילה מקבצת אותן.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "אין עדיין חבילות שיידרים: פתחו את תיקיית החבילות והניחו בה כמה.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "בחרו חבילת שיידרים. בחירה בחבילה שבשימוש מכבה את השיידרים.",
|
||||
"Changed options apply with Apply, or when you leave.": "אפשרויות ששונו יוחלו עם החל, או כשתצאו.",
|
||||
"Pack settings": "הגדרות החבילה",
|
||||
"Built-in settings": "הגדרות מובנות",
|
||||
"%s settings": "הגדרות של %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "הגדרות שיצר מחבר החבילה: הן משנות רק את החבילה הזו."
|
||||
}
|
||||
@@ -62,11 +62,7 @@
|
||||
"horizon.mods.dev": " (देव)",
|
||||
"horizon.mods.settings": "सेटिंग्स",
|
||||
"horizon.mods.open_folder": "मॉड फ़ोल्डर खोलें",
|
||||
"horizon.shader_packs.empty": "कोई शेडर पैक नहीं: उन्हें shaderpacks फ़ोल्डर में रखें",
|
||||
"horizon.shader_packs.kind_horizon": " (क्षितिज)",
|
||||
"horizon.shader_options.open": "शेडर विकल्प...",
|
||||
"horizon.shader_options.profile": "प्रोफ़ाइल",
|
||||
"horizon.shader_options.loading": "शेडर पैक के विकल्प पढ़े जा रहे हैं...",
|
||||
"horizon.agent.copy_details": "कनेक्शन विवरण कॉपी करें",
|
||||
"horizon.multiplayer.join": "शामिल हों",
|
||||
"horizon.multiplayer.cancel_reconnecting": "पुनः कनेक्ट करना रद्द करें",
|
||||
@@ -440,5 +436,22 @@
|
||||
"horizon.repro.bundle": "रीप्रो बंडल: %s",
|
||||
"horizon.repro.error": "रीप्रो: %s",
|
||||
"horizon.repro.no_recording": "निर्यात करने के लिए अभी तक कोई रिकॉर्डिंग नहीं है।",
|
||||
"horizon.repro.replay_started": "रीप्ले शुरू हुआ: %s"
|
||||
"horizon.repro.replay_started": "रीप्ले शुरू हुआ: %s",
|
||||
"Installed": "इंस्टॉल किए गए",
|
||||
"Apply": "लागू करें",
|
||||
"Reset all options": "सभी विकल्प रीसेट करें",
|
||||
"A Horizon shader pack.": "एक Horizon शेडर पैक।",
|
||||
"An OptiFine or Iris shader pack.": "एक OptiFine या Iris शेडर पैक।",
|
||||
"In use: press to turn shaders off.": "उपयोग में: शेडर बंद करने के लिए दबाएँ।",
|
||||
"Press to use it.": "इसे उपयोग करने के लिए दबाएँ।",
|
||||
"Put shader pack .zip files or folders in it.": "शेडर पैक की .zip फ़ाइलें या फ़ोल्डर इसमें रखें।",
|
||||
"Puts every option of this pack back to its default.": "इस पैक के हर विकल्प को उसके डिफ़ॉल्ट पर लौटाता है।",
|
||||
"Sets many options at once, as the pack groups them.": "पैक के समूहों के अनुसार कई विकल्प एक साथ सेट करता है।",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "अभी कोई शेडर पैक नहीं है: पैक फ़ोल्डर खोलें और उसमें कुछ रखें।",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "एक शेडर पैक चुनें। उपयोग में चल रहे पैक को चुनने से शेडर बंद हो जाते हैं।",
|
||||
"Changed options apply with Apply, or when you leave.": "बदले गए विकल्प लागू करें से, या बाहर निकलने पर लागू होते हैं।",
|
||||
"Pack settings": "पैक की सेटिंग",
|
||||
"Built-in settings": "अंतर्निहित सेटिंग",
|
||||
"%s settings": "%s की सेटिंग",
|
||||
"Settings made by the pack's author: they change only this pack.": "पैक के लेखक की बनाई सेटिंग: ये केवल इसी पैक को बदलती हैं।"
|
||||
}
|
||||
@@ -57,10 +57,7 @@
|
||||
"horizon.mods.dev": " (Razvojno)",
|
||||
"horizon.mods.settings": "Postavke",
|
||||
"horizon.mods.open_folder": "Otvori mapu modova",
|
||||
"horizon.shader_packs.empty": "Nema paketa shadera: stavite ih u mapu shaderpacks",
|
||||
"horizon.shader_options.open": "Postavke shadera...",
|
||||
"horizon.shader_options.profile": "Profil",
|
||||
"horizon.shader_options.loading": "Čitanje postavki paketa shadera...",
|
||||
"horizon.agent.copy_details": "Kopiraj pojedinosti veze",
|
||||
"horizon.multiplayer.join": "Pridruži se",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Otkaži ponovno povezivanje",
|
||||
@@ -433,5 +430,22 @@
|
||||
"horizon.repro.bundle": "Paket reprodukcije: %s",
|
||||
"horizon.repro.error": "Reprodukcija: %s",
|
||||
"horizon.repro.no_recording": "Još nema snimke za izvoz.",
|
||||
"horizon.repro.replay_started": "Reprodukcija započeta: %s"
|
||||
"horizon.repro.replay_started": "Reprodukcija započeta: %s",
|
||||
"Installed": "Instalirani",
|
||||
"Apply": "Primijeni",
|
||||
"Reset all options": "Vrati sve opcije na zadano",
|
||||
"A Horizon shader pack.": "Horizon paket shadera.",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine ili Iris paket shadera.",
|
||||
"In use: press to turn shaders off.": "U upotrebi: pritisni za isključivanje shadera.",
|
||||
"Press to use it.": "Pritisni za korištenje.",
|
||||
"Put shader pack .zip files or folders in it.": "Stavi u nju .zip datoteke ili mape paketa shadera.",
|
||||
"Puts every option of this pack back to its default.": "Vraća svaku opciju ovog paketa na zadanu vrijednost.",
|
||||
"Sets many options at once, as the pack groups them.": "Postavlja mnogo opcija odjednom, onako kako ih paket grupira.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Još nema paketa shadera: otvori mapu paketa i stavi nekoliko u nju.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Odaberi paket shadera. Odabir paketa koji je u upotrebi isključuje shadere.",
|
||||
"Changed options apply with Apply, or when you leave.": "Promijenjene opcije primjenjuju se gumbom Primijeni ili kad izađeš.",
|
||||
"Pack settings": "Postavke paketa",
|
||||
"Built-in settings": "Ugrađene postavke",
|
||||
"%s settings": "Postavke za %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Postavke koje je napravio autor paketa: mijenjaju samo ovaj paket."
|
||||
}
|
||||
@@ -66,11 +66,7 @@
|
||||
"horizon.mods.settings": "Beállítások elemre",
|
||||
"horizon.mods.in_world": " (hagyd a világot, hogy megváltozzon)",
|
||||
"horizon.mods.open_folder": "Nyissa meg a Mod mappát",
|
||||
"horizon.shader_packs.empty": "Nincs árnyékoló csomag: helyezze őket a shaderpacks mappába",
|
||||
"horizon.shader_packs.kind_horizon": " (Horizont)",
|
||||
"horizon.shader_options.open": "Árnyékoló opciók...",
|
||||
"horizon.shader_options.profile": "Profil",
|
||||
"horizon.shader_options.loading": "A shader csomag opcióinak elolvasása...",
|
||||
"horizon.agent.copy_details": "Másolja a kapcsolat részleteit",
|
||||
"horizon.multiplayer.join": "Csatlakozz",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Az újracsatlakozás megszakítása",
|
||||
@@ -461,5 +457,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Más játékosok nyomai és aurái rejtve maradnak; a többi kozmetikumuk továbbra is látszik. Alapértelmezetten ki.",
|
||||
"horizon.extras.cosmetics.shown": "Megjelenítve",
|
||||
"horizon.extras.cosmetics.hidden": "Rejtett",
|
||||
"horizon.extras.cosmetics.name_tags": "Csak névcímkék"
|
||||
"horizon.extras.cosmetics.name_tags": "Csak névcímkék",
|
||||
"Installed": "Telepítettek",
|
||||
"Apply": "Alkalmaz",
|
||||
"Reset all options": "Összes beállítás visszaállítása",
|
||||
"A Horizon shader pack.": "Egy Horizon shadercsomag.",
|
||||
"An OptiFine or Iris shader pack.": "Egy OptiFine- vagy Iris-shadercsomag.",
|
||||
"In use: press to turn shaders off.": "Használatban: nyomd meg a shaderek kikapcsolásához.",
|
||||
"Press to use it.": "Nyomd meg a használatához.",
|
||||
"Put shader pack .zip files or folders in it.": "Tegyél bele shadercsomag .zip fájlokat vagy mappákat.",
|
||||
"Puts every option of this pack back to its default.": "A csomag minden beállítását visszaállítja az alapértelmezettre.",
|
||||
"Sets many options at once, as the pack groups them.": "Sok beállítást állít be egyszerre, ahogy a csomag csoportosítja őket.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Még nincs shadercsomag: nyisd meg a csomagmappát, és tegyél bele párat.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Válassz egy shadercsomagot. A használatban lévő kiválasztása kikapcsolja a shadereket.",
|
||||
"Changed options apply with Apply, or when you leave.": "A módosított beállítások az Alkalmaz gombbal vagy kilépéskor lépnek érvénybe.",
|
||||
"Pack settings": "Csomagbeállítások",
|
||||
"Built-in settings": "Beépített beállítások",
|
||||
"%s settings": "%s beállításai",
|
||||
"Settings made by the pack's author: they change only this pack.": "A csomag szerzője által készített beállítások: csak ezt a csomagot módosítják."
|
||||
}
|
||||
@@ -67,9 +67,7 @@
|
||||
"horizon.mods.settings": "Կարգավորումներ",
|
||||
"horizon.mods.in_world": " (թողեք աշխարհը փոխվի)",
|
||||
"horizon.mods.open_folder": "Բացեք Mod թղթապանակը",
|
||||
"horizon.shader_packs.empty": "Շեյդերների փաթեթներ չկան. դրեք դրանք shaderpacks թղթապանակում",
|
||||
"horizon.shader_options.profile": "Անձնագիր",
|
||||
"horizon.shader_options.loading": "Շեյդերների փաթեթի տարբերակների ընթերցում...",
|
||||
"horizon.agent.copy_details": "Պատճենել կապի մանրամասները",
|
||||
"horizon.multiplayer.join": "Միանալ",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Չեղարկել վերամիացումը",
|
||||
@@ -467,5 +465,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Այլ խաղացողների հետքերն ու աուրաները մնում են թաքնված. նրանց մյուս կոսմետիկան դեռ երևում է։ Լռելյայն անջատված է։",
|
||||
"horizon.extras.cosmetics.shown": "Ցուցադրված է",
|
||||
"horizon.extras.cosmetics.hidden": "Թաքնված",
|
||||
"horizon.extras.cosmetics.name_tags": "Միայն անունների պիտակներ"
|
||||
"horizon.extras.cosmetics.name_tags": "Միայն անունների պիտակներ",
|
||||
"Installed": "Տեղադրված",
|
||||
"Apply": "Կիրառել",
|
||||
"Reset all options": "Վերակայել բոլոր ընտրանքները",
|
||||
"A Horizon shader pack.": "Horizon շեյդերների փաթեթ։",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine կամ Iris շեյդերների փաթեթ։",
|
||||
"In use: press to turn shaders off.": "Օգտագործվում է․ սեղմեք՝ շեյդերն անջատելու համար։",
|
||||
"Press to use it.": "Սեղմեք՝ օգտագործելու համար։",
|
||||
"Put shader pack .zip files or folders in it.": "Դրեք դրա մեջ շեյդերների փաթեթների .zip ֆայլեր կամ պանակներ։",
|
||||
"Puts every option of this pack back to its default.": "Այս փաթեթի յուրաքանչյուր ընտրանք վերադարձնում է լռելյայնին։",
|
||||
"Sets many options at once, as the pack groups them.": "Միանգամից սահմանում է բազմաթիվ ընտրանքներ՝ ինչպես փաթեթն է խմբավորել։",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Շեյդերների փաթեթներ դեռ չկան․ բացեք փաթեթների պանակը և դրեք մի քանիսը։",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Ընտրեք շեյդերների փաթեթ։ Օգտագործվողն ընտրելը անջատում է շեյդերները։",
|
||||
"Changed options apply with Apply, or when you leave.": "Փոփոխված ընտրանքները կիրառվում են «Կիրառել» կոճակով կամ դուրս գալիս։",
|
||||
"Pack settings": "Փաթեթի կարգավորումներ",
|
||||
"Built-in settings": "Ներկառուցված կարգավորումներ",
|
||||
"%s settings": "%s կարգավորումներ",
|
||||
"Settings made by the pack's author: they change only this pack.": "Փաթեթի հեղինակի ստեղծած կարգավորումներ․ դրանք փոխում են միայն այս փաթեթը։"
|
||||
}
|
||||
@@ -57,10 +57,7 @@
|
||||
"horizon.mods.dev": " (Pengembang)",
|
||||
"horizon.mods.settings": "Pengaturan",
|
||||
"horizon.mods.open_folder": "Buka Folder Mod",
|
||||
"horizon.shader_packs.empty": "Tidak ada paket shader: masukkan ke dalam folder shaderpacks",
|
||||
"horizon.shader_options.open": "Opsi Shader...",
|
||||
"horizon.shader_options.profile": "Profil",
|
||||
"horizon.shader_options.loading": "Membaca opsi paket shader...",
|
||||
"horizon.agent.copy_details": "Salin Detail Koneksi",
|
||||
"horizon.multiplayer.join": "Gabung",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Batalkan Menyambungkan Ulang",
|
||||
@@ -426,5 +423,22 @@
|
||||
"horizon.repro.bundle": "Bundel reproduksi: %s",
|
||||
"horizon.repro.error": "Reproduksi: %s",
|
||||
"horizon.repro.no_recording": "Belum ada rekaman untuk diekspor.",
|
||||
"horizon.repro.replay_started": "Pemutaran ulang dimulai: %s"
|
||||
"horizon.repro.replay_started": "Pemutaran ulang dimulai: %s",
|
||||
"Installed": "Terpasang",
|
||||
"Apply": "Terapkan",
|
||||
"Reset all options": "Setel ulang semua opsi",
|
||||
"A Horizon shader pack.": "Paket shader Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Paket shader OptiFine atau Iris.",
|
||||
"In use: press to turn shaders off.": "Sedang dipakai: tekan untuk mematikan shader.",
|
||||
"Press to use it.": "Tekan untuk memakainya.",
|
||||
"Put shader pack .zip files or folders in it.": "Taruh file .zip atau folder paket shader di dalamnya.",
|
||||
"Puts every option of this pack back to its default.": "Mengembalikan setiap opsi paket ini ke nilai bawaannya.",
|
||||
"Sets many options at once, as the pack groups them.": "Mengatur banyak opsi sekaligus, sesuai pengelompokan paket.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Belum ada paket shader: buka folder paket dan taruh beberapa di dalamnya.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Pilih paket shader. Memilih yang sedang dipakai akan mematikan shader.",
|
||||
"Changed options apply with Apply, or when you leave.": "Opsi yang diubah diterapkan dengan Terapkan, atau saat Anda keluar.",
|
||||
"Pack settings": "Pengaturan paket",
|
||||
"Built-in settings": "Pengaturan bawaan",
|
||||
"%s settings": "Pengaturan %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Pengaturan buatan pembuat paket: hanya mengubah paket ini."
|
||||
}
|
||||
@@ -71,10 +71,7 @@
|
||||
"horizon.mods.settings": "Ntọala",
|
||||
"horizon.mods.in_world": " (hapụ ụwa ka ọ gbanwee)",
|
||||
"horizon.mods.open_folder": "Mepee folda Mod",
|
||||
"horizon.shader_packs.empty": "Enweghị ngwugwu shader: tinye ha na nchekwa shaderpacks",
|
||||
"horizon.shader_options.open": "Nhọrọ Shader...",
|
||||
"horizon.shader_options.profile": "Profaịlụ",
|
||||
"horizon.shader_options.loading": "Ịgụ nhọrọ mkpọ shader...",
|
||||
"horizon.agent.copy_details": "Detuo nkọwa njikọ",
|
||||
"horizon.multiplayer.join": "Soro",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Kagbuo njikọ ọzọ",
|
||||
@@ -463,5 +460,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Akara ụkwụ na aura ndị egwuregwu ndị ọzọ na-anọgide zoro ezo; ihe ịchọ mma ha ndị ọzọ ka na-egosi. Gbanyụrụ na ndabara.",
|
||||
"horizon.extras.cosmetics.shown": "Egosiri",
|
||||
"horizon.extras.cosmetics.hidden": "Zoro ezo",
|
||||
"horizon.extras.cosmetics.name_tags": "Naanị mkpado aha"
|
||||
"horizon.extras.cosmetics.name_tags": "Naanị mkpado aha",
|
||||
"Installed": "E wụnyere",
|
||||
"Apply": "Tinye ya",
|
||||
"Reset all options": "Weghachi nhọrọ niile",
|
||||
"A Horizon shader pack.": "Mpempe shader Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Mpempe shader OptiFine ma ọ bụ Iris.",
|
||||
"In use: press to turn shaders off.": "A na-eji ya: pịa ka i gbanyụọ shader.",
|
||||
"Press to use it.": "Pịa ka i jiri ya.",
|
||||
"Put shader pack .zip files or folders in it.": "Tinye faịlụ .zip ma ọ bụ folda mpempe shader n'ime ya.",
|
||||
"Puts every option of this pack back to its default.": "Na-eweghachi nhọrọ ọ bụla nke mpempe a na ntọala mbụ.",
|
||||
"Sets many options at once, as the pack groups them.": "Na-ahazi ọtụtụ nhọrọ n'otu oge, dị ka mpempe ahụ si achịkọta ha.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Enweghị mpempe shader ọ bụla ugbu a: mepee folda mpempe ma tinye ụfọdụ n'ime ya.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Họrọ mpempe shader. Ịhọrọ nke a na-eji na-agbanyụ shader.",
|
||||
"Changed options apply with Apply, or when you leave.": "A na-etinye nhọrọ ndị agbanwere site na Tinye ya, ma ọ bụ mgbe i si apụ.",
|
||||
"Pack settings": "Ntọala mpempe",
|
||||
"Built-in settings": "Ntọala e wuru n'ime ya",
|
||||
"%s settings": "Ntọala %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Ntọala onye mere mpempe kere: ha na-agbanwe naanị mpempe a."
|
||||
}
|
||||
@@ -70,10 +70,7 @@
|
||||
"horizon.mods.settings": "Stillingar",
|
||||
"horizon.mods.in_world": " (láttu heiminn breytast)",
|
||||
"horizon.mods.open_folder": "Opnaðu Mod Folder",
|
||||
"horizon.shader_packs.empty": "Engar shader pakkar: settu þá í shaderpacks möppuna",
|
||||
"horizon.shader_options.open": "Skuggavalkostir...",
|
||||
"horizon.shader_options.profile": "Prófíll",
|
||||
"horizon.shader_options.loading": "Er að lesa valkosti shader pakkans...",
|
||||
"horizon.agent.copy_details": "Afritaðu tengingarupplýsingar",
|
||||
"horizon.multiplayer.join": "Vertu með",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Hætta við að tengjast aftur",
|
||||
@@ -469,5 +466,22 @@
|
||||
"horizon.extras.cosmetics.shown": "Sýnd",
|
||||
"horizon.extras.cosmetics.hidden": "Falinn",
|
||||
"horizon.extras.cosmetics.name_tags": "Eingöngu nafnamerki",
|
||||
"Shader Packs...": "Shader pakkar..."
|
||||
"Shader Packs...": "Shader pakkar...",
|
||||
"Installed": "Uppsett",
|
||||
"Apply": "Virkja",
|
||||
"Reset all options": "Endurstilla alla valkosti",
|
||||
"A Horizon shader pack.": "Horizon-skuggapakki.",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine- eða Iris-skuggapakki.",
|
||||
"In use: press to turn shaders off.": "Í notkun: ýttu til að slökkva á skuggum.",
|
||||
"Press to use it.": "Ýttu til að nota hann.",
|
||||
"Put shader pack .zip files or folders in it.": "Settu .zip-skrár eða möppur með skuggapökkum í hana.",
|
||||
"Puts every option of this pack back to its default.": "Setur alla valkosti þessa pakka aftur á sjálfgefið gildi.",
|
||||
"Sets many options at once, as the pack groups them.": "Stillir marga valkosti í einu, eins og pakkinn flokkar þá.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Engir skuggapakkar enn: opnaðu pakkamöppuna og settu nokkra í hana.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Veldu skuggapakka. Ef þú velur þann sem er í notkun slokknar á skuggum.",
|
||||
"Changed options apply with Apply, or when you leave.": "Breyttir valkostir taka gildi með Virkja, eða þegar þú ferð út.",
|
||||
"Pack settings": "Pakkastillingar",
|
||||
"Built-in settings": "Innbyggðar stillingar",
|
||||
"%s settings": "Stillingar %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Stillingar sem höfundur pakkans bjó til: þær breyta aðeins þessum pakka."
|
||||
}
|
||||
@@ -74,10 +74,7 @@
|
||||
"horizon.mods.settings": "Impostazioni",
|
||||
"horizon.mods.in_world": " (lasciare che il mondo cambi)",
|
||||
"horizon.mods.open_folder": "Apri la cartella Mod",
|
||||
"horizon.shader_packs.empty": "Nessun pacchetto shader: inseriscili nella cartella shaderpacks",
|
||||
"horizon.shader_options.open": "Opzioni shader...",
|
||||
"horizon.shader_options.profile": "Profilo",
|
||||
"horizon.shader_options.loading": "Leggendo le opzioni del pacchetto shader...",
|
||||
"horizon.agent.copy_details": "Copia i dettagli della connessione",
|
||||
"horizon.multiplayer.join": "Partecipa",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Annulla riconnessione",
|
||||
@@ -483,5 +480,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Le scie e le aure degli altri giocatori restano nascoste; gli altri loro cosmetici restano visibili. Disattivato per impostazione predefinita.",
|
||||
"horizon.extras.cosmetics.shown": "Mostrato",
|
||||
"horizon.extras.cosmetics.hidden": "Nascosto",
|
||||
"horizon.extras.cosmetics.name_tags": "Solo targhette con nome"
|
||||
"horizon.extras.cosmetics.name_tags": "Solo targhette con nome",
|
||||
"Installed": "Installati",
|
||||
"Apply": "Applica",
|
||||
"Reset all options": "Ripristina tutte le opzioni",
|
||||
"A Horizon shader pack.": "Un pacchetto di shader Horizon.",
|
||||
"An OptiFine or Iris shader pack.": "Un pacchetto di shader OptiFine o Iris.",
|
||||
"In use: press to turn shaders off.": "In uso: premi per disattivare gli shader.",
|
||||
"Press to use it.": "Premi per usarlo.",
|
||||
"Put shader pack .zip files or folders in it.": "Mettici dentro file .zip o cartelle di pacchetti di shader.",
|
||||
"Puts every option of this pack back to its default.": "Riporta ogni opzione di questo pacchetto al valore predefinito.",
|
||||
"Sets many options at once, as the pack groups them.": "Imposta molte opzioni insieme, come le raggruppa il pacchetto.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Nessun pacchetto di shader per ora: apri la cartella dei pacchetti e mettine qualcuno.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Scegli un pacchetto di shader. Scegliere quello in uso disattiva gli shader.",
|
||||
"Changed options apply with Apply, or when you leave.": "Le opzioni modificate si applicano con Applica, o quando esci.",
|
||||
"Pack settings": "Impostazioni del pacchetto",
|
||||
"Built-in settings": "Impostazioni integrate",
|
||||
"%s settings": "Impostazioni di %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Impostazioni create dall'autore del pacchetto: cambiano solo questo pacchetto."
|
||||
}
|
||||
@@ -76,10 +76,7 @@
|
||||
"horizon.mods.settings": "設定",
|
||||
"horizon.mods.in_world": " (世界の変化に任せて)",
|
||||
"horizon.mods.open_folder": "Modフォルダーを開く",
|
||||
"horizon.shader_packs.empty": "シェーダー パックはありません。シェーダー パック フォルダーに入れてください。",
|
||||
"horizon.shader_options.open": "シェーダオプション...",
|
||||
"horizon.shader_options.profile": "プロフィール",
|
||||
"horizon.shader_options.loading": "シェーダー パックのオプションを読んでいます...",
|
||||
"horizon.agent.copy_details": "接続詳細のコピー",
|
||||
"horizon.multiplayer.join": "参加する",
|
||||
"horizon.multiplayer.cancel_reconnecting": "再接続のキャンセル",
|
||||
@@ -490,5 +487,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "他のプレイヤーの軌跡とオーラを非表示にします。それ以外のコスメティックは表示されたままです。デフォルトはオフ。",
|
||||
"horizon.extras.cosmetics.shown": "示されている",
|
||||
"horizon.extras.cosmetics.hidden": "隠された",
|
||||
"horizon.extras.cosmetics.name_tags": "ネームタグのみ"
|
||||
"horizon.extras.cosmetics.name_tags": "ネームタグのみ",
|
||||
"Installed": "インストール済み",
|
||||
"Apply": "適用",
|
||||
"Reset all options": "すべてのオプションをリセット",
|
||||
"A Horizon shader pack.": "Horizon のシェーダーパック。",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine または Iris のシェーダーパック。",
|
||||
"In use: press to turn shaders off.": "使用中: 押すとシェーダーをオフにします。",
|
||||
"Press to use it.": "押すと使用します。",
|
||||
"Put shader pack .zip files or folders in it.": "シェーダーパックの .zip ファイルまたはフォルダーをここに入れてください。",
|
||||
"Puts every option of this pack back to its default.": "このパックのすべてのオプションを初期値に戻します。",
|
||||
"Sets many options at once, as the pack groups them.": "パックのグループ分けに従って、多数のオプションを一度に設定します。",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "シェーダーパックはまだありません: パックフォルダーを開いて入れてください。",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "シェーダーパックを選んでください。使用中のものを選ぶとシェーダーがオフになります。",
|
||||
"Changed options apply with Apply, or when you leave.": "変更したオプションは「適用」を押すか、画面を閉じると反映されます。",
|
||||
"Pack settings": "パックの設定",
|
||||
"Built-in settings": "内蔵設定",
|
||||
"%s settings": "%s の設定",
|
||||
"Settings made by the pack's author: they change only this pack.": "パックの作者が用意した設定: このパックだけが変わります。"
|
||||
}
|
||||
@@ -70,9 +70,7 @@
|
||||
"horizon.mods.settings": "პარამეტრები",
|
||||
"horizon.mods.in_world": " (დატოვე სამყარო, რომ შეიცვალოს)",
|
||||
"horizon.mods.open_folder": "გახსენით Mod Folder",
|
||||
"horizon.shader_packs.empty": "შადერის პაკეტების გარეშე: ჩადეთ ისინი shaderpacks საქაღალდეში",
|
||||
"horizon.shader_options.profile": "პროფილი",
|
||||
"horizon.shader_options.loading": "შადერის პაკეტის ვარიანტების წაკითხვა...",
|
||||
"horizon.agent.copy_details": "დააკოპირეთ კავშირის დეტალები",
|
||||
"horizon.multiplayer.join": "შეუერთდი",
|
||||
"horizon.multiplayer.cancel_reconnecting": "ხელახლა დაკავშირების გაუქმება",
|
||||
@@ -475,5 +473,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "სხვა მოთამაშეების კვალი და აურები დამალული რჩება; მათი სხვა კოსმეტიკა კვლავ ჩანს. ნაგულისხმევად გამორთულია.",
|
||||
"horizon.extras.cosmetics.shown": "ნაჩვენებია",
|
||||
"horizon.extras.cosmetics.hidden": "დამალული",
|
||||
"horizon.extras.cosmetics.name_tags": "მხოლოდ სახელების ტეგები"
|
||||
"horizon.extras.cosmetics.name_tags": "მხოლოდ სახელების ტეგები",
|
||||
"Installed": "დაყენებული",
|
||||
"Apply": "გამოყენება",
|
||||
"Reset all options": "ყველა პარამეტრის გადატვირთვა",
|
||||
"A Horizon shader pack.": "Horizon-ის შეიდერების პაკეტი.",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine-ის ან Iris-ის შეიდერების პაკეტი.",
|
||||
"In use: press to turn shaders off.": "გამოიყენება: დააჭირეთ შეიდერების გამოსართავად.",
|
||||
"Press to use it.": "დააჭირეთ გამოსაყენებლად.",
|
||||
"Put shader pack .zip files or folders in it.": "ჩადეთ მასში შეიდერების პაკეტების .zip ფაილები ან საქაღალდეები.",
|
||||
"Puts every option of this pack back to its default.": "ამ პაკეტის ყველა პარამეტრს სტანდარტულზე აბრუნებს.",
|
||||
"Sets many options at once, as the pack groups them.": "ერთბაშად ადგენს ბევრ პარამეტრს, ისე, როგორც პაკეტი აჯგუფებს მათ.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "შეიდერების პაკეტები ჯერ არ არის: გახსენით პაკეტების საქაღალდე და ჩადეთ რამდენიმე.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "აირჩიეთ შეიდერების პაკეტი. გამოყენებულის არჩევა შეიდერებს გამორთავს.",
|
||||
"Changed options apply with Apply, or when you leave.": "შეცვლილი პარამეტრები გამოიყენება ღილაკით „გამოყენება“ ან გასვლისას.",
|
||||
"Pack settings": "პაკეტის პარამეტრები",
|
||||
"Built-in settings": "ჩაშენებული პარამეტრები",
|
||||
"%s settings": "%s-ის პარამეტრები",
|
||||
"Settings made by the pack's author: they change only this pack.": "პაკეტის ავტორის შექმნილი პარამეტრები: ისინი მხოლოდ ამ პაკეტს ცვლიან."
|
||||
}
|
||||
@@ -73,10 +73,7 @@
|
||||
"horizon.mods.settings": "Параметрлер",
|
||||
"horizon.mods.in_world": " (әлемді өзгерту үшін қалдырыңыз)",
|
||||
"horizon.mods.open_folder": "Мод қалтасын ашыңыз",
|
||||
"horizon.shader_packs.empty": "Шейдер бумалары жоқ: оларды shaderpacks қалтасына салыңыз",
|
||||
"horizon.shader_options.open": "Шейдер опциялары...",
|
||||
"horizon.shader_options.profile": "Профиль",
|
||||
"horizon.shader_options.loading": "Шейдер бумасының опциялары оқылуда...",
|
||||
"horizon.agent.copy_details": "Қосылым мәліметтерін көшіру",
|
||||
"horizon.multiplayer.join": "Қосылыңыз",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Қайта қосылудан бас тарту",
|
||||
@@ -477,5 +474,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Басқа ойыншылардың іздері мен ауралары жасырын қалады; олардың басқа косметикасы көрінеді. Әдепкі бойынша өшірулі.",
|
||||
"horizon.extras.cosmetics.shown": "Көрсетілген",
|
||||
"horizon.extras.cosmetics.hidden": "Жасырын",
|
||||
"horizon.extras.cosmetics.name_tags": "Тек атау тегтері"
|
||||
"horizon.extras.cosmetics.name_tags": "Тек атау тегтері",
|
||||
"Installed": "Орнатылған",
|
||||
"Apply": "Қолдану",
|
||||
"Reset all options": "Барлық опцияларды ысыру",
|
||||
"A Horizon shader pack.": "Horizon шейдер жинағы.",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine немесе Iris шейдер жинағы.",
|
||||
"In use: press to turn shaders off.": "Қолданылуда: шейдерлерді өшіру үшін басыңыз.",
|
||||
"Press to use it.": "Қолдану үшін басыңыз.",
|
||||
"Put shader pack .zip files or folders in it.": "Оған шейдер жинақтарының .zip файлдарын немесе қалталарын салыңыз.",
|
||||
"Puts every option of this pack back to its default.": "Осы жинақтың барлық опцияларын бастапқы мәніне қайтарады.",
|
||||
"Sets many options at once, as the pack groups them.": "Жинақ топтағандай, көп опцияны бірден орнатады.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Шейдер жинақтары әлі жоқ: жинақ қалтасын ашып, бірнешеуін салыңыз.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Шейдер жинағын таңдаңыз. Қолданылып жатқанын таңдасаңыз, шейдерлер өшеді.",
|
||||
"Changed options apply with Apply, or when you leave.": "Өзгертілген опциялар «Қолдану» батырмасымен немесе шыққанда қолданылады.",
|
||||
"Pack settings": "Жинақ параметрлері",
|
||||
"Built-in settings": "Кіріктірілген параметрлер",
|
||||
"%s settings": "%s параметрлері",
|
||||
"Settings made by the pack's author: they change only this pack.": "Жинақ авторы жасаған параметрлер: олар тек осы жинақты өзгертеді."
|
||||
}
|
||||
@@ -74,10 +74,7 @@
|
||||
"horizon.mods.settings": "ಸೆಟ್ಟಿಂಗ್ಗಳು",
|
||||
"horizon.mods.in_world": " (ಜಗತ್ತನ್ನು ಬದಲಾಯಿಸಲು ಬಿಡಿ)",
|
||||
"horizon.mods.open_folder": "ಮಾಡ್ ಫೋಲ್ಡರ್ ತೆರೆಯಿರಿ",
|
||||
"horizon.shader_packs.empty": "ಶೇಡರ್ ಪ್ಯಾಕ್ಗಳಿಲ್ಲ: ಅವುಗಳನ್ನು ಶೇಡರ್ಪ್ಯಾಕ್ಸ್ ಫೋಲ್ಡರ್ನಲ್ಲಿ ಇರಿಸಿ",
|
||||
"horizon.shader_options.open": "ಶೇಡರ್ ಆಯ್ಕೆಗಳು...",
|
||||
"horizon.shader_options.profile": "ಪ್ರೊಫೈಲ್",
|
||||
"horizon.shader_options.loading": "ಶೇಡರ್ ಪ್ಯಾಕ್ನ ಆಯ್ಕೆಗಳನ್ನು ಓದಲಾಗುತ್ತಿದೆ...",
|
||||
"horizon.agent.copy_details": "ಸಂಪರ್ಕ ವಿವರಗಳನ್ನು ನಕಲಿಸಿ",
|
||||
"horizon.multiplayer.join": "ಸೇರಿಕೊಳ್ಳಿ",
|
||||
"horizon.multiplayer.cancel_reconnecting": "ಮರುಸಂಪರ್ಕವನ್ನು ರದ್ದುಮಾಡಿ",
|
||||
@@ -484,5 +481,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "ಇತರ ಆಟಗಾರರ ಹಾದಿಗಳು ಮತ್ತು ಪ್ರಭಾವಳಿಗಳು ಮರೆಯಾಗಿರುತ್ತವೆ; ಅವರ ಇತರ ಅಲಂಕಾರಗಳು ಕಾಣುತ್ತವೆ. ಪೂರ್ವನಿಯೋಜಿತವಾಗಿ ಆಫ್.",
|
||||
"horizon.extras.cosmetics.shown": "ತೋರಿಸಲಾಗಿದೆ",
|
||||
"horizon.extras.cosmetics.hidden": "ಮರೆಮಾಡಲಾಗಿದೆ",
|
||||
"horizon.extras.cosmetics.name_tags": "ಹೆಸರು ಟ್ಯಾಗ್ಗಳು ಮಾತ್ರ"
|
||||
"horizon.extras.cosmetics.name_tags": "ಹೆಸರು ಟ್ಯಾಗ್ಗಳು ಮಾತ್ರ",
|
||||
"Installed": "ಸ್ಥಾಪಿತವಾದವು",
|
||||
"Apply": "ಅನ್ವಯಿಸಿ",
|
||||
"Reset all options": "ಎಲ್ಲಾ ಆಯ್ಕೆಗಳನ್ನು ಮರುಹೊಂದಿಸಿ",
|
||||
"A Horizon shader pack.": "ಒಂದು Horizon ಶೇಡರ್ ಪ್ಯಾಕ್.",
|
||||
"An OptiFine or Iris shader pack.": "ಒಂದು OptiFine ಅಥವಾ Iris ಶೇಡರ್ ಪ್ಯಾಕ್.",
|
||||
"In use: press to turn shaders off.": "ಬಳಕೆಯಲ್ಲಿದೆ: ಶೇಡರ್ಗಳನ್ನು ಆಫ್ ಮಾಡಲು ಒತ್ತಿ.",
|
||||
"Press to use it.": "ಬಳಸಲು ಒತ್ತಿ.",
|
||||
"Put shader pack .zip files or folders in it.": "ಶೇಡರ್ ಪ್ಯಾಕ್ .zip ಫೈಲ್ಗಳು ಅಥವಾ ಫೋಲ್ಡರ್ಗಳನ್ನು ಅದರಲ್ಲಿ ಇರಿಸಿ.",
|
||||
"Puts every option of this pack back to its default.": "ಈ ಪ್ಯಾಕ್ನ ಪ್ರತಿ ಆಯ್ಕೆಯನ್ನು ಅದರ ಡೀಫಾಲ್ಟ್ಗೆ ಮರಳಿಸುತ್ತದೆ.",
|
||||
"Sets many options at once, as the pack groups them.": "ಪ್ಯಾಕ್ ಗುಂಪು ಮಾಡಿರುವಂತೆ, ಹಲವು ಆಯ್ಕೆಗಳನ್ನು ಒಮ್ಮೆಲೇ ಹೊಂದಿಸುತ್ತದೆ.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "ಇನ್ನೂ ಶೇಡರ್ ಪ್ಯಾಕ್ಗಳಿಲ್ಲ: ಪ್ಯಾಕ್ ಫೋಲ್ಡರ್ ತೆರೆದು ಕೆಲವನ್ನು ಇರಿಸಿ.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "ಒಂದು ಶೇಡರ್ ಪ್ಯಾಕ್ ಆರಿಸಿ. ಬಳಕೆಯಲ್ಲಿರುವುದನ್ನು ಆರಿಸಿದರೆ ಶೇಡರ್ಗಳು ಆಫ್ ಆಗುತ್ತವೆ.",
|
||||
"Changed options apply with Apply, or when you leave.": "ಬದಲಿಸಿದ ಆಯ್ಕೆಗಳು ಅನ್ವಯಿಸಿ ಮೂಲಕ, ಅಥವಾ ನೀವು ಹೊರಬಂದಾಗ ಅನ್ವಯವಾಗುತ್ತವೆ.",
|
||||
"Pack settings": "ಪ್ಯಾಕ್ ಸೆಟ್ಟಿಂಗ್ಗಳು",
|
||||
"Built-in settings": "ಅಂತರ್ನಿರ್ಮಿತ ಸೆಟ್ಟಿಂಗ್ಗಳು",
|
||||
"%s settings": "%s ಸೆಟ್ಟಿಂಗ್ಗಳು",
|
||||
"Settings made by the pack's author: they change only this pack.": "ಪ್ಯಾಕ್ನ ಲೇಖಕರು ರಚಿಸಿದ ಸೆಟ್ಟಿಂಗ್ಗಳು: ಅವು ಈ ಪ್ಯಾಕ್ ಅನ್ನು ಮಾತ್ರ ಬದಲಿಸುತ್ತವೆ."
|
||||
}
|
||||
@@ -57,10 +57,7 @@
|
||||
"horizon.mods.dev": " (개발용)",
|
||||
"horizon.mods.settings": "설정",
|
||||
"horizon.mods.open_folder": "모드 폴더 열기",
|
||||
"horizon.shader_packs.empty": "셰이더 팩 없음: shaderpacks 폴더에 넣으세요.",
|
||||
"horizon.shader_options.open": "셰이더 옵션...",
|
||||
"horizon.shader_options.profile": "프로필",
|
||||
"horizon.shader_options.loading": "셰이더 팩 옵션을 읽는 중...",
|
||||
"horizon.agent.copy_details": "연결 세부 정보 복사",
|
||||
"horizon.multiplayer.join": "참가",
|
||||
"horizon.multiplayer.cancel_reconnecting": "재연결 취소",
|
||||
@@ -436,5 +433,22 @@
|
||||
"horizon.repro.bundle": "재현 번들: %s",
|
||||
"horizon.repro.error": "재현 오류: %s",
|
||||
"horizon.repro.no_recording": "아직 내보낼 기록이 없습니다.",
|
||||
"horizon.repro.replay_started": "재생이 시작되었습니다: %s"
|
||||
"horizon.repro.replay_started": "재생이 시작되었습니다: %s",
|
||||
"Installed": "설치됨",
|
||||
"Apply": "적용",
|
||||
"Reset all options": "모든 옵션 초기화",
|
||||
"A Horizon shader pack.": "Horizon 셰이더 팩.",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine 또는 Iris 셰이더 팩.",
|
||||
"In use: press to turn shaders off.": "사용 중: 누르면 셰이더를 끕니다.",
|
||||
"Press to use it.": "눌러서 사용합니다.",
|
||||
"Put shader pack .zip files or folders in it.": "셰이더 팩 .zip 파일이나 폴더를 여기에 넣으세요.",
|
||||
"Puts every option of this pack back to its default.": "이 팩의 모든 옵션을 기본값으로 되돌립니다.",
|
||||
"Sets many options at once, as the pack groups them.": "팩이 묶어 둔 대로 여러 옵션을 한 번에 설정합니다.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "아직 셰이더 팩이 없습니다: 팩 폴더를 열어 몇 개 넣으세요.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "셰이더 팩을 선택하세요. 사용 중인 팩을 선택하면 셰이더가 꺼집니다.",
|
||||
"Changed options apply with Apply, or when you leave.": "변경한 옵션은 적용을 누르거나 나갈 때 적용됩니다.",
|
||||
"Pack settings": "팩 설정",
|
||||
"Built-in settings": "내장 설정",
|
||||
"%s settings": "%s 설정",
|
||||
"Settings made by the pack's author: they change only this pack.": "팩 제작자가 만든 설정: 이 팩에만 적용됩니다."
|
||||
}
|
||||
@@ -66,10 +66,7 @@
|
||||
"horizon.mods.settings": "Орнотуулар",
|
||||
"horizon.mods.in_world": " (өзгөрүү үчүн дүйнөнү таштап)",
|
||||
"horizon.mods.open_folder": "Mod папкасын ачыңыз",
|
||||
"horizon.shader_packs.empty": "Шейдер топтомдору жок: аларды shaderpacks папкасына салыңыз",
|
||||
"horizon.shader_options.open": "Шейдер параметрлери...",
|
||||
"horizon.shader_options.profile": "Профиль",
|
||||
"horizon.shader_options.loading": "Шейдер топтомунун параметрлери окулууда...",
|
||||
"horizon.agent.copy_details": "Туташуу чоо-жайын көчүрүү",
|
||||
"horizon.multiplayer.join": "Кошулуу",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Кайра туташууну жокко чыгаруу",
|
||||
@@ -451,5 +448,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Башка оюнчулардын издери жана ауралары жашыруун калат; алардын башка косметикасы көрүнөт. Демейки боюнча өчүк.",
|
||||
"horizon.extras.cosmetics.shown": "Көрсөтүлдү",
|
||||
"horizon.extras.cosmetics.hidden": "Жашырылган",
|
||||
"horizon.extras.cosmetics.name_tags": "Аталышы гана"
|
||||
"horizon.extras.cosmetics.name_tags": "Аталышы гана",
|
||||
"Installed": "Орнотулган",
|
||||
"Apply": "Колдонуу",
|
||||
"Reset all options": "Бардык параметрлерди баштапкыга келтирүү",
|
||||
"A Horizon shader pack.": "Horizon шейдер топтомү.",
|
||||
"An OptiFine or Iris shader pack.": "OptiFine же Iris шейдер топтомү.",
|
||||
"In use: press to turn shaders off.": "Колдонулуп жатат: шейдерлерди өчүрүү үчүн басыңыз.",
|
||||
"Press to use it.": "Колдонуу үчүн басыңыз.",
|
||||
"Put shader pack .zip files or folders in it.": "Ага шейдер топтомдорунун .zip файлдарын же папкаларын салыңыз.",
|
||||
"Puts every option of this pack back to its default.": "Бул топтомдун бардык параметрлерин демейкисине кайтарат.",
|
||||
"Sets many options at once, as the pack groups them.": "Топтом топтогондой эле, көп параметрди бир убакта коёт.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Шейдер топтомдору азырынча жок: топтомдор папкасын ачып, бир нечесин салыңыз.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Шейдер топтомун тандаңыз. Колдонулуп жатканын тандасаңыз, шейдерлер өчөт.",
|
||||
"Changed options apply with Apply, or when you leave.": "Өзгөртүлгөн параметрлер «Колдонуу» менен же чыкканда колдонулат.",
|
||||
"Pack settings": "Топтом жөндөөлөрү",
|
||||
"Built-in settings": "Орнотулган жөндөөлөр",
|
||||
"%s settings": "%s жөндөөлөрү",
|
||||
"Settings made by the pack's author: they change only this pack.": "Топтомдун автору жасаган жөндөөлөр: алар ушул топтомду гана өзгөртөт."
|
||||
}
|
||||
@@ -48,9 +48,6 @@
|
||||
"horizon.mods.settings": "Occasus",
|
||||
"horizon.mods.in_world": " (Mutationem relinquere mundum)",
|
||||
"horizon.mods.open_folder": "Mod aperta Folder",
|
||||
"horizon.shader_packs.empty": "Non shader sarcinas posuit in shaderpacks folder",
|
||||
"horizon.shader_options.open": "Optiones Shader ...",
|
||||
"horizon.shader_options.loading": "Legens opacum sarcina scriptor optiones...",
|
||||
"horizon.agent.copy_details": "Exemplum Connection Details",
|
||||
"horizon.multiplayer.join_friend": "Iungere amico",
|
||||
"horizon.multiplayer.invite_code": "invitare Code",
|
||||
@@ -402,5 +399,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Vestigia et aurae aliorum lusorum celata manent; cetera ornamenta eorum adhuc videntur. Ex defalta exstinctum.",
|
||||
"horizon.extras.cosmetics.shown": "Ostensum",
|
||||
"horizon.extras.cosmetics.hidden": "Occultus",
|
||||
"horizon.extras.cosmetics.name_tags": "Nomen tags tantum"
|
||||
"horizon.extras.cosmetics.name_tags": "Nomen tags tantum",
|
||||
"Installed": "Instituta",
|
||||
"Apply": "Applica",
|
||||
"Reset all options": "Omnes optiones restitue",
|
||||
"A Horizon shader pack.": "Fasciculus umbrarum Horizontis.",
|
||||
"An OptiFine or Iris shader pack.": "Fasciculus umbrarum OptiFine vel Iris.",
|
||||
"In use: press to turn shaders off.": "In usu: preme ut umbrae exstinguantur.",
|
||||
"Press to use it.": "Preme ut eo utaris.",
|
||||
"Put shader pack .zip files or folders in it.": "Pone in eo fasciculos .zip vel plicas fasciculorum umbrarum.",
|
||||
"Puts every option of this pack back to its default.": "Omnes optiones huius fasciculi ad praefinita restituit.",
|
||||
"Sets many options at once, as the pack groups them.": "Multas optiones simul ponit, ut fasciculus eas colligit.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Nondum fasciculi umbrarum sunt: aperi plicam fasciculorum et aliquos in eam pone.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Fasciculum umbrarum elige. Eum qui in usu est eligere umbras exstinguit.",
|
||||
"Changed options apply with Apply, or when you leave.": "Optiones mutatae applicantur per Applica, vel cum discedis.",
|
||||
"Pack settings": "Optiones fasciculi",
|
||||
"Built-in settings": "Optiones insitae",
|
||||
"%s settings": "Optiones %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Optiones ab auctore fasciculi factae: hunc tantum fasciculum mutant."
|
||||
}
|
||||
@@ -57,10 +57,7 @@
|
||||
"horizon.mods.settings": "Astellungen",
|
||||
"horizon.mods.in_world": " (loosst d'Welt änneren)",
|
||||
"horizon.mods.open_folder": "Open Mod Dossier",
|
||||
"horizon.shader_packs.empty": "Keng Shader Packs: setzt se an den Shaderpacks Dossier",
|
||||
"horizon.shader_options.open": "Shader Optiounen ...",
|
||||
"horizon.shader_options.profile": "Profil",
|
||||
"horizon.shader_options.loading": "Liest d'Optioune vum Shader Pack ...",
|
||||
"horizon.agent.copy_details": "Kopie Verbindung Detailer",
|
||||
"horizon.multiplayer.join": "Maacht mat",
|
||||
"horizon.multiplayer.cancel_reconnecting": "Ofbriechen Reconnecting",
|
||||
@@ -438,5 +435,22 @@
|
||||
"horizon.extras.cosmetics_hide_fx.tooltip": "Spueren an Auraen vun anere Spiller bleiwe verstoppt; hir aner Kosmetik ass ëmmer nach ze gesinn. Standardméisseg aus.",
|
||||
"horizon.extras.cosmetics.shown": "gewisen",
|
||||
"horizon.extras.cosmetics.hidden": "Verstoppt",
|
||||
"horizon.extras.cosmetics.name_tags": "Numm Tags nëmmen"
|
||||
"horizon.extras.cosmetics.name_tags": "Numm Tags nëmmen",
|
||||
"Installed": "Installéiert",
|
||||
"Apply": "Uwenden",
|
||||
"Reset all options": "All Optiounen zrécksetzen",
|
||||
"A Horizon shader pack.": "E Horizon-Shader-Pak.",
|
||||
"An OptiFine or Iris shader pack.": "E OptiFine- oder Iris-Shader-Pak.",
|
||||
"In use: press to turn shaders off.": "Am Gebrauch: dréck fir d'Shader auszeschalten.",
|
||||
"Press to use it.": "Dréck fir en ze benotzen.",
|
||||
"Put shader pack .zip files or folders in it.": "Leet .zip-Dateien oder Dossieren vu Shader-Paken dran.",
|
||||
"Puts every option of this pack back to its default.": "Setzt all Optioun vun dësem Pak op de Standard zréck.",
|
||||
"Sets many options at once, as the pack groups them.": "Stellt vill Optiounen op eemol an, wéi de Pak se gruppéiert.",
|
||||
"No shader packs yet: open the pack folder and put some in it.": "Nach keng Shader-Paken: maach den Pak-Dossier op a leet e puer dran.",
|
||||
"Pick a shader pack. Picking the one in use turns shaders off.": "Wiel e Shader-Pak. Dee vum Gebrauch ze wielen schalt d'Shader aus.",
|
||||
"Changed options apply with Apply, or when you leave.": "Geännert Optioune gi mat Uwenden ugewannt, oder wann dir fortgitt.",
|
||||
"Pack settings": "Pak-Astellungen",
|
||||
"Built-in settings": "Agebaut Astellungen",
|
||||
"%s settings": "Astellunge vu(n) %s",
|
||||
"Settings made by the pack's author: they change only this pack.": "Astellungen, déi den Auteur vum Pak gemaach huet: si änneren nëmmen dëse Pak."
|
||||
}
|
||||
Loaded 100 of 762 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user