Compare commits
1239 Commits
d40f5292b6
...
log_chat_s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1c5b19697 | ||
|
|
7e801c6fd7 | ||
|
|
93619f2766 | ||
|
|
7c44f94755 | ||
|
|
5c51f458f2 | ||
|
|
413aa00ce6 | ||
|
|
0a9b493c14 | ||
|
|
3a86c035b3 | ||
|
|
0a933ba09c | ||
|
|
6b04dbc210 | ||
|
|
c651f733a7 | ||
|
|
4a04f8df83 | ||
|
|
096ad0c413 | ||
|
|
74c4bcc072 | ||
|
|
2516c5a798 | ||
|
|
ef5541cce4 | ||
|
|
efe7bce08a | ||
|
|
a6f5002a84 | ||
|
|
05250933e7 | ||
|
|
f827421b37 | ||
|
|
ac162936ca | ||
|
|
2164700a7a | ||
|
|
d6af55bc04 | ||
|
|
f8c20a0f8d | ||
|
|
d6e2257158 | ||
|
|
a0a0d832f9 | ||
|
|
c063e16c51 | ||
|
|
dd14d72ed3 | ||
|
|
6743904372 | ||
|
|
a20ab8a33a | ||
|
|
85e16032c2 | ||
|
|
924c79b0ad | ||
|
|
ed1ebd536b | ||
|
|
156122f9e3 | ||
|
|
a6c58c0f68 | ||
|
|
ea07cb2cb1 | ||
|
|
bea698b2f1 | ||
|
|
9528f19845 | ||
|
|
d0ca1af555 | ||
|
|
78873b8e00 | ||
|
|
e48a77b67a | ||
|
|
95e698f44e | ||
|
|
a542767720 | ||
|
|
c3f902ebcf | ||
|
|
34e1e05dd8 | ||
|
|
df20709158 | ||
|
|
2cf2d7eed7 | ||
|
|
1300ecc18f | ||
|
|
dcd4c41d81 | ||
|
|
032f20b622 | ||
|
|
4c2f52a465 | ||
|
|
f8eefe310c | ||
|
|
c47ebfbb52 | ||
|
|
2c0a4f5873 | ||
|
|
81147bc3f3 | ||
|
|
efa673d999 | ||
|
|
9e1f7cad55 | ||
|
|
9fccc35046 | ||
|
|
afc6532aa8 | ||
|
|
8869533d07 | ||
|
|
b198eec753 | ||
|
|
f590e806a1 | ||
|
|
3f53abc4c4 | ||
|
|
5de04b0db9 | ||
|
|
0c1875db07 | ||
|
|
638c0d7db1 | ||
|
|
6e98d7f73e | ||
|
|
ec7e5021ae | ||
|
|
b2f32f8907 | ||
|
|
ed3f809982 | ||
|
|
e5cca8d0b4 | ||
|
|
f173b6f604 | ||
|
|
e495f4eca0 | ||
|
|
c07af73eae | ||
|
|
eb09870a10 | ||
|
|
0f22e3d4e4 | ||
|
|
af199a3a37 | ||
|
|
330f438a18 | ||
|
|
c7e67529b5 | ||
|
|
de952e6caf | ||
|
|
2246922118 | ||
|
|
fdb4ce4e0f | ||
|
|
0d479e7c18 | ||
|
|
4436f07fbf | ||
|
|
27fbd3ddeb | ||
|
|
6ae03038cf | ||
|
|
17210c1088 | ||
|
|
827f4682a5 | ||
|
|
a546ca2f92 | ||
|
|
3ea5b08848 | ||
|
|
4aec67c0c6 | ||
|
|
bca1dd599d | ||
|
|
d69deba56f | ||
|
|
81fec36fbb | ||
|
|
f8b9dfc73e | ||
|
|
d4fd66adf9 | ||
|
|
ec75dcd9e9 | ||
|
|
2e16ad47e5 | ||
|
|
7e9d87a808 | ||
|
|
98f7fe0db7 | ||
|
|
575f6624a2 | ||
|
|
962e7e911d | ||
|
|
c588c3ef8d | ||
|
|
70624fa44f | ||
|
|
06e3e013be | ||
|
|
06e4a2e010 | ||
|
|
bcfaa44d66 | ||
|
|
1539741e71 | ||
|
|
3ad38ba503 | ||
|
|
7cef83c51c | ||
|
|
493cf3f1eb | ||
|
|
3c7389f582 | ||
|
|
374166e9d3 | ||
|
|
31607bd0cf | ||
|
|
2b6977d53f | ||
|
|
a04cfaec78 | ||
|
|
13313b4270 | ||
|
|
7175e9062d | ||
|
|
94177848a8 | ||
|
|
9e548fd0f0 | ||
|
|
f4eb5739d6 | ||
|
|
637d8689f5 | ||
|
|
a3ee6645c1 | ||
|
|
649e7e8c28 | ||
|
|
797c71e500 | ||
|
|
e53f2b6044 | ||
|
|
2b2e2d6fe3 | ||
|
|
027e38ee69 | ||
|
|
9e926b0f53 | ||
|
|
f9bce6252c | ||
|
|
dd1528bd4a | ||
|
|
79567f3f8a | ||
|
|
8b8651dcf7 | ||
|
|
b429ca7879 | ||
|
|
1ad1d18abf | ||
|
|
93602c7c95 | ||
|
|
62e692063a | ||
|
|
9478548bdb | ||
|
|
8dc75eec34 | ||
|
|
3e7e4df2ed | ||
|
|
7e8f4a3479 | ||
|
|
86a4fb1d6f | ||
|
|
5ff7c9f269 | ||
|
|
b340c42ab1 | ||
|
|
ec7461f09f | ||
|
|
ba632fbf8b | ||
|
|
89a4823a83 | ||
|
|
bd53aa734e | ||
|
|
cde5208c54 | ||
|
|
e652f95be9 | ||
|
|
00cd438190 | ||
|
|
91ddfd6fc1 | ||
|
|
915ad36ddc | ||
|
|
56e6a9e6dc | ||
|
|
bc1a901f54 | ||
|
|
7d0340ad41 | ||
|
|
42ac32dace | ||
|
|
24c19f64c9 | ||
|
|
af326bfeb8 | ||
|
|
3745377387 | ||
|
|
0ed00a5766 | ||
|
|
6f6d039650 | ||
|
|
2359dce21b | ||
|
|
fd5e05ca6b | ||
|
|
3177de7b2c | ||
|
|
eb550bbbbb | ||
|
|
7004b552c9 | ||
|
|
3f19c81392 | ||
|
|
a62b085f1d | ||
|
|
a514f3a388 | ||
|
|
ebae15a9fe | ||
|
|
10db26fa0b | ||
|
|
47105e42cd | ||
|
|
2cb1e5f8c5 | ||
|
|
79288c6ee3 | ||
|
|
93dc94c1a0 | ||
|
|
79765004fc | ||
|
|
08d07eeb06 | ||
|
|
4cd1b4e208 | ||
|
|
bb2a44705f | ||
|
|
70243850b1 | ||
|
|
dff7cd3bbf | ||
|
|
24a09a94fe | ||
|
|
8144c5684e | ||
|
|
315b26e85c | ||
|
|
ea775ab824 | ||
|
|
1c693a2c4b | ||
|
|
41123d0520 | ||
|
|
d26b9a10ed | ||
|
|
58088ecabe | ||
|
|
a2a7b30727 | ||
|
|
3dcbdaa56e | ||
|
|
0fb42b39d9 | ||
|
|
dc2fa932ea | ||
|
|
d08d03274b | ||
|
|
b6495113c4 | ||
|
|
de78f4130b | ||
|
|
a16486fbfc | ||
|
|
9b94796a32 | ||
|
|
76b8a949e1 | ||
|
|
b0514bdb87 | ||
|
|
961702d818 | ||
|
|
7a2e25af86 | ||
|
|
f71684a120 | ||
|
|
361ec67ad9 | ||
|
|
48a5b5b9bc | ||
|
|
2804d103ae | ||
|
|
59ecb8d8a3 | ||
|
|
148cd911d5 | ||
|
|
2c86411eb4 | ||
|
|
2d755b010c | ||
|
|
619c5a13a1 | ||
|
|
f7e1b4259c | ||
|
|
82f35bbe1c | ||
|
|
b4c767ce48 | ||
|
|
fb5aae827a | ||
|
|
831cce2f9c | ||
|
|
994eddd46f | ||
|
|
85637d580e | ||
|
|
992305a1c0 | ||
|
|
0da4766300 | ||
|
|
fe2c227270 | ||
|
|
955634a520 | ||
|
|
83bbb357d1 | ||
|
|
bd590dfcf4 | ||
|
|
d4a08e5b58 | ||
|
|
9da921756d | ||
|
|
cccd40ca13 | ||
|
|
b5b44c048d | ||
|
|
7d4a590240 | ||
|
|
082f369fe3 | ||
|
|
1a8a3eca4d | ||
|
|
270c37764e | ||
|
|
a8b13d012b | ||
|
|
0230b79269 | ||
|
|
e126c59e94 | ||
|
|
d555baa0c2 | ||
|
|
3f73e7a1fb | ||
|
|
4823c3365f | ||
|
|
7d27df7053 | ||
|
|
4c9ae3678b | ||
|
|
2cf6ac6383 | ||
|
|
7cd7b32042 | ||
|
|
c3b22c9102 | ||
|
|
1124c6e6c2 | ||
|
|
28b02f662f | ||
|
|
792231fbb9 | ||
|
|
f8c8c36307 | ||
|
|
885274e3d7 | ||
|
|
8a0197c88b | ||
|
|
b2215c6dd1 | ||
|
|
42181d77a5 | ||
|
|
f8921226b8 | ||
|
|
c2c56c6374 | ||
|
|
f325e9c7b9 | ||
|
|
c19a7d338b | ||
|
|
7206a33d0d | ||
|
|
c27d6dd1b1 | ||
|
|
28a8f3a01f | ||
|
|
32750700a4 | ||
|
|
be971c4a34 | ||
|
|
d2780382bc | ||
|
|
a66feafa3f | ||
|
|
f0ec8105b3 | ||
|
|
15824ef70f | ||
|
|
c2ca0a1545 | ||
|
|
5aa7ee43db | ||
|
|
7779d0c27c | ||
|
|
288d839456 | ||
|
|
cc9fa4b3ef | ||
|
|
bb0a39ba6c | ||
|
|
c56d2c1bbe | ||
|
|
8ce9e01c1c | ||
|
|
3f0126eef4 | ||
|
|
5f51dec9f9 | ||
|
|
0c5e998e31 | ||
|
|
07d3ab2613 | ||
|
|
de49ad6a79 | ||
|
|
88043e297b | ||
|
|
f416e123f0 | ||
|
|
ec206dc359 | ||
|
|
25be2149c1 | ||
|
|
44c6747dec | ||
|
|
5d8e35088f | ||
|
|
e61db3faea | ||
|
|
0fac8d0d31 | ||
|
|
53f959e4bf | ||
|
|
a4966bc299 | ||
|
|
fa5a347d2e | ||
|
|
3ca951930e | ||
|
|
5f407211ac | ||
|
|
484784bd4d | ||
|
|
77baa7a35f | ||
|
|
3a5358ad7c | ||
|
|
74ead504ec | ||
|
|
8833e95f21 | ||
|
|
d657a72107 | ||
|
|
abc3a79330 | ||
|
|
25571df2b6 | ||
|
|
e4f289921c | ||
|
|
444e849241 | ||
|
|
2e3a30c9ca | ||
|
|
18870cbb03 | ||
|
|
fcae6a26d1 | ||
|
|
aa05081636 | ||
|
|
368ac277d0 | ||
|
|
631cbb1421 | ||
|
|
6fe039f62e | ||
|
|
f5ecb9ad99 | ||
|
|
84378969c3 | ||
|
|
4c04d4fa30 | ||
|
|
abec71c42d | ||
|
|
03c24da611 | ||
|
|
4be3add30a | ||
|
|
c27511dd66 | ||
|
|
12cb5997f0 | ||
|
|
ad19506836 | ||
|
|
0b13ee3e49 | ||
|
|
c92a6be4af | ||
|
|
561466d822 | ||
|
|
c6ed381085 | ||
|
|
aae2ec109b | ||
|
|
9c8470d18e | ||
|
|
c4332c808b | ||
|
|
1cdc823bca | ||
|
|
5c7188be81 | ||
|
|
2a0e787ae7 | ||
|
|
7538943f59 | ||
|
|
99253702c5 | ||
|
|
5c09d5d828 | ||
|
|
daf36769d6 | ||
|
|
ba444de014 | ||
|
|
6a67906823 | ||
|
|
681edb1879 | ||
|
|
b7d929de5a | ||
|
|
6ffd786d5b | ||
|
|
979483b63c | ||
|
|
6be947bbf0 | ||
|
|
5292919917 | ||
|
|
497b5e30ad | ||
|
|
4f1b17b832 | ||
|
|
ad08045e5f | ||
|
|
d3d949176d | ||
|
|
89d906a613 | ||
|
|
8f3ca30840 | ||
|
|
c5e4545ecd | ||
|
|
894d08adf0 | ||
|
|
92b3dde789 | ||
|
|
7fb88b974e | ||
|
|
798593bede | ||
|
|
3e06749006 | ||
|
|
9ffa7be606 | ||
|
|
0313d04970 | ||
|
|
ad13658076 | ||
|
|
0b110a0423 | ||
|
|
34a92f3406 | ||
|
|
b792e3eecf | ||
|
|
e27aa0d379 | ||
|
|
2b1121b970 | ||
|
|
5537a194fa | ||
|
|
56c615d890 | ||
|
|
e8d710dee4 | ||
|
|
4a4217c3b3 | ||
|
|
965a490b70 | ||
|
|
5b7a714ac2 | ||
|
|
90013bc6d3 | ||
|
|
246a1b9fec | ||
|
|
e5ffed2a4f | ||
|
|
c41a1302b6 | ||
|
|
bad58b5821 | ||
|
|
4c551cef00 | ||
|
|
d3b7688a7d | ||
|
|
9c78c45faf | ||
|
|
ce41ec3fc3 | ||
|
|
07ef98a578 | ||
|
|
c541a0e0dc | ||
|
|
f1573d4b0c | ||
|
|
ede12610a8 | ||
|
|
2fbbff2860 | ||
|
|
cd9001e7b9 | ||
|
|
87cff1f6f1 | ||
|
|
39aec39364 | ||
|
|
a2096b6768 | ||
|
|
cf11c6633e | ||
|
|
31154e3d2d | ||
|
|
e10cdc6690 | ||
|
|
f09959ecba | ||
|
|
b2acc653a0 | ||
|
|
78f660124c | ||
|
|
8f46247dc9 | ||
|
|
5676d7be40 | ||
|
|
d459d3883d | ||
|
|
af0dee3382 | ||
|
|
a4317fec57 | ||
|
|
3c2945c1bc | ||
|
|
d136ccc350 | ||
|
|
76cbd5582d | ||
|
|
d26d9e0b9c | ||
|
|
c187d21b95 | ||
|
|
36660b89e9 | ||
|
|
3de6a5fd5a | ||
|
|
737890d395 | ||
|
|
c04041e328 | ||
|
|
c028d8ad29 | ||
|
|
597e8e1573 | ||
|
|
0b03d7ea31 | ||
|
|
ee24d0539a | ||
|
|
25188e849e | ||
|
|
0f7b24a8c0 | ||
|
|
fc8103098e | ||
|
|
79454d8fd2 | ||
|
|
0740991c12 | ||
|
|
117a47f81f | ||
|
|
299c9b5506 | ||
|
|
30300d18bf | ||
|
|
665bebc460 | ||
|
|
385999dce9 | ||
|
|
13991f12c4 | ||
|
|
b623c5aa72 | ||
|
|
37b8912a91 | ||
|
|
e7d49723ea | ||
|
|
e2cb83436c | ||
|
|
2b28da97f9 | ||
|
|
5da2f1ba8a | ||
|
|
3111d06fcf | ||
|
|
6340379e3d | ||
|
|
e78ab81b26 | ||
|
|
e8391de36a | ||
|
|
ad3caf7e00 | ||
|
|
7e669013d9 | ||
|
|
d9425e4119 | ||
|
|
c4756b78ae | ||
|
|
b40b67242f | ||
|
|
37e5b0d74c | ||
|
|
cf3e213e7d | ||
|
|
f679bb1e2e | ||
|
|
9c6ee3ad02 | ||
|
|
a584bd8598 | ||
|
|
efce78179f | ||
|
|
cdef6413e7 | ||
|
|
9913ecb42f | ||
|
|
1707dbd92b | ||
|
|
fceb8a479d | ||
|
|
e186dc428e | ||
|
|
5c0b82e65b | ||
|
|
d379258afb | ||
|
|
e4ec2dfb4a | ||
|
|
e06d74b417 | ||
|
|
ab3b6a0e43 | ||
|
|
e022744b0a | ||
|
|
23b7b56c28 | ||
|
|
7ee79a0e64 | ||
|
|
8f59ddff5d | ||
|
|
db90cfb259 | ||
|
|
c892666b8f | ||
|
|
541cf3b720 | ||
|
|
0338258b45 | ||
|
|
ce3ad6fbb3 | ||
|
|
1b2c119b58 | ||
|
|
a324af1afd | ||
|
|
715d882456 | ||
|
|
39a7e477c6 | ||
|
|
85999d7a40 | ||
|
|
6ac33befe5 | ||
|
|
cfde11556f | ||
|
|
71ae0aedfb | ||
|
|
6193da6899 | ||
|
|
459bbf0025 | ||
|
|
96de59f75a | ||
|
|
ebf90970b2 | ||
|
|
747f41a8c3 | ||
|
|
639dc7c288 | ||
|
|
6ee274be6b | ||
|
|
017e645557 | ||
|
|
bcbf88719f | ||
|
|
81aa068b15 | ||
|
|
2a3ef8f910 | ||
|
|
0d30b19b1b | ||
|
|
98fe3c98ab | ||
|
|
cc632660eb | ||
|
|
1f65fd234b | ||
|
|
052c74e2d7 | ||
|
|
611ac8ce54 | ||
|
|
c6d5a1a182 | ||
|
|
09259be796 | ||
|
|
409efe7f4a | ||
|
|
0ea4bb10a1 | ||
|
|
56109b0ca1 | ||
|
|
1fda679b9f | ||
|
|
440987c0a4 | ||
|
|
cdbcfb9e20 | ||
|
|
19585ec7ac | ||
|
|
ddfdfcfee3 | ||
|
|
49fd39b4b6 | ||
|
|
55787fb7b7 | ||
|
|
6a8e5c7c83 | ||
|
|
037326024b | ||
|
|
04e9cef38e | ||
|
|
2126f3c5a2 | ||
|
|
bdc50899de | ||
|
|
75d4fa2509 | ||
|
|
9d77ca2bf8 | ||
|
|
5f9e0ffd43 | ||
|
|
dfa922de91 | ||
|
|
c66f63805a | ||
|
|
f2e582ce83 | ||
|
|
e903baf680 | ||
|
|
94f2ce4f56 | ||
|
|
dc4c596fa0 | ||
|
|
b9adbf8089 | ||
|
|
427e56d718 | ||
|
|
01819714a2 | ||
|
|
aa121dcb2f | ||
|
|
9c568aba33 | ||
|
|
22d3b5e5a1 | ||
|
|
04b61a20ea | ||
|
|
417f787294 | ||
|
|
0536c58b78 | ||
|
|
711055a3bc | ||
|
|
0efbefc62a | ||
|
|
c7cc9a68fd | ||
|
|
5450572e0a | ||
|
|
8b4478260e | ||
|
|
d9cfb2c278 | ||
|
|
1904012604 | ||
|
|
2c221fdc68 | ||
|
|
955bf96a84 | ||
|
|
f210b39bbc | ||
|
|
59c0c908cc | ||
|
|
640aa6545b | ||
|
|
269542c4b0 | ||
|
|
f820adab51 | ||
|
|
48d5b3c8d9 | ||
|
|
81875a1f3f | ||
|
|
0bd139b061 | ||
|
|
9719c1a3b0 | ||
|
|
0051d6fd63 | ||
|
|
2a1eeae7ca | ||
|
|
43325fdee7 | ||
|
|
087aebe058 | ||
|
|
18450aaff0 | ||
|
|
1af1ebd9e3 | ||
|
|
f76009d095 | ||
|
|
79875056bc | ||
|
|
0eb173e046 | ||
|
|
7d9aa58103 | ||
|
|
874fa67036 | ||
|
|
bf7f81b7fe | ||
|
|
cf7f57252e | ||
|
|
e2296ad2d1 | ||
|
|
332ab244a7 | ||
|
|
ed90322a0b | ||
|
|
9809f6ed08 | ||
|
|
7401182a1b | ||
|
|
2c68964566 | ||
|
|
c639905119 | ||
|
|
9bcf1b3295 | ||
|
|
feef5be143 | ||
|
|
f805d67741 | ||
|
|
497251c0eb | ||
|
|
c111cea7fc | ||
|
|
eff4c66410 | ||
|
|
78e47ea70a | ||
|
|
147cb566f2 | ||
|
|
8452f71481 | ||
|
|
8b11b499ed | ||
|
|
ecaa414508 | ||
|
|
c09d7cbdea | ||
|
|
5a0c8439fc | ||
|
|
43219e16da | ||
|
|
e4539e9cb5 | ||
|
|
67254e0b39 | ||
|
|
09063d23da | ||
|
|
42989c74aa | ||
|
|
637faac90e | ||
|
|
065040e5b8 | ||
|
|
b62c883951 | ||
|
|
63e8835fa0 | ||
|
|
70f62dba4b | ||
|
|
564da2fb8d | ||
|
|
c6af758ee5 | ||
|
|
b57be1cc08 | ||
|
|
c84d088dfa | ||
|
|
631cf4ba74 | ||
|
|
02e1c7444b | ||
|
|
60a752cdef | ||
|
|
48a2e5c7a1 | ||
|
|
52a605787b | ||
|
|
f43d2fb98e | ||
|
|
fb55764887 | ||
|
|
346d267e26 | ||
|
|
8cd08e6a58 | ||
|
|
53218414f8 | ||
|
|
ef1390bbdf | ||
|
|
6bd64677ec | ||
|
|
559a76be8d | ||
|
|
6f6fb5b393 | ||
|
|
10856ccfd0 | ||
|
|
8f14dd6113 | ||
|
|
791595e3ae | ||
|
|
ca3ab78cf8 | ||
|
|
67855f2adf | ||
|
|
f06d7d29ef | ||
|
|
b72d4ab7c6 | ||
|
|
d22bdbe944 | ||
|
|
d24533d561 | ||
|
|
1334575ba9 | ||
|
|
9c6cb501a4 | ||
|
|
5048a50403 | ||
|
|
d450ef43c6 | ||
|
|
6dd076b27f | ||
|
|
79ae71a517 | ||
|
|
bb17cfa179 | ||
|
|
d1583e8587 | ||
|
|
86b9227577 | ||
|
|
d010157611 | ||
|
|
6794b2dc40 | ||
|
|
00f504f6aa | ||
|
|
014bca449f | ||
|
|
464e0dc7d2 | ||
|
|
d6285affec | ||
|
|
b29b685058 | ||
|
|
771b9ddfda | ||
|
|
a69ea79d83 | ||
|
|
c0839d4521 | ||
|
|
fff41a44ed | ||
|
|
eac6d4b617 | ||
|
|
720b925fd5 | ||
|
|
b60b1e369a | ||
|
|
014cbc0cbd | ||
|
|
806f0fd270 | ||
|
|
4e9ef7a334 | ||
|
|
a44e956ed9 | ||
|
|
6bcfc20533 | ||
|
|
e1db0b89bd | ||
|
|
168d5171f4 | ||
|
|
b4882a8b03 | ||
|
|
03dd99699b | ||
|
|
9524db20fe | ||
|
|
96235654f1 | ||
|
|
833e6bd652 | ||
|
|
073ce4a718 | ||
|
|
507cdf1256 | ||
|
|
b5b16df9e0 | ||
|
|
0e438dd508 | ||
|
|
b0899d0ee4 | ||
|
|
9c60ac23a3 | ||
|
|
c050b211eb | ||
|
|
29c4aebe19 | ||
|
|
87aa7c11c5 | ||
|
|
ab50182c92 | ||
|
|
327c1ba23b | ||
|
|
2aa37d9392 | ||
|
|
b09c4f3770 | ||
|
|
251680056d | ||
|
|
e9eb0da2b3 | ||
|
|
e2b26754fe | ||
|
|
87850378c7 | ||
|
|
b073155018 | ||
|
|
7775b42b59 | ||
|
|
a9e5744a37 | ||
|
|
b3b82b97ea | ||
|
|
2612e7f297 | ||
|
|
c39d10b780 | ||
|
|
f21572d22a | ||
|
|
ab6dc5db32 | ||
|
|
b070c0818f | ||
|
|
ec4449f092 | ||
|
|
5d7a24d5d0 | ||
|
|
41f28f2519 | ||
|
|
25dd0508c2 | ||
|
|
a79b449fb2 | ||
|
|
56fabb4561 | ||
|
|
bdce0570bd | ||
|
|
0a7e802d4b | ||
|
|
dd9aca83dd | ||
|
|
32bc99a11a | ||
|
|
1218ca09ae | ||
|
|
db2cf125f8 | ||
|
|
f090f1a02b | ||
|
|
536ffb3f31 | ||
|
|
c124684197 | ||
|
|
0c4dff77c9 | ||
|
|
8101c70c91 | ||
|
|
7f05227e56 | ||
|
|
323204014c | ||
|
|
8a56e14d7a | ||
|
|
77eba309b2 | ||
|
|
a2edc82b0c | ||
|
|
00203201f4 | ||
|
|
c779f4de8d | ||
|
|
fc8c533129 | ||
|
|
723ba5a507 | ||
|
|
2d0b5f5fea | ||
|
|
da8eb68d9d | ||
|
|
058b725ca9 | ||
|
|
1cd3e1bf3f | ||
|
|
c24913ea24 | ||
|
|
9761a68cd4 | ||
|
|
4e5556dccc | ||
|
|
9a46f3053a | ||
|
|
bc3c97398b | ||
|
|
50d4936e51 | ||
|
|
8c5a286574 | ||
|
|
0c0c65df78 | ||
|
|
5c741821a2 | ||
|
|
04d45e1f54 | ||
|
|
014163d7d3 | ||
|
|
2c435c0506 | ||
|
|
6bc613e93f | ||
|
|
b313f47660 | ||
|
|
d05b07a5b0 | ||
|
|
ab28e6a75a | ||
|
|
14ef6b5774 | ||
|
|
578a9fe457 | ||
|
|
ebaed9966b | ||
|
|
b8756c4737 | ||
|
|
88fb83bc57 | ||
|
|
05ed9d9a73 | ||
|
|
261bf88382 | ||
|
|
7b9a173f5a | ||
|
|
5164a11c15 | ||
|
|
0649f3dc32 | ||
|
|
9d5d2ab493 | ||
|
|
822a29aa76 | ||
|
|
ff276b4877 | ||
|
|
bd809e5af7 | ||
|
|
0aac5885fb | ||
|
|
bfb159b9b3 | ||
|
|
c45e78cf1d | ||
|
|
62d1f002dd | ||
|
|
84f8c6a4c6 | ||
|
|
c1f99cb094 | ||
|
|
c64eea7872 | ||
|
|
bebe3f710b | ||
|
|
0bfa53b58d | ||
|
|
e8d5c005a2 | ||
|
|
c58621e036 | ||
|
|
ff5b5149b3 | ||
|
|
81bcff0f8a | ||
|
|
bdc142ae51 | ||
|
|
7b01204eed | ||
|
|
734afd8bcf | ||
|
|
5ddc7b1177 | ||
|
|
59f6a6a2c2 | ||
|
|
3716ffb3b5 | ||
|
|
4312a4d3f4 | ||
|
|
dccab8fd21 | ||
|
|
0138bc9b3f | ||
|
|
a5dd4ffe36 | ||
|
|
fb420e88bd | ||
|
|
0d84804d81 | ||
|
|
b3168928c6 | ||
|
|
c1de85f700 | ||
|
|
387554acbb | ||
|
|
446d37b832 | ||
|
|
e9e6c4b988 | ||
|
|
2ed0656d1b | ||
|
|
fe35c42ead | ||
|
|
a16542e052 | ||
|
|
06cc47847d | ||
|
|
387e594f4a | ||
|
|
0d97f3374c | ||
|
|
ccb1bd7a74 | ||
|
|
2a3271b0d0 | ||
|
|
6a7159e9a1 | ||
|
|
cf0e73e75e | ||
|
|
34a68cd2ca | ||
|
|
cac0438d48 | ||
|
|
9302bac619 | ||
|
|
616046179f | ||
|
|
0c572a862c | ||
|
|
64b2bd4735 | ||
|
|
f337450348 | ||
|
|
bc29707e87 | ||
|
|
a20f5981e2 | ||
|
|
b693f20789 | ||
|
|
69d9bbb400 | ||
|
|
fc4ebf332d | ||
|
|
1a3d3cd31e | ||
|
|
056420c834 | ||
|
|
2af417bf49 | ||
|
|
cbcd413ed1 | ||
|
|
8c174551f5 | ||
|
|
6b463f9d9e | ||
|
|
239891070d | ||
|
|
5fc36bd45f | ||
|
|
2f331548e1 | ||
|
|
34262fb33c | ||
|
|
fa0254cb27 | ||
|
|
d91d1f5466 | ||
|
|
84e6623773 | ||
|
|
0f4f52fa1a | ||
|
|
b48a74f065 | ||
|
|
64f35feb13 | ||
|
|
670067ae3a | ||
|
|
c63be29770 | ||
|
|
4f8ae422ec | ||
|
|
f24a966f53 | ||
|
|
78fdfbfb09 | ||
|
|
bdb0cfe243 | ||
|
|
106c18480f | ||
|
|
97c61e0068 | ||
|
|
6c9e33b76c | ||
|
|
e5a7b6e795 | ||
|
|
affc98851c | ||
|
|
10d7436cf0 | ||
|
|
5fcda6a1a4 | ||
|
|
2c96f6ec8b | ||
|
|
377bb4b6cc | ||
|
|
150e28a672 | ||
|
|
cf21c8e906 | ||
|
|
7859b913bc | ||
|
|
2481bddf58 | ||
|
|
c547f3f26d | ||
|
|
a4bb58007f | ||
|
|
ed5c7bb836 | ||
|
|
af81dca3ff | ||
|
|
188f0e2451 | ||
|
|
0c43801a2c | ||
|
|
8f985118cd | ||
|
|
ec09e618ff | ||
|
|
799c4c9e3c | ||
|
|
25a6b4b6b9 | ||
|
|
714f2c6dc2 | ||
|
|
e04439ea14 | ||
|
|
24093fd0c5 | ||
|
|
c18d10c846 | ||
|
|
4fca85f63d | ||
|
|
dc0f26a1cd | ||
|
|
d4457a4028 | ||
|
|
6c032bb8f7 | ||
|
|
a3d0a50f4d | ||
|
|
519db10f61 | ||
|
|
00a23e6c11 | ||
|
|
6e89bef657 | ||
|
|
5f97e2de5a | ||
|
|
7b82d85b27 | ||
|
|
ac610c54eb | ||
|
|
63247d2d11 | ||
|
|
d3c44de5d2 | ||
|
|
ade07607a5 | ||
|
|
3760b14235 | ||
|
|
a054d2115d | ||
|
|
9d29303142 | ||
|
|
12e1d327ef | ||
|
|
09834d3954 | ||
|
|
8fda46e241 | ||
|
|
8993901641 | ||
|
|
2fe13fe442 | ||
|
|
1a4f366e4b | ||
|
|
2d332d0a13 | ||
|
|
d630a6ef7d | ||
|
|
0c22499534 | ||
|
|
4077f28285 | ||
|
|
4e031a6ea5 | ||
|
|
311d55ff45 | ||
|
|
64cdfcbeab | ||
|
|
6026d088c8 | ||
|
|
53e4d0dd87 | ||
|
|
2ced4abc24 | ||
|
|
b58c1ea5bc | ||
|
|
ca6aa5ebf1 | ||
|
|
f979e6da0f | ||
|
|
32121a38f4 | ||
|
|
2fde98a0d1 | ||
|
|
ead78bc3a3 | ||
|
|
00857df990 | ||
|
|
1a037c06bf | ||
|
|
0741439dd6 | ||
|
|
0e5447d6d2 | ||
|
|
680144b24f | ||
|
|
9a1823d805 | ||
|
|
e96865b55e | ||
|
|
8ba144f43a | ||
|
|
1f10dafbea | ||
|
|
da638a495c | ||
|
|
d83a871520 | ||
|
|
a0b1bdd154 | ||
|
|
34ff23d030 | ||
|
|
f7f304a2e0 | ||
|
|
32ad81d0ff | ||
|
|
ea3a62927d | ||
|
|
ba951b6470 | ||
|
|
6c56ea4c55 | ||
|
|
6a86a99fce | ||
|
|
f270cb3bde | ||
|
|
aa5b193746 | ||
|
|
adf515d50b | ||
|
|
da507b2eed | ||
|
|
adf6a81862 | ||
|
|
264564d006 | ||
|
|
02d31a2f2c | ||
|
|
59473bdf9f | ||
|
|
dd7441e0b4 | ||
|
|
ad833a6fbb | ||
|
|
9f196f2693 | ||
|
|
ac6934405e | ||
|
|
d8100cb9f2 | ||
|
|
2733ed4b1c | ||
|
|
018777472a | ||
|
|
20f6e01afe | ||
|
|
3904576574 | ||
|
|
c4acd8b361 | ||
|
|
8529512edb | ||
|
|
c20cffad5c | ||
|
|
07ed6a889e | ||
|
|
d67e0a4eef | ||
|
|
8e7fa26709 | ||
|
|
deacc7ad65 | ||
|
|
65361ed8dc | ||
|
|
bb1e830264 | ||
|
|
ca6b87d05e | ||
|
|
855568cab7 | ||
|
|
6b0db6699d | ||
|
|
ab9b393238 | ||
|
|
61b124ddf5 | ||
|
|
678b238c1c | ||
|
|
304fc458eb | ||
|
|
caad8ba44b | ||
|
|
db8a28f2c0 | ||
|
|
0f5b78442b | ||
|
|
a5e472dfe6 | ||
|
|
4b7036be0f | ||
|
|
6386e96134 | ||
|
|
b267374d20 | ||
|
|
342fc5b0e9 | ||
|
|
ff49411bc1 | ||
|
|
c80020f451 | ||
|
|
54c2f7d2b4 | ||
|
|
59ad9e3cd7 | ||
|
|
ead6bdecb6 | ||
|
|
104801cdca | ||
|
|
e6914f707a | ||
|
|
acca837142 | ||
|
|
330ca92045 | ||
|
|
499efa1d0a | ||
|
|
89e1d71aec | ||
|
|
2faae285db | ||
|
|
bdef619803 | ||
|
|
3f4f9e7354 | ||
|
|
3b67e425ed | ||
|
|
c8efc5fdd7 | ||
|
|
c2568ebd1f | ||
|
|
2996a1ddde | ||
|
|
2ea2106eca | ||
|
|
9f526610dd | ||
|
|
3259737774 | ||
|
|
360f24f609 | ||
|
|
60cbf79c9b | ||
|
|
d98017c140 | ||
|
|
c0ae7ea497 | ||
|
|
ac53b89421 | ||
|
|
46ba8ef5dd | ||
|
|
cc0f116194 | ||
|
|
8dc255f401 | ||
|
|
dd4bbc3546 | ||
|
|
db0aabcb88 | ||
|
|
a086fdaa5b | ||
|
|
ee6f8ae45d | ||
|
|
91802e6f10 | ||
|
|
d1797a021f | ||
|
|
3ae617c55b | ||
|
|
f6614c1c58 | ||
|
|
889de5e08a | ||
|
|
b97d1a4c6c | ||
|
|
cfde0d7867 | ||
|
|
399cef8fb2 | ||
|
|
58e447d8d0 | ||
|
|
43f339b91e | ||
|
|
9534443771 | ||
|
|
39755a2fce | ||
|
|
73be3641ea | ||
|
|
03b413a892 | ||
|
|
31c37662cf | ||
|
|
57a452a705 | ||
|
|
9d174cd87d | ||
|
|
9a3c39878d | ||
|
|
498c6e3d8b | ||
|
|
25cb3728ca | ||
|
|
fbe147ce61 | ||
|
|
eb287d9b8d | ||
|
|
4dd787be13 | ||
|
|
5d91b678bb | ||
|
|
b3ee3551ca | ||
|
|
2e5ef7f059 | ||
|
|
72646fc7ff | ||
|
|
01fec1ae02 | ||
|
|
3be1de230c | ||
|
|
e83e580f23 | ||
|
|
8e80117eb8 | ||
|
|
0c2d060d43 | ||
|
|
5157bc375d | ||
|
|
b35b560ca1 | ||
|
|
c0da41a18a | ||
|
|
d9f5588a1f | ||
|
|
61c3c252ea | ||
|
|
ed3ca78667 | ||
|
|
6fb7bb1c08 | ||
|
|
57cef527ba | ||
|
|
48a2a75211 | ||
|
|
3f0159cd89 | ||
|
|
7baae40b2d | ||
|
|
fc0bdce151 | ||
|
|
64de28427c | ||
|
|
c4ca3ca743 | ||
|
|
724511e244 | ||
|
|
e3646595ab | ||
|
|
d2ecd0c777 | ||
|
|
a24308baa5 | ||
|
|
aa8e85fbf4 | ||
|
|
11a892f991 | ||
|
|
cf255fc78e | ||
|
|
258de7a6fd | ||
|
|
fcfee31972 | ||
|
|
11b59b0a65 | ||
|
|
0bb2bc651b | ||
|
|
c63788b686 | ||
|
|
60e86f563c | ||
|
|
ce39e97b86 | ||
|
|
06aa378dfd | ||
|
|
43ebb93ff6 | ||
|
|
4fe2ed3df0 | ||
|
|
1a299d10ed | ||
|
|
d1dc6293e8 | ||
|
|
9f1ea57d3c | ||
|
|
917b0512bf | ||
|
|
b9b5b90330 | ||
|
|
216758dbc7 | ||
|
|
96dc085b35 | ||
|
|
b28a3b6a5a | ||
|
|
500ee54f04 | ||
|
|
dd95b199b7 | ||
|
|
3d9ac5a85e | ||
|
|
8503678fc7 | ||
|
|
37ce5e447f | ||
|
|
44d7903a4b | ||
|
|
a3c0cee2cc | ||
|
|
4cc9b1be2b | ||
|
|
f1fba1ed14 | ||
|
|
3bb42522b8 | ||
|
|
143cd8f856 | ||
|
|
4547f3c2b9 | ||
|
|
c3ff5d954a | ||
|
|
43ddee5d30 | ||
|
|
813a1984f9 | ||
|
|
9a5f5f9f8f | ||
|
|
cb55039ec9 | ||
|
|
a51a9700cf | ||
|
|
59d40c8b4e | ||
|
|
7adcba5b7f | ||
|
|
fe6de396f2 | ||
|
|
47af7a9023 | ||
|
|
b1f5367822 | ||
|
|
cd40d150c1 | ||
|
|
98160512b8 | ||
|
|
9a235f2256 | ||
|
|
754e7845f3 | ||
|
|
cbd6b67456 | ||
|
|
1f0e73906e | ||
|
|
f4d1c924d7 | ||
|
|
1a98312595 | ||
|
|
3bc4a6c9dc | ||
|
|
8b96b75960 | ||
|
|
d69dbd2793 | ||
|
|
cd5eb89ebc | ||
|
|
20c683fb4f | ||
|
|
c427e24360 | ||
|
|
feced5505a | ||
|
|
806eebd269 | ||
|
|
b394e15998 | ||
|
|
13d446e27e | ||
|
|
85d62f7e5e | ||
|
|
7515c180b9 | ||
|
|
36d44925cb | ||
|
|
c0f3f97811 | ||
|
|
4e72026ff9 | ||
|
|
74df2d22da | ||
|
|
68d053336b | ||
|
|
876b66b295 | ||
|
|
5eb6ba6e5c | ||
|
|
1dc14ed9f1 | ||
|
|
72bb6c4c99 | ||
|
|
a960eb471b | ||
|
|
e76d89f0db | ||
|
|
d2fdd3c753 | ||
|
|
30de1cdf5d | ||
|
|
6b151e6be5 | ||
|
|
f5450cdf50 | ||
|
|
4b9de8ac42 | ||
|
|
13a6e027ef | ||
|
|
fc77c3ce48 | ||
|
|
85c8f6c446 | ||
|
|
d349209dc9 | ||
|
|
c8779e2a6b | ||
|
|
b55606c37f | ||
|
|
12fb091bbc | ||
|
|
bc37d7169d | ||
|
|
9ae26f2645 | ||
|
|
6367729f98 | ||
|
|
b8b93af977 | ||
|
|
9f96d0c772 | ||
|
|
d5c940ba4c | ||
|
|
144e716cdf | ||
|
|
686040a316 | ||
|
|
d427072cc9 | ||
|
|
60a446123b | ||
|
|
c009f58980 | ||
|
|
79b10ba9a5 | ||
|
|
d05e0f23ea | ||
|
|
26b6118f50 | ||
|
|
0a90c2a95e | ||
|
|
d77fd5c13e | ||
|
|
4dec79a5fb | ||
|
|
b7e0ed9b87 | ||
|
|
0ab7caedd9 | ||
|
|
3824a591d5 | ||
|
|
3e6123f6f6 | ||
|
|
2763e1502b | ||
|
|
0b90622251 | ||
|
|
9b8895df39 | ||
|
|
f6c1453b5b | ||
|
|
7e9619b41b | ||
|
|
90aeb38427 | ||
|
|
6040187844 | ||
|
|
e72d0ed2c6 | ||
|
|
c3b4e2b237 | ||
|
|
7769764b0b | ||
|
|
b25146265d | ||
|
|
e0df59464e | ||
|
|
a67320e431 | ||
|
|
e41279fe6b | ||
|
|
29eaab59be | ||
|
|
541d53127a | ||
|
|
bdcf754d34 | ||
|
|
a67e85e092 | ||
|
|
24536fa296 | ||
|
|
38ed21edd2 | ||
|
|
5d2f2bdd1d | ||
|
|
6515403ae6 | ||
|
|
2f696b2ce7 | ||
|
|
61d51d971c | ||
|
|
90c7680743 | ||
|
|
b59bb998eb | ||
|
|
9845306b68 | ||
|
|
4eb683ab46 | ||
|
|
9d7feb176a | ||
|
|
eab0bf8f82 | ||
|
|
085a4c421b | ||
|
|
4fc4fb2fb3 | ||
|
|
0e5ed6a30c | ||
|
|
5cc59ae3ac | ||
|
|
161f4cbdff | ||
|
|
5b0f69b411 | ||
|
|
23f3f8d90c | ||
|
|
2ac855488b | ||
|
|
c08ddb61b3 | ||
|
|
fb55f2824e | ||
|
|
1b0c93e5ff | ||
|
|
19c8c36030 | ||
|
|
931118e1d8 | ||
|
|
61f1660b38 | ||
|
|
634cf900e6 | ||
|
|
a148f30070 | ||
|
|
3e0daa62c4 | ||
|
|
aac1bae899 | ||
|
|
f5f2f58664 | ||
|
|
f428a44bfc | ||
|
|
ce412e4404 | ||
|
|
7bd4b4558e | ||
|
|
619fb6633a | ||
|
|
bf64339890 | ||
|
|
d9787b168d | ||
|
|
4a81d9b6f7 | ||
|
|
4370c47f6e | ||
|
|
f69e6289b5 | ||
|
|
bfd0cd7108 | ||
|
|
df534736a1 | ||
|
|
93a97d5d6f | ||
|
|
3275875ae5 | ||
|
|
88f830a9e5 | ||
|
|
c609c4af14 | ||
|
|
94c8339e17 | ||
|
|
b742a776eb | ||
|
|
1899eed839 | ||
|
|
43d1a20d8c | ||
|
|
1692f32ffc | ||
|
|
e07869e71f | ||
|
|
c9dfb215ae | ||
|
|
98896f9a75 | ||
|
|
a14cc8cc4d | ||
|
|
e1940eec77 | ||
|
|
a1dfb42812 | ||
|
|
3b2fad6ea8 | ||
|
|
d9df27d574 | ||
|
|
ae45707c84 | ||
|
|
e22d7b31f9 | ||
|
|
0dcb341059 | ||
|
|
3ecb267594 | ||
|
|
2744b44d93 | ||
|
|
0528ef58b2 | ||
|
|
2a223363b8 | ||
|
|
de9a5eb71e | ||
|
|
2b0afd6acb | ||
|
|
3ab421cbe3 | ||
|
|
54dac39e83 | ||
|
|
2de212710a | ||
|
|
60fbecd4a7 | ||
|
|
82458b5f7e | ||
|
|
d0974cfdd2 | ||
|
|
da16e4ed99 | ||
|
|
55536bba4c | ||
|
|
388222c5c7 | ||
|
|
169c60883b | ||
|
|
285443f10f | ||
|
|
d83e579dfe | ||
|
|
486a07602b | ||
|
|
949ba589c0 | ||
|
|
b6a5d19871 | ||
|
|
ce002ce8c1 | ||
|
|
9c3e366d03 | ||
|
|
bb96e22e64 | ||
|
|
a691f2ebac | ||
|
|
7638822e49 | ||
|
|
a9cf728ee1 | ||
|
|
2c4a135c2b | ||
|
|
d686634c0b | ||
|
|
32b0003a72 | ||
|
|
c234b4c78f | ||
|
|
f2a242b09a | ||
|
|
a1efb28f0b | ||
|
|
d217ab39c2 | ||
|
|
31840328b7 | ||
|
|
54547a11d0 | ||
|
|
f99db8d754 | ||
|
|
1ce916182d | ||
|
|
09ba09f4e3 | ||
|
|
2ac85ac61d | ||
|
|
44e024a94e |
1015
.editorconfig
1015
.editorconfig
File diff suppressed because it is too large
Load Diff
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
patreon: orahosting
|
||||
1
.github/ISSUE_TEMPLATE/crash-report.md
vendored
1
.github/ISSUE_TEMPLATE/crash-report.md
vendored
@@ -16,7 +16,6 @@ assignees: ''
|
||||
- OpenRA Version: ENGINE VERSION
|
||||
- OpenRA Mod: GAME MOD
|
||||
- Source: Official download package OR self-compiled OR third-party package
|
||||
- For self-compiled or third-party packages: Mono version
|
||||
|
||||
|
||||
## Exception log
|
||||
|
||||
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
47
.github/workflows/ci.yml
vendored
47
.github/workflows/ci.yml
vendored
@@ -2,25 +2,30 @@ name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
pull_request:
|
||||
branches: [ bleed, 'prep-*' ]
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- '*.py'
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Linux (.NET 6.0)
|
||||
name: Linux (.NET 8.0)
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
- name: Install .NET 8.0
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Check Code
|
||||
run: |
|
||||
@@ -33,36 +38,18 @@ jobs:
|
||||
make check-scripts
|
||||
make TREAT_WARNINGS_AS_ERRORS=true test
|
||||
|
||||
linux-mono:
|
||||
name: Linux (mono)
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Check Code
|
||||
run: |
|
||||
mono --version
|
||||
make RUNTIME=mono check
|
||||
|
||||
- name: Check Mods
|
||||
run: |
|
||||
# check-scripts does not depend on .net/mono, so is not needed here
|
||||
make RUNTIME=mono TREAT_WARNINGS_AS_ERRORS=true test
|
||||
|
||||
windows:
|
||||
name: Windows (.NET 6.0)
|
||||
runs-on: windows-2019
|
||||
name: Windows (.NET 8.0)
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
- name: Install .NET 8.0
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Check Code
|
||||
shell: powershell
|
||||
@@ -74,7 +61,7 @@ jobs:
|
||||
|
||||
- name: Check Mods
|
||||
run: |
|
||||
choco install lua --version 5.1.5.52
|
||||
choco install lua --version 5.1.5.52 --no-progress
|
||||
$ENV:Path = $ENV:Path + ";C:\Program Files (x86)\Lua\5.1\"
|
||||
$ENV:TREAT_WARNINGS_AS_ERRORS = "true"
|
||||
.\make.ps1 check-scripts
|
||||
|
||||
163
.github/workflows/documentation.yml
vendored
163
.github/workflows/documentation.yml
vendored
@@ -1,6 +1,9 @@
|
||||
name: Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ bleed ]
|
||||
tags: [ 'release-*', 'playtest-*' ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
@@ -12,132 +15,136 @@ permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
wiki:
|
||||
name: Update Wiki
|
||||
prepare:
|
||||
name: Prepare version strings
|
||||
if: github.repository == 'openra/openra'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag }}
|
||||
- name: Prepare environment variables
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||
VERSION_TYPE=`echo "${GITHUB_REF#refs/tags/}" | cut -d"-" -f1`
|
||||
echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
echo "VERSION_TYPE=$VERSION_TYPE" >> $GITHUB_ENV
|
||||
else
|
||||
echo "GIT_TAG=bleed" >> $GITHUB_ENV
|
||||
echo "VERSION_TYPE=bleed" >> $GITHUB_ENV
|
||||
fi
|
||||
else
|
||||
VERSION_TYPE=`echo "${{ github.event.inputs.tag }}" | cut -d"-" -f1`
|
||||
echo "GIT_TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
|
||||
echo "VERSION_TYPE=$VERSION_TYPE" >> $GITHUB_ENV
|
||||
fi
|
||||
outputs:
|
||||
git_tag: ${{ env.GIT_TAG }}
|
||||
version_type: ${{ env.VERSION_TYPE }}
|
||||
|
||||
- name: Install .NET 6
|
||||
uses: actions/setup-dotnet@v3
|
||||
wiki:
|
||||
name: Update Wiki
|
||||
needs: prepare
|
||||
if: github.repository == 'openra/openra' && needs.prepare.outputs.version_type != 'bleed'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Debug output
|
||||
run: |
|
||||
echo ${{ needs.prepare.outputs.git_tag }}
|
||||
echo ${{ needs.prepare.outputs.version_type }}
|
||||
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
ref: ${{ needs.prepare.outputs.git_tag }}
|
||||
|
||||
- name: Install .NET 8
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Prepare Environment
|
||||
run: |
|
||||
make all
|
||||
|
||||
- name: Clone Wiki
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: openra/openra.wiki
|
||||
token: ${{ secrets.DOCS_TOKEN }}
|
||||
path: wiki
|
||||
|
||||
- name: Update Wiki (Playtest)
|
||||
if: startsWith(github.event.inputs.tag, 'playtest-')
|
||||
env:
|
||||
GIT_TAG: ${{ github.event.inputs.tag }}
|
||||
if: startsWith(needs.prepare.outputs.git_tag, 'playtest-')
|
||||
run: |
|
||||
./utility.sh all --settings-docs "${GIT_TAG}" > "wiki/Settings (playtest).md"
|
||||
./utility.sh all --settings-docs "${{ needs.prepare.outputs.git_tag }}" > "wiki/Settings (playtest).md"
|
||||
|
||||
- name: Update Wiki (Release)
|
||||
if: startsWith(github.event.inputs.tag, 'release-')
|
||||
env:
|
||||
GIT_TAG: ${{ github.event.inputs.tag }}
|
||||
if: startsWith(needs.prepare.outputs.git_tag, 'release-')
|
||||
run: |
|
||||
./utility.sh all --settings-docs "${GIT_TAG}" > "wiki/Settings.md"
|
||||
./utility.sh all --settings-docs "${{ needs.prepare.outputs.git_tag }}" > "wiki/Settings.md"
|
||||
|
||||
- name: Push Wiki
|
||||
env:
|
||||
GIT_TAG: ${{ github.event.inputs.tag }}
|
||||
run: |
|
||||
cd wiki
|
||||
git config --local user.email "actions@github.com"
|
||||
git config --local user.name "GitHub Actions"
|
||||
git add --all
|
||||
git commit -m "Update auto-generated documentation for ${GIT_TAG}"
|
||||
git push origin master
|
||||
git status
|
||||
git diff-index --quiet HEAD || \
|
||||
(
|
||||
git add --all && \
|
||||
git commit -m "Update auto-generated documentation for ${{ needs.prepare.outputs.git_tag }}" && \
|
||||
git push origin master
|
||||
)
|
||||
|
||||
docs:
|
||||
name: Update docs.openra.net
|
||||
needs: prepare
|
||||
if: github.repository == 'openra/openra'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag }}
|
||||
- name: Debug output
|
||||
run: |
|
||||
echo ${{ needs.prepare.outputs.git_tag }}
|
||||
echo ${{ needs.prepare.outputs.version_type }}
|
||||
|
||||
- name: Install .NET 6
|
||||
uses: actions/setup-dotnet@v3
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
ref: ${{ needs.prepare.outputs.git_tag }}
|
||||
|
||||
- name: Install .NET 8
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Prepare Environment
|
||||
run: |
|
||||
make all
|
||||
|
||||
- name: Clone docs.openra.net (Playtest)
|
||||
if: startsWith(github.event.inputs.tag, 'playtest-')
|
||||
uses: actions/checkout@v3
|
||||
# version_type is release/playtest/bleed - the name of the target branch.
|
||||
- name: Clone docs.openra.net
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: openra/docs
|
||||
token: ${{ secrets.DOCS_TOKEN }}
|
||||
path: docs
|
||||
ref: playtest
|
||||
ref: ${{ needs.prepare.outputs.version_type }}
|
||||
|
||||
- name: Clone docs.openra.net (Release)
|
||||
if: startsWith(github.event.inputs.tag, 'release-')
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: openra/docs
|
||||
token: ${{ secrets.DOCS_TOKEN }}
|
||||
path: docs
|
||||
ref: release
|
||||
|
||||
- name: Update docs.openra.net (Playtest)
|
||||
if: startsWith(github.event.inputs.tag, 'playtest-')
|
||||
env:
|
||||
GIT_TAG: ${{ github.event.inputs.tag }}
|
||||
- name: Generate docs files
|
||||
run: |
|
||||
./utility.sh all --docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/traits.md"
|
||||
./utility.sh all --weapon-docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/weapons.md"
|
||||
./utility.sh all --sprite-sequence-docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/sprite-sequences.md"
|
||||
./utility.sh all --lua-docs "${GIT_TAG}" > "docs/api/lua.md"
|
||||
./utility.sh all --docs "${{ needs.prepare.outputs.git_tag }}" | python3 ./packaging/format-docs.py > "docs/api/traits.md"
|
||||
./utility.sh all --weapon-docs "${{ needs.prepare.outputs.git_tag }}" | python3 ./packaging/format-docs.py > "docs/api/weapons.md"
|
||||
./utility.sh all --sprite-sequence-docs "${{ needs.prepare.outputs.git_tag }}" | python3 ./packaging/format-docs.py > "docs/api/sprite-sequences.md"
|
||||
./utility.sh all --lua-docs "${{ needs.prepare.outputs.git_tag }}" > "docs/api/lua.md"
|
||||
|
||||
- name: Update docs.openra.net (Release)
|
||||
if: startsWith(github.event.inputs.tag, 'release-')
|
||||
env:
|
||||
GIT_TAG: ${{ github.event.inputs.tag }}
|
||||
run: |
|
||||
./utility.sh all --docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/traits.md"
|
||||
./utility.sh all --weapon-docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/weapons.md"
|
||||
./utility.sh all --sprite-sequence-docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/sprite-sequences.md"
|
||||
./utility.sh all --lua-docs "${GIT_TAG}" > "docs/api/lua.md"
|
||||
|
||||
- name: Commit docs.openra.net
|
||||
env:
|
||||
GIT_TAG: ${{ github.event.inputs.tag }}
|
||||
- name: Update docs.openra.net
|
||||
run: |
|
||||
cd docs
|
||||
git config --local user.email "actions@github.com"
|
||||
git config --local user.name "GitHub Actions"
|
||||
git add api/*.md
|
||||
git commit -m "Update auto-generated documentation for ${GIT_TAG}"
|
||||
|
||||
- name: Push docs.openra.net (Release)
|
||||
if: startsWith(github.event.inputs.tag, 'release-')
|
||||
run: |
|
||||
cd docs
|
||||
git push origin release
|
||||
|
||||
- name: Push docs.openra.net (Playtest)
|
||||
if: startsWith(github.event.inputs.tag, 'playtest-')
|
||||
run: |
|
||||
cd docs
|
||||
git push origin playtest
|
||||
git status
|
||||
git diff-index --quiet HEAD || \
|
||||
(
|
||||
git add api/*.md && \
|
||||
git commit -m "Update auto-generated documentation for ${{ needs.prepare.outputs.git_tag }}" && \
|
||||
git push origin ${{ needs.prepare.outputs.version_type }}
|
||||
)
|
||||
|
||||
85
.github/workflows/itch.yml
vendored
85
.github/workflows/itch.yml
vendored
@@ -15,73 +15,56 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.repository == 'openra/openra'
|
||||
steps:
|
||||
- name: Download Packages
|
||||
- name: Download Butler
|
||||
env:
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
run: |
|
||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}-x64.exe"
|
||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}-x64-winportable.zip" -O "OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip"
|
||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}.dmg"
|
||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Dune-2000-x86_64.AppImage"
|
||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Red-Alert-x86_64.AppImage"
|
||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Tiberian-Dawn-x86_64.AppImage"
|
||||
wget -q "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.event.inputs.tag }}/packaging/.itch.toml"
|
||||
zip -u "OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip" .itch.toml
|
||||
wget -cq -O butler-linux-amd64.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
|
||||
unzip butler-linux-amd64.zip
|
||||
rm butler-linux-amd64.zip
|
||||
chmod +x butler
|
||||
./butler -V
|
||||
./butler login
|
||||
|
||||
- name: Publish Windows Installer
|
||||
uses: josephbmanley/butler-publish-itchio-action@master
|
||||
env:
|
||||
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
CHANNEL: win
|
||||
ITCH_GAME: openra
|
||||
ITCH_USER: openra
|
||||
VERSION: ${{ github.event.inputs.tag }}
|
||||
PACKAGE: OpenRA-${{ github.event.inputs.tag }}-x64.exe
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
run: |
|
||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}-x64.exe"
|
||||
./butler push "OpenRA-${{ github.event.inputs.tag }}-x64.exe" "openra/openra:win" --userversion ${{ github.event.inputs.tag }}
|
||||
|
||||
- name: Publish Windows Itch Bundle
|
||||
uses: josephbmanley/butler-publish-itchio-action@master
|
||||
env:
|
||||
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
CHANNEL: itch
|
||||
ITCH_GAME: openra
|
||||
ITCH_USER: openra
|
||||
VERSION: ${{ github.event.inputs.tag }}
|
||||
PACKAGE: OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
run: |
|
||||
wget -q "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.event.inputs.tag }}/packaging/.itch.toml"
|
||||
zip -u "OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip" .itch.toml
|
||||
./butler push "OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip" "openra/openra:itch" --userversion ${{ github.event.inputs.tag }}
|
||||
|
||||
- name: Publish macOS Package
|
||||
uses: josephbmanley/butler-publish-itchio-action@master
|
||||
env:
|
||||
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
CHANNEL: macos
|
||||
ITCH_GAME: openra
|
||||
ITCH_USER: openra
|
||||
VERSION: ${{ github.event.inputs.tag }}
|
||||
PACKAGE: OpenRA-${{ github.event.inputs.tag }}.dmg
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
run: |
|
||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}.dmg"
|
||||
./butler push "OpenRA-${{ github.event.inputs.tag }}.dmg" "openra/openra:macos" --userversion ${{ github.event.inputs.tag }}
|
||||
|
||||
- name: Publish RA AppImage
|
||||
uses: josephbmanley/butler-publish-itchio-action@master
|
||||
env:
|
||||
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
CHANNEL: linux-ra
|
||||
ITCH_GAME: openra
|
||||
ITCH_USER: openra
|
||||
VERSION: ${{ github.event.inputs.tag }}
|
||||
PACKAGE: OpenRA-Red-Alert-x86_64.AppImage
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
run: |
|
||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Red-Alert-x86_64.AppImage"
|
||||
./butler push "OpenRA-Red-Alert-x86_64.AppImage" "openra/openra:linux-ra" --userversion ${{ github.event.inputs.tag }}
|
||||
|
||||
- name: Publish TD AppImage
|
||||
uses: josephbmanley/butler-publish-itchio-action@master
|
||||
env:
|
||||
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
CHANNEL: linux-cnc
|
||||
ITCH_GAME: openra
|
||||
ITCH_USER: openra
|
||||
VERSION: ${{ github.event.inputs.tag }}
|
||||
PACKAGE: OpenRA-Tiberian-Dawn-x86_64.AppImage
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
run: |
|
||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Tiberian-Dawn-x86_64.AppImage"
|
||||
./butler push "OpenRA-Tiberian-Dawn-x86_64.AppImage" "openra/openra:linux-cnc" --userversion ${{ github.event.inputs.tag }}
|
||||
|
||||
- name: Publish D2k AppImage
|
||||
uses: josephbmanley/butler-publish-itchio-action@master
|
||||
env:
|
||||
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
CHANNEL: linux-d2k
|
||||
ITCH_GAME: openra
|
||||
ITCH_USER: openra
|
||||
VERSION: ${{ github.event.inputs.tag }}
|
||||
PACKAGE: OpenRA-Dune-2000-x86_64.AppImage
|
||||
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||
run: |
|
||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Dune-2000-x86_64.AppImage"
|
||||
./butler push "OpenRA-Dune-2000-x86_64.AppImage" "openra/openra:linux-d2k" --userversion ${{ github.event.inputs.tag }}
|
||||
|
||||
94
.github/workflows/packaging.yml
vendored
94
.github/workflows/packaging.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Prepare Environment
|
||||
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
|
||||
@@ -40,12 +40,12 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
- name: Install .NET 8.0
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Prepare Environment
|
||||
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
|
||||
@@ -53,29 +53,27 @@ jobs:
|
||||
- name: Package AppImages
|
||||
run: |
|
||||
mkdir -p build/linux
|
||||
sudo apt install libfuse2
|
||||
sudo apt-get install -y desktop-file-utils
|
||||
./packaging/linux/buildpackage.sh "${GIT_TAG}" "${PWD}/build/linux"
|
||||
|
||||
- name: Upload Packages
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
file_glob: true
|
||||
file: build/linux/*
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
gh release upload ${{ github.ref_name }} build/linux/*
|
||||
|
||||
macos:
|
||||
name: macOS Disk Image
|
||||
runs-on: macos-11
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
- name: Install .NET 8.0
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Prepare Environment
|
||||
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
|
||||
@@ -92,25 +90,23 @@ jobs:
|
||||
./packaging/macos/buildpackage.sh "${GIT_TAG}" "${PWD}/build/macos"
|
||||
|
||||
- name: Upload Package
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
file_glob: true
|
||||
file: build/macos/*
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
gh release upload ${{ github.ref_name }} build/macos/*
|
||||
|
||||
windows:
|
||||
name: Windows Installers
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
- name: Install .NET 8.0
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Prepare Environment
|
||||
run: |
|
||||
@@ -123,11 +119,37 @@ jobs:
|
||||
mkdir -p build/windows
|
||||
./packaging/windows/buildpackage.sh "${GIT_TAG}" "${PWD}/build/windows"
|
||||
|
||||
- name: Upload Packages
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
- name: Upload Installer
|
||||
id: unsigned-artifact
|
||||
if: github.repository == 'openra/openra'
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
file_glob: true
|
||||
file: build/windows/*
|
||||
path: build/windows/*.exe
|
||||
|
||||
- name: Microsoft Authenticode
|
||||
id: signpath
|
||||
if: github.repository == 'openra/openra'
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||
organization-id: '${{ secrets.SIGNPATH_ORGANISATION_ID }}'
|
||||
project-slug: 'OpenRA'
|
||||
signing-policy-slug: 'release-signing'
|
||||
github-artifact-id: '${{ steps.unsigned-artifact.outputs.artifact-id }}'
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: 'build/windows/signed/'
|
||||
parameters: |
|
||||
version: ${{ toJSON(github.ref_name) }}
|
||||
|
||||
- name: Upload Packages
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
gh release upload "${{ github.ref_name }}" build/windows/*.zip
|
||||
|
||||
if [ "${GITHUB_REPOSITORY}" = "openra/openra" ]; then
|
||||
gh release upload "${{ github.ref_name }}" build/windows/signed/*.exe
|
||||
else
|
||||
gh release upload "${{ github.ref_name }}" build/windows/*.exe
|
||||
fi
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -25,10 +25,6 @@ IP2LOCATION-LITE-DB1.IPV6.BIN.ZIP
|
||||
\#*
|
||||
.*.sw?
|
||||
|
||||
# Monodevelop
|
||||
*.pidb
|
||||
*.userprefs
|
||||
|
||||
# Mac OS X
|
||||
.DS_Store
|
||||
|
||||
|
||||
63
AUTHORS
63
AUTHORS
@@ -1,5 +1,4 @@
|
||||
OpenRA wouldn't be where it is today without the
|
||||
hard work of many contributors.
|
||||
OpenRA wouldn't be where it is today without the hard work of many contributors.
|
||||
|
||||
The OpenRA developers are:
|
||||
* Gustas Kažukauskas (PunkPun)
|
||||
@@ -37,6 +36,7 @@ Also thanks to:
|
||||
* Andreas Beck (baxtor)
|
||||
* Ang Soon Li (asl97)
|
||||
* Arik Lirette (Angusm3)
|
||||
* Ashley Newson
|
||||
* Barnaby Smith (mvi)
|
||||
* Bellator
|
||||
* Bernd Stellwag (burned42)
|
||||
@@ -55,6 +55,7 @@ Also thanks to:
|
||||
* Constantin Helmig (CH4Code)
|
||||
* D2k Sardaukar
|
||||
* D'Arcy Rush (r34ch)
|
||||
* Dan Stoian (stoiandan)
|
||||
* Daniel Derejvanik (Harisson)
|
||||
* Danny Keary (Dan9550)
|
||||
* David Jiménez (Rydra)
|
||||
@@ -98,6 +99,7 @@ Also thanks to:
|
||||
* Kanar
|
||||
* Kenny Hoxworth (hoxworth)
|
||||
* Kevin Azzam (ChaoticMind)
|
||||
* Kevin Streser
|
||||
* Krishnakanth Mallik
|
||||
* Kyle Smith (Smitty)
|
||||
* Kyrre Soerensen (zypres)
|
||||
@@ -141,6 +143,7 @@ Also thanks to:
|
||||
* Riderr3
|
||||
* riiga
|
||||
* Rikhardur Bjarni Einarsson (WolfGaming)
|
||||
* Robert Nordan (robpvn)
|
||||
* Sascha Biedermann (bidifx)
|
||||
* Sean Hunt (coppro)
|
||||
* Shawn Collins (UberWaffe)
|
||||
@@ -150,6 +153,7 @@ Also thanks to:
|
||||
* Teemu Nieminen (Temeez)
|
||||
* Thomas Christlieb (ThomasChr)
|
||||
* Tim Mylemans (gecko)
|
||||
* Tinix
|
||||
* Tirili
|
||||
* Tomas Einarsson (Mesacer)
|
||||
* Tom van Leth (tovl)
|
||||
@@ -161,61 +165,42 @@ Also thanks to:
|
||||
* Wojciech Walaszek (Voidwalker)
|
||||
* Wuschel
|
||||
|
||||
Using GNU FreeFont distributed under the GNU GPL
|
||||
terms.
|
||||
Using GNU FreeFont distributed under the GNU GPL terms.
|
||||
|
||||
Using Simple DirectMedia Layer distributed under
|
||||
the terms of the zlib license.
|
||||
Using Simple DirectMedia Layer distributed under the terms of the zlib license.
|
||||
|
||||
Using FreeType distributed under the terms of the
|
||||
FreeType License.
|
||||
Using FreeType distributed under the terms of the FreeType License.
|
||||
|
||||
Using OpenAL Soft distributed under the GNU LGPL.
|
||||
|
||||
Using SDL2-CS and OpenAL-CS created by Ethan
|
||||
Lee and released under the zlib license.
|
||||
Using SDL2-CS and OpenAL-CS created by Ethan Lee and released under the zlib license.
|
||||
|
||||
Using Eluant created by Chris Howie and released
|
||||
under the MIT license.
|
||||
Using Eluant created by Chris Howie and released under the MIT license.
|
||||
|
||||
Using FuzzyLogicLibrary (fuzzynet) by Dmitry
|
||||
Kaluzhny and released under the GNU GPL terms.
|
||||
Using FuzzyLogicLibrary (fuzzynet) by Dmitry Kaluzhny and released under the GNU GPL terms.
|
||||
|
||||
Using Mono.Nat by Alan McGovern, Ben Motmans,
|
||||
Nicholas Terry distributed under the MIT license.
|
||||
Using Mono.Nat by Alan McGovern, Ben Motmans, Nicholas Terry distributed under the MIT license.
|
||||
|
||||
Using MP3Sharp by Robert Bruke and Zane Wagner
|
||||
licensed under the GNU LGPL Version 3.
|
||||
Using MP3Sharp by Robert Bruke and Zane Wagner licensed under the GNU LGPL Version 3.
|
||||
|
||||
Using TagLib# by Stephen Shaw licensed under the
|
||||
GNU LGPL Version 2.1.
|
||||
Using TagLib# by Stephen Shaw licensed under the GNU LGPL Version 2.1.
|
||||
|
||||
Using NVorbis by Andrew Ward distributed under
|
||||
the MIT license.
|
||||
Using NVorbis by Andrew Ward distributed under the MIT license.
|
||||
|
||||
Using ICSharpCode.SharpZipLib initially by Mike
|
||||
Krueger and distributed under the GNU GPL terms.
|
||||
Using ICSharpCode.SharpZipLib initially by Mike Krueger and distributed under the GNU GPL terms.
|
||||
|
||||
Using rix0rrr.BeaconLib developed by Rico Huijbers
|
||||
distributed under MIT License.
|
||||
Using rix0rrr.BeaconLib developed by Rico Huijbers distributed under MIT License.
|
||||
|
||||
Using DiscordRichPresence developed by Lachee
|
||||
distributed under MIT License.
|
||||
Using DiscordRichPresence developed by Lachee distributed under MIT License.
|
||||
|
||||
Using Json.NET developed by James Newton-King
|
||||
distributed under MIT License.
|
||||
Using Json.NET developed by James Newton-King distributed under MIT License.
|
||||
|
||||
Using ANGLE distributed under the BS3 3-Clause license.
|
||||
|
||||
Using Pfim developed by Nick Babcock
|
||||
distributed under the MIT license.
|
||||
Using Pfim developed by Nick Babcock distributed under the MIT license.
|
||||
|
||||
Using Linguini by the Space Station 14 team
|
||||
licensed under Apache and MIT terms.
|
||||
Using Linguini by the Space Station 14 team licensed under Apache and MIT terms.
|
||||
|
||||
This site or product includes IP2Location LITE data
|
||||
available from http://www.ip2location.com.
|
||||
This site or product includes IP2Location LITE data available from https://www.ip2location.com.
|
||||
|
||||
Finally, special thanks goes to the original teams
|
||||
at Westwood Studios and EA for creating the classic
|
||||
games which OpenRA aims to reimagine.
|
||||
Finally, special thanks goes to the original teams at Westwood Studios and EA for creating the classic games which OpenRA aims to reimagine.
|
||||
|
||||
@@ -70,7 +70,7 @@ members of the project's leadership.
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at [http://contributor-covenant.org/version/1/4][version]
|
||||
available at [https://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
[homepage]: https://contributor-covenant.org
|
||||
[version]: https://contributor-covenant.org/version/1/4/
|
||||
|
||||
@@ -16,7 +16,7 @@ Help us keep OpenRA open and inclusive. Please read and follow our [Code of Cond
|
||||
|
||||
* [Coding standard](https://github.com/OpenRA/OpenRA/wiki/Coding-Standard)
|
||||
* [Branches and Releases](https://github.com/OpenRA/OpenRA/wiki/Branches-and-Releases)
|
||||
* [Licensing](http://www.gnu.org/licenses/quick-guide-gplv3.html)
|
||||
* [Licensing](https://www.gnu.org/licenses/quick-guide-gplv3.html)
|
||||
|
||||
Please `git rebase` to the latest revision of the bleed branch.
|
||||
|
||||
@@ -24,6 +24,6 @@ Don't forget to add yourself to [AUTHORS](https://github.com/OpenRA/OpenRA/blob/
|
||||
|
||||
Please propose a [CHANGELOG](https://github.com/OpenRA/OpenRA/wiki/CHANGELOG) entry in the pull-request comments.
|
||||
|
||||
While your pull-request is in review it will be helpful if you join [IRC](irc://chat.freenode.net/openra) to discuss the changes.
|
||||
While your pull-request is in review it will be helpful if you join [Discord](https://discord.openra.net) or [IRC](ircs://irc.libera.chat:6697/openra) to discuss the changes.
|
||||
|
||||
See also the in-depth guide on [contributing](https://github.com/OpenRA/OpenRA/wiki/Contributing) to the OpenRA project.
|
||||
|
||||
8
COPYING
8
COPYING
@@ -1,7 +1,7 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
@@ -645,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
@@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
<https://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Optimize>true</Optimize>
|
||||
<LangVersion>9</LangVersion>
|
||||
<LangVersion>12</LangVersion>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<EngineRootPath Condition="'$(EngineRootPath)' == ''">..</EngineRootPath>
|
||||
<OutputPath>$(EngineRootPath)/bin</OutputPath>
|
||||
@@ -18,8 +18,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework Condition="'$(MSBuildRuntimeType)'!='Mono'">net6.0</TargetFramework>
|
||||
<TargetFramework Condition="'$(MSBuildRuntimeType)'=='Mono'">netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -33,7 +32,7 @@
|
||||
<Optimize>false</Optimize>
|
||||
<!-- Enable only for Debug builds to improve compile-time performance for Release builds -->
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<!-- Enabling GenerateDocumentationFile is required for IDE0005 (Remove unnecessary import)
|
||||
<!-- Enabling GenerateDocumentationFile is required for IDE0005 (Remove unnecessary import)
|
||||
rule to run in command line builds. https://github.com/dotnet/roslyn/issues/41640
|
||||
Enable only for Debug builds to improve compile-time performance for Release builds -->
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
@@ -51,8 +50,10 @@
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<!-- StyleCop -->
|
||||
<!-- StyleCop/Roslynator -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="All" />
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.13.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.13.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0
|
||||
|
||||
RUN \
|
||||
apt-get update; \
|
||||
apt-get -y upgrade; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
wget \
|
||||
make \
|
||||
python3 \
|
||||
unzip \
|
||||
mono-complete
|
||||
|
||||
|
||||
RUN useradd -d /home/openra -m -s /sbin/nologin openra
|
||||
|
||||
WORKDIR /home/openra
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN chown -R openra:openra .
|
||||
|
||||
USER openra
|
||||
|
||||
RUN make
|
||||
|
||||
EXPOSE 1234
|
||||
|
||||
ENTRYPOINT ["./launch-dedicated.sh"]
|
||||
16
INSTALL.md
16
INSTALL.md
@@ -7,8 +7,8 @@ Windows
|
||||
=======
|
||||
|
||||
Compiling OpenRA requires the following dependencies:
|
||||
* [Windows PowerShell >= 4.0](http://microsoft.com/powershell) (included by default in recent Windows 10 versions)
|
||||
* [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0) (or via Visual Studio)
|
||||
* [Windows PowerShell >= 4.0](https://microsoft.com/powershell) (included by default in recent Windows 10 versions)
|
||||
* [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) (or via Visual Studio)
|
||||
|
||||
To compile OpenRA, open the `OpenRA.sln` solution in the main folder, build it from the command-line with `dotnet` or use the Makefile analogue command `make all` scripted in PowerShell syntax.
|
||||
|
||||
@@ -17,15 +17,13 @@ Run the game with `launch-game.cmd`. It can be handed arguments that specify the
|
||||
Linux
|
||||
=====
|
||||
|
||||
.NET 6 or Mono (version 6.12 or later) is required to compile OpenRA. We recommend using .NET 6 when possible, as Mono is poorly packaged by most Linux distributions (e.g. missing the required `msbuild` toolchain), and has been deprecated as a standalone project.
|
||||
.NET 8 is required to compile OpenRA. The [.NET 8 download page](https://dotnet.microsoft.com/download/dotnet/8.0) provides repositories for various package managers and binary releases for several architectures.
|
||||
|
||||
The [.NET 6 download page](https://dotnet.microsoft.com/download/dotnet/6.0) provides repositories for various package managers and binary releases for several architectures. If you prefer to use Mono, we suggest adding the [upstream repository](https://www.mono-project.com/download/stable/#download-lin) for your distro to obtain the latest version and the `msbuild` toolchain.
|
||||
|
||||
To compile OpenRA, run `make` from the command line (or `make RUNTIME=mono` if using Mono). After this one can run the game with `./launch-game.sh`. It is also possible to specify the mod you wish to run from the command line, e.g. with `./launch-game.sh Game.Mod=ts` if you wish to try the experimental Tiberian Sun mod.
|
||||
To compile OpenRA, run `make` from the command line. After this one can run the game with `./launch-game.sh`. It is also possible to specify the mod you wish to run from the command line, e.g. with `./launch-game.sh Game.Mod=ts` if you wish to try the experimental Tiberian Sun mod.
|
||||
|
||||
The default behaviour on the x86_64 architecture is to download several pre-compiled native libraries using the Nuget packaging manager. If you prefer to use system libraries, compile instead using `make TARGETPLATFORM=unix-generic`.
|
||||
|
||||
If you choose to use system libraries, or your system is not x86_64, you will need to install [SDL 2](https://www.libsdl.org/download-2.0.php), [FreeType](http://gnuwin32.sourceforge.net/packages/freetype.htm), [OpenAL](https://openal-soft.org/), and [liblua 5.1](http://luabinaries.sourceforge.net/download.html) before compiling OpenRA.
|
||||
If you choose to use system libraries, or your system is not x86_64, you will need to install [SDL 2](https://www.libsdl.org/download-2.0.php), [FreeType](https://gnuwin32.sourceforge.net/packages/freetype.htm), [OpenAL](https://openal-soft.org/), and [liblua 5.1](https://luabinaries.sourceforge.net/download.html) before compiling OpenRA.
|
||||
|
||||
These can be installed using your package manager on various distros:
|
||||
|
||||
@@ -78,6 +76,6 @@ Type `sudo make install` for system-wide installation. Run `sudo make install-li
|
||||
macOS
|
||||
=====
|
||||
|
||||
[.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0) or [Mono](https://www.mono-project.com/download/stable/#download-mac) (version 6.12 or later) is required to compile OpenRA. We recommend using .NET 6 unless you are running a very old version of macOS (10.9 through 10.14).
|
||||
[.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) is required to compile OpenRA.
|
||||
|
||||
To compile OpenRA, run `make` from the command line (or `make RUNTIME=mono` if using Mono). Run with `./launch-game.sh`.
|
||||
To compile OpenRA, run `make` from the command line. Run with `./launch-game.sh`.
|
||||
|
||||
47
Makefile
47
Makefile
@@ -3,20 +3,17 @@
|
||||
# to compile, run:
|
||||
# make
|
||||
#
|
||||
# to compile using Mono (version 6.12 or greater) instead of .NET 6, run:
|
||||
# make RUNTIME=mono
|
||||
#
|
||||
# to compile using system libraries for native dependencies, run:
|
||||
# make [RUNTIME=net6] TARGETPLATFORM=unix-generic
|
||||
# make TARGETPLATFORM=unix-generic
|
||||
#
|
||||
# to check the official mods for erroneous yaml files, run:
|
||||
# make [RUNTIME=net6] test
|
||||
# make test
|
||||
#
|
||||
# to check the engine and official mod dlls for code style violations, run:
|
||||
# make [RUNTIME=net6] check
|
||||
# make check
|
||||
#
|
||||
# to compile and install Red Alert, Tiberian Dawn, and Dune 2000, run:
|
||||
# make [RUNTIME=net6] [prefix=/foo] [bindir=/bar/bin] install
|
||||
# make [prefix=/foo] [bindir=/bar/bin] install
|
||||
#
|
||||
# to compile and install Red Alert, Tiberian Dawn, and Dune 2000
|
||||
# using system libraries for native dependencies, run:
|
||||
@@ -48,15 +45,12 @@ gameinstalldir ?= $(libdir)/openra
|
||||
|
||||
# Toolchain
|
||||
CWD = $(shell pwd)
|
||||
MSBUILD = msbuild -verbosity:m -nologo
|
||||
DOTNET = dotnet
|
||||
MONO = mono
|
||||
RM = rm
|
||||
RM_R = $(RM) -r
|
||||
RM_F = $(RM) -f
|
||||
RM_RF = $(RM) -rf
|
||||
|
||||
RUNTIME ?= net6
|
||||
CONFIGURATION ?= Release
|
||||
DOTNET_RID = $(shell ${DOTNET} --info | grep RID: | cut -w -f3)
|
||||
ARCH_X64 = $(shell echo ${DOTNET_RID} | grep x64)
|
||||
@@ -91,18 +85,12 @@ endif
|
||||
#
|
||||
all:
|
||||
@echo "Compiling in ${CONFIGURATION} mode..."
|
||||
ifeq ($(RUNTIME), mono)
|
||||
@command -v $(firstword $(MSBUILD)) >/dev/null || (echo "OpenRA requires the '$(MSBUILD)' tool provided by Mono >= 6.12."; exit 1)
|
||||
@$(MSBUILD) -t:Build -restore -p:Configuration=${CONFIGURATION} -p:TargetPlatform=$(TARGETPLATFORM)
|
||||
else
|
||||
@$(DOTNET) build -c ${CONFIGURATION} -nologo -p:TargetPlatform=$(TARGETPLATFORM)
|
||||
endif
|
||||
ifeq ($(TARGETPLATFORM), unix-generic)
|
||||
@./configure-system-libraries.sh
|
||||
endif
|
||||
@./fetch-geoip.sh
|
||||
|
||||
# dotnet clean and msbuild -t:Clean leave files that cause problems when switching between mono/dotnet
|
||||
# Deleting the intermediate / output directories ensures the build directory is actually clean
|
||||
clean:
|
||||
@-$(RM_RF) ./bin ./*/obj
|
||||
@@ -111,12 +99,8 @@ clean:
|
||||
check:
|
||||
@echo
|
||||
@echo "Compiling in Debug mode..."
|
||||
ifeq ($(RUNTIME), mono)
|
||||
@$(MSBUILD) -t:clean\;build -restore -p:Configuration=Debug -warnaserror -p:TargetPlatform=$(TARGETPLATFORM)
|
||||
else
|
||||
@$(DOTNET) clean -c Debug --nologo --verbosity minimal
|
||||
@$(DOTNET) build -c Debug -nologo -warnaserror -p:TargetPlatform=$(TARGETPLATFORM)
|
||||
endif
|
||||
ifeq ($(TARGETPLATFORM), unix-generic)
|
||||
@./configure-system-libraries.sh
|
||||
endif
|
||||
@@ -135,15 +119,19 @@ check-scripts:
|
||||
test: all
|
||||
@echo
|
||||
@echo "Testing Tiberian Sun mod MiniYAML..."
|
||||
@./utility.sh ts-content --check-yaml
|
||||
@./utility.sh ts --check-yaml
|
||||
@echo
|
||||
@echo "Testing Dune 2000 mod MiniYAML..."
|
||||
@./utility.sh d2k-content --check-yaml
|
||||
@./utility.sh d2k --check-yaml
|
||||
@echo
|
||||
@echo "Testing Tiberian Dawn mod MiniYAML..."
|
||||
@./utility.sh cnc-content --check-yaml
|
||||
@./utility.sh cnc --check-yaml
|
||||
@echo
|
||||
@echo "Testing Red Alert mod MiniYAML..."
|
||||
@./utility.sh ra-content --check-yaml
|
||||
@./utility.sh ra --check-yaml
|
||||
|
||||
tests:
|
||||
@@ -153,15 +141,15 @@ tests:
|
||||
|
||||
############# LOCAL INSTALLATION AND DOWNSTREAM PACKAGING ##############
|
||||
#
|
||||
version: VERSION mods/ra/mod.yaml mods/cnc/mod.yaml mods/d2k/mod.yaml mods/ts/mod.yaml mods/modcontent/mod.yaml mods/all/mod.yaml
|
||||
version: VERSION mods/*/mod.yaml
|
||||
ifeq ($(VERSION),)
|
||||
$(error Unable to determine new version (requires git or override of variable VERSION))
|
||||
endif
|
||||
@sh -c '. ./packaging/functions.sh; set_engine_version "$(VERSION)" .'
|
||||
@sh -c '. ./packaging/functions.sh; set_mod_version "$(VERSION)" mods/ra/mod.yaml mods/cnc/mod.yaml mods/d2k/mod.yaml mods/ts/mod.yaml mods/modcontent/mod.yaml mods/all/mod.yaml'
|
||||
@sh -c '. ./packaging/functions.sh; set_mod_version "$(VERSION)" mods/*/mod.yaml'
|
||||
|
||||
install:
|
||||
@sh -c '. ./packaging/functions.sh; install_assemblies $(CWD) $(DESTDIR)$(gameinstalldir) $(TARGETPLATFORM) $(RUNTIME) True True True'
|
||||
@sh -c '. ./packaging/functions.sh; install_assemblies $(CWD) $(DESTDIR)$(gameinstalldir) $(TARGETPLATFORM) True True True'
|
||||
@sh -c '. ./packaging/functions.sh; install_data $(CWD) $(DESTDIR)$(gameinstalldir) cnc d2k ra'
|
||||
|
||||
install-linux-shortcuts:
|
||||
@@ -178,24 +166,21 @@ help:
|
||||
@echo 'to compile, run:'
|
||||
@echo ' make'
|
||||
@echo
|
||||
@echo 'to compile using Mono (version 6.12 or greater) instead of .NET 6, run:'
|
||||
@echo ' make RUNTIME=mono'
|
||||
@echo
|
||||
@echo 'to compile using system libraries for native dependencies, run:'
|
||||
@echo ' make [RUNTIME=net6] TARGETPLATFORM=unix-generic'
|
||||
@echo ' make TARGETPLATFORM=unix-generic'
|
||||
@echo
|
||||
@echo 'to check the official mods for erroneous yaml files, run:'
|
||||
@echo ' make [RUNTIME=net6] [TREAT_WARNINGS_AS_ERRORS=false] test'
|
||||
@echo ' make [TREAT_WARNINGS_AS_ERRORS=false] test'
|
||||
@echo
|
||||
@echo 'to check the engine and official mod dlls for code style violations, run:'
|
||||
@echo ' make [RUNTIME=net6] check'
|
||||
@echo ' make check'
|
||||
@echo
|
||||
@echo 'to compile and install Red Alert, Tiberian Dawn, and Dune 2000 run:'
|
||||
@echo ' make [RUNTIME=net6] [prefix=/foo] [TARGETPLATFORM=unix-generic] install'
|
||||
@echo ' make [prefix=/foo] [TARGETPLATFORM=unix-generic] install'
|
||||
@echo
|
||||
@echo 'to compile and install Red Alert, Tiberian Dawn, and Dune 2000'
|
||||
@echo 'using system libraries for native dependencies, run:'
|
||||
@echo ' make [RUNTIME=net6] [prefix=/foo] [bindir=/bar/bin] TARGETPLATFORM=unix-generic install'
|
||||
@echo ' make [prefix=/foo] [bindir=/bar/bin] TARGETPLATFORM=unix-generic install'
|
||||
@echo
|
||||
@echo 'to install FreeDesktop startup scripts, desktop files, icons, and MIME metadata'
|
||||
@echo ' make install-linux-shortcuts'
|
||||
|
||||
@@ -146,18 +146,22 @@ namespace OpenRA.Activities
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Called every tick to run activity logic. Returns false if the activity should
|
||||
/// remain active, or true if it is complete. Cancelled activities must ensure they
|
||||
/// return the actor to a consistent state before returning true.
|
||||
///
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Child activities can be queued using QueueChild, and these will be ticked
|
||||
/// instead of the parent while they are active. Activities that need to run logic
|
||||
/// in parallel with child activities should set ChildHasPriority to false and
|
||||
/// manually call TickChildren.
|
||||
///
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Queuing one or more child activities and returning true is valid, and causes
|
||||
/// the activity to be completed immediately (without ticking again) once the
|
||||
/// children have completed.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public virtual bool Tick(Actor self)
|
||||
{
|
||||
@@ -222,10 +226,11 @@ namespace OpenRA.Activities
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints the activity tree, starting from the top or optionally from a given origin.
|
||||
///
|
||||
/// <para>Prints the activity tree, starting from the top or optionally from a given origin.</para>
|
||||
/// <para>
|
||||
/// Call this method from any place that's called during a tick, such as the Tick() method itself or
|
||||
/// the Before(First|Last)Run() methods. The origin activity will be marked in the output.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="self">The actor performing this activity.</param>
|
||||
/// <param name="origin">Activity from which to start traversing, and which to mark. If null, mark the calling activity, and start traversal from the top.</param>
|
||||
|
||||
@@ -71,7 +71,12 @@ namespace OpenRA
|
||||
public IEffectiveOwner EffectiveOwner { get; }
|
||||
public IOccupySpace OccupiesSpace { get; }
|
||||
public ITargetable[] Targetables { get; }
|
||||
public IEnumerable<ITargetablePositions> EnabledTargetablePositions { get; private set; }
|
||||
public IEnumerable<ITargetablePositions> EnabledTargetablePositions { get; }
|
||||
readonly ICrushable[] crushables;
|
||||
public ICrushable[] Crushables
|
||||
{
|
||||
get => crushables ?? throw new InvalidOperationException($"Crushables for {Info.Name} are not initialized.");
|
||||
}
|
||||
|
||||
public bool IsIdle => CurrentActivity == null;
|
||||
public bool IsDead => Disposed || (health != null && health.IsDead);
|
||||
@@ -84,21 +89,21 @@ namespace OpenRA
|
||||
sealed class ConditionState
|
||||
{
|
||||
/// <summary>Delegates that have registered to be notified when this condition changes.</summary>
|
||||
public readonly List<VariableObserverNotifier> Notifiers = new();
|
||||
public readonly List<VariableObserverNotifier> Notifiers = [];
|
||||
|
||||
/// <summary>Unique integers identifying granted instances of the condition.</summary>
|
||||
public readonly HashSet<int> Tokens = new();
|
||||
public readonly HashSet<int> Tokens = [];
|
||||
}
|
||||
|
||||
readonly Dictionary<string, ConditionState> conditionStates = new();
|
||||
readonly Dictionary<string, ConditionState> conditionStates = [];
|
||||
|
||||
/// <summary>Each granted condition receives a unique token that is used when revoking.</summary>
|
||||
readonly Dictionary<int, string> conditionTokens = new();
|
||||
readonly Dictionary<int, string> conditionTokens = [];
|
||||
|
||||
int nextConditionToken = 1;
|
||||
|
||||
/// <summary>Cache of condition -> enabled state for quick evaluation of token counter conditions.</summary>
|
||||
readonly Dictionary<string, int> conditionCache = new();
|
||||
readonly Dictionary<string, int> conditionCache = [];
|
||||
|
||||
/// <summary>Read-only version of conditionCache that is passed to IConditionConsumers.</summary>
|
||||
readonly IReadOnlyDictionary<string, int> readOnlyConditionCache;
|
||||
@@ -118,6 +123,9 @@ namespace OpenRA
|
||||
readonly IEnumerable<WPos> enabledTargetableWorldPositions;
|
||||
bool created;
|
||||
|
||||
IEnumerable<IRenderable> renderables;
|
||||
WorldRenderer lastWorldRenderer;
|
||||
|
||||
internal Actor(World world, string name, TypeDictionary initDict)
|
||||
{
|
||||
var duplicateInit = initDict.WithInterface<ISingleInstanceInit>().GroupBy(i => i.GetType())
|
||||
@@ -155,6 +163,7 @@ namespace OpenRA
|
||||
var targetablesList = new List<ITargetable>();
|
||||
var targetablePositionsList = new List<ITargetablePositions>();
|
||||
var syncHashesList = new List<SyncHash>();
|
||||
var crushablesList = new List<ICrushable>();
|
||||
|
||||
foreach (var traitInfo in Info.TraitsInConstructOrder())
|
||||
{
|
||||
@@ -181,6 +190,7 @@ namespace OpenRA
|
||||
{ if (trait is ITargetable t) targetablesList.Add(t); }
|
||||
{ if (trait is ITargetablePositions t) targetablePositionsList.Add(t); }
|
||||
{ if (trait is ISync t) syncHashesList.Add(new SyncHash(t)); }
|
||||
{ if (trait is ICrushable t) crushablesList.Add(t); }
|
||||
}
|
||||
|
||||
resolveOrders = resolveOrdersList.ToArray();
|
||||
@@ -195,6 +205,7 @@ namespace OpenRA
|
||||
EnabledTargetablePositions = targetablePositions.Where(Exts.IsTraitEnabled);
|
||||
enabledTargetableWorldPositions = EnabledTargetablePositions.SelectMany(tp => tp.TargetablePositions(this));
|
||||
SyncHashes = syncHashesList.ToArray();
|
||||
crushables = crushablesList.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,11 +290,18 @@ namespace OpenRA
|
||||
|
||||
public IEnumerable<IRenderable> Render(WorldRenderer wr)
|
||||
{
|
||||
if (lastWorldRenderer != wr)
|
||||
{
|
||||
// PERF: Cache the enumerable to reduce allocations.
|
||||
lastWorldRenderer = wr;
|
||||
renderables = Renderables(wr);
|
||||
}
|
||||
|
||||
// PERF: Avoid LINQ.
|
||||
var renderables = Renderables(wr);
|
||||
var modifiedRenderables = renderables;
|
||||
foreach (var modifier in renderModifiers)
|
||||
renderables = modifier.ModifyRender(this, wr, renderables);
|
||||
return renderables;
|
||||
modifiedRenderables = modifier.ModifyRender(this, wr, modifiedRenderables);
|
||||
return modifiedRenderables;
|
||||
}
|
||||
|
||||
IEnumerable<IRenderable> Renderables(WorldRenderer wr)
|
||||
@@ -496,7 +514,7 @@ namespace OpenRA
|
||||
if (!visibilityModifier.IsVisible(this, player))
|
||||
return false;
|
||||
|
||||
return defaultVisibility.IsVisible(this, player);
|
||||
return defaultVisibility?.IsVisible(this, player) ?? true;
|
||||
}
|
||||
|
||||
public BitSet<TargetableType> GetAllTargetTypes()
|
||||
@@ -533,7 +551,7 @@ namespace OpenRA
|
||||
if (EnabledTargetablePositions.Any())
|
||||
return enabledTargetableWorldPositions;
|
||||
|
||||
return new[] { CenterPosition };
|
||||
return [CenterPosition];
|
||||
}
|
||||
|
||||
#region Conditions
|
||||
|
||||
@@ -16,7 +16,8 @@ using OpenRA.Scripting;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public readonly struct CPos : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding, IEquatable<CPos>
|
||||
public readonly struct CPos : IEquatable<CPos>, IScriptBindable,
|
||||
ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding, ILuaToStringBinding
|
||||
{
|
||||
// Coordinates are packed in a 32 bit signed int
|
||||
// X and Y are 12 bits (signed): -2048...2047
|
||||
@@ -77,17 +78,9 @@ namespace OpenRA
|
||||
return new MPos(X, Y);
|
||||
|
||||
// Convert from RectangularIsometric cell (x, y) position to rectangular map position (u, v)
|
||||
// - The staggered rows make this fiddly (hint: draw a diagram!)
|
||||
// (a) Consider the relationships:
|
||||
// - +1x (even -> odd) adds (0, 1) to (u, v)
|
||||
// - +1x (odd -> even) adds (1, 1) to (u, v)
|
||||
// - +1y (even -> odd) adds (-1, 1) to (u, v)
|
||||
// - +1y (odd -> even) adds (0, 1) to (u, v)
|
||||
// (b) Therefore:
|
||||
// - ax + by adds (a - b)/2 to u (only even increments count)
|
||||
// - ax + by adds a + b to v
|
||||
var u = (X - Y) / 2;
|
||||
// The staggered rows make this fiddly.
|
||||
var v = X + Y;
|
||||
var u = (v - (v & 1)) / 2 - Y;
|
||||
return new MPos(u, v);
|
||||
}
|
||||
|
||||
@@ -96,7 +89,8 @@ namespace OpenRA
|
||||
public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right)
|
||||
{
|
||||
if (!left.TryGetClrValue(out CPos a) || !right.TryGetClrValue(out CVec b))
|
||||
throw new LuaException($"Attempted to call CPos.Add(CPos, CVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
|
||||
throw new LuaException("Attempted to call CPos.Add(CPos, CVec) with invalid arguments " +
|
||||
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
|
||||
|
||||
return new LuaCustomClrObject(a + b);
|
||||
}
|
||||
@@ -105,7 +99,8 @@ namespace OpenRA
|
||||
{
|
||||
var rightType = right.WrappedClrType();
|
||||
if (!left.TryGetClrValue(out CPos a))
|
||||
throw new LuaException($"Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments ({left.WrappedClrType().Name}, {rightType.Name})");
|
||||
throw new LuaException("Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments " +
|
||||
$"({left.WrappedClrType().Name}, {rightType.Name})");
|
||||
|
||||
if (rightType == typeof(CPos))
|
||||
{
|
||||
@@ -118,7 +113,8 @@ namespace OpenRA
|
||||
return new LuaCustomClrObject(a - b);
|
||||
}
|
||||
|
||||
throw new LuaException($"Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments ({left.WrappedClrType().Name}, {rightType.Name})");
|
||||
throw new LuaException("Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments " +
|
||||
$"({left.WrappedClrType().Name}, {rightType.Name})");
|
||||
}
|
||||
|
||||
public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right)
|
||||
@@ -145,6 +141,8 @@ namespace OpenRA
|
||||
set => throw new LuaException("CPos is read-only. Use CPos.New to create a new value");
|
||||
}
|
||||
|
||||
public LuaValue ToString(LuaRuntime runtime) => ToString();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ using OpenRA.Scripting;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public readonly struct CVec : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaUnaryMinusBinding, ILuaEqualityBinding, ILuaTableBinding, IEquatable<CVec>
|
||||
public readonly struct CVec : IEquatable<CVec>, IScriptBindable,
|
||||
ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaUnaryMinusBinding,
|
||||
ILuaMultiplicationBinding, ILuaDivisionBinding, ILuaTableBinding, ILuaToStringBinding
|
||||
{
|
||||
public readonly int X, Y;
|
||||
|
||||
@@ -60,23 +62,24 @@ namespace OpenRA
|
||||
public override string ToString() { return X + "," + Y; }
|
||||
|
||||
public static readonly CVec[] Directions =
|
||||
{
|
||||
new CVec(-1, -1),
|
||||
new CVec(-1, 0),
|
||||
new CVec(-1, 1),
|
||||
new CVec(0, -1),
|
||||
new CVec(0, 1),
|
||||
new CVec(1, -1),
|
||||
new CVec(1, 0),
|
||||
new CVec(1, 1),
|
||||
};
|
||||
[
|
||||
new(-1, -1),
|
||||
new(-1, 0),
|
||||
new(-1, 1),
|
||||
new(0, -1),
|
||||
new(0, 1),
|
||||
new(1, -1),
|
||||
new(1, 0),
|
||||
new(1, 1),
|
||||
];
|
||||
|
||||
#region Scripting interface
|
||||
|
||||
public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right)
|
||||
{
|
||||
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b))
|
||||
throw new LuaException($"Attempted to call CVec.Add(CVec, CVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
|
||||
throw new LuaException("Attempted to call CVec.Add(CVec, CVec) with invalid arguments " +
|
||||
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
|
||||
|
||||
return new LuaCustomClrObject(a + b);
|
||||
}
|
||||
@@ -84,16 +87,12 @@ namespace OpenRA
|
||||
public LuaValue Subtract(LuaRuntime runtime, LuaValue left, LuaValue right)
|
||||
{
|
||||
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b))
|
||||
throw new LuaException($"Attempted to call CVec.Subtract(CVec, CVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
|
||||
throw new LuaException("Attempted to call CVec.Subtract(CVec, CVec) with invalid arguments " +
|
||||
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
|
||||
|
||||
return new LuaCustomClrObject(a - b);
|
||||
}
|
||||
|
||||
public LuaValue Minus(LuaRuntime runtime)
|
||||
{
|
||||
return new LuaCustomClrObject(-this);
|
||||
}
|
||||
|
||||
public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right)
|
||||
{
|
||||
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b))
|
||||
@@ -102,6 +101,29 @@ namespace OpenRA
|
||||
return a == b;
|
||||
}
|
||||
|
||||
public LuaValue Minus(LuaRuntime runtime)
|
||||
{
|
||||
return new LuaCustomClrObject(-this);
|
||||
}
|
||||
|
||||
public LuaValue Multiply(LuaRuntime runtime, LuaValue left, LuaValue right)
|
||||
{
|
||||
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out int b))
|
||||
throw new LuaException("Attempted to call CVec.Multiply(CVec, integer) with invalid arguments " +
|
||||
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
|
||||
|
||||
return new LuaCustomClrObject(a * b);
|
||||
}
|
||||
|
||||
public LuaValue Divide(LuaRuntime runtime, LuaValue left, LuaValue right)
|
||||
{
|
||||
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out int b))
|
||||
throw new LuaException("Attempted to call CVec.Multiply(CVec, integer) with invalid arguments " +
|
||||
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
|
||||
|
||||
return new LuaCustomClrObject(a / b);
|
||||
}
|
||||
|
||||
public LuaValue this[LuaRuntime runtime, LuaValue key]
|
||||
{
|
||||
get
|
||||
@@ -110,6 +132,7 @@ namespace OpenRA
|
||||
{
|
||||
case "X": return X;
|
||||
case "Y": return Y;
|
||||
case "Length": return Length;
|
||||
default: throw new LuaException($"CVec does not define a member '{key}'");
|
||||
}
|
||||
}
|
||||
@@ -117,6 +140,8 @@ namespace OpenRA
|
||||
set => throw new LuaException("CVec is read-only. Use CVec.New to create a new value");
|
||||
}
|
||||
|
||||
public LuaValue ToString(LuaRuntime runtime) => ToString();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,10 @@ namespace OpenRA
|
||||
public static class CryptoUtil
|
||||
{
|
||||
// Fixed byte pattern for the OID header
|
||||
static readonly byte[] OIDHeader = { 0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0 };
|
||||
static readonly byte[] OIDHeader = [0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0];
|
||||
|
||||
static readonly char[] HexUpperAlphabet = "0123456789ABCDEF".ToArray();
|
||||
static readonly char[] HexLowerAlphabet = "0123456789abcdef".ToArray();
|
||||
|
||||
public static string PublicKeyFingerprint(RSAParameters parameters)
|
||||
{
|
||||
@@ -53,33 +56,33 @@ namespace OpenRA
|
||||
using (var s = new MemoryStream(data))
|
||||
{
|
||||
// SEQUENCE
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
ReadTLVLength(s);
|
||||
|
||||
// SEQUENCE -> fixed header junk
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
var headerLength = ReadTLVLength(s);
|
||||
s.Position += headerLength;
|
||||
|
||||
// SEQUENCE -> BIT_STRING
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
ReadTLVLength(s);
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
|
||||
// SEQUENCE -> BIT_STRING -> SEQUENCE
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
ReadTLVLength(s);
|
||||
|
||||
// SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (modulus)
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
var modulusLength = ReadTLVLength(s);
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
var modulus = s.ReadBytes(modulusLength - 1);
|
||||
|
||||
// SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (exponent)
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
var exponentLength = ReadTLVLength(s);
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
var exponent = s.ReadBytes(exponentLength - 1);
|
||||
|
||||
return new RSAParameters
|
||||
@@ -158,13 +161,13 @@ namespace OpenRA
|
||||
|
||||
static int ReadTLVLength(Stream s)
|
||||
{
|
||||
var length = s.ReadByte();
|
||||
var length = s.ReadUInt8();
|
||||
if (length < 0x80)
|
||||
return length;
|
||||
|
||||
var data = new byte[4];
|
||||
s.ReadBytes(data, 0, Math.Min(length & 0x7F, 4));
|
||||
return BitConverter.ToInt32(data.ToArray(), 0);
|
||||
Span<byte> data = stackalloc byte[4];
|
||||
s.ReadBytes(data[..Math.Min(length & 0x7F, 4)]);
|
||||
return BitConverter.ToInt32(data);
|
||||
}
|
||||
|
||||
static int TripletFullLength(int dataLength)
|
||||
@@ -207,8 +210,7 @@ namespace OpenRA
|
||||
using (var rsa = new RSACryptoServiceProvider())
|
||||
{
|
||||
rsa.ImportParameters(parameters);
|
||||
using (var csp = SHA1.Create())
|
||||
return Convert.ToBase64String(rsa.SignHash(csp.ComputeHash(data), CryptoConfig.MapNameToOID("SHA1")));
|
||||
return Convert.ToBase64String(rsa.SignHash(SHA1.HashData(data), CryptoConfig.MapNameToOID("SHA1")));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -233,8 +235,7 @@ namespace OpenRA
|
||||
using (var rsa = new RSACryptoServiceProvider())
|
||||
{
|
||||
rsa.ImportParameters(parameters);
|
||||
using (var csp = SHA1.Create())
|
||||
return rsa.VerifyHash(csp.ComputeHash(data), CryptoConfig.MapNameToOID("SHA1"), Convert.FromBase64String(signature));
|
||||
return rsa.VerifyHash(SHA1.HashData(data), CryptoConfig.MapNameToOID("SHA1"), Convert.FromBase64String(signature));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -249,19 +250,42 @@ namespace OpenRA
|
||||
|
||||
public static string SHA1Hash(Stream data)
|
||||
{
|
||||
using (var csp = SHA1.Create())
|
||||
return new string(csp.ComputeHash(data).SelectMany(a => a.ToString("x2")).ToArray());
|
||||
return ToHex(SHA1.HashData(data), true);
|
||||
}
|
||||
|
||||
public static string SHA1Hash(byte[] data)
|
||||
{
|
||||
using (var csp = SHA1.Create())
|
||||
return new string(csp.ComputeHash(data).SelectMany(a => a.ToString("x2")).ToArray());
|
||||
return ToHex(SHA1.HashData(data), true);
|
||||
}
|
||||
|
||||
public static string SHA1Hash(string data)
|
||||
{
|
||||
return SHA1Hash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
public static string ToHex(ReadOnlySpan<byte> source, bool lowerCase = false)
|
||||
{
|
||||
if (source.Length == 0)
|
||||
return string.Empty;
|
||||
|
||||
// excessively avoid stack overflow if source is too large (considering that we're allocating a new string)
|
||||
var buffer = source.Length <= 256 ? stackalloc char[source.Length * 2] : new char[source.Length * 2];
|
||||
return ToHexInternal(source, buffer, lowerCase);
|
||||
}
|
||||
|
||||
static string ToHexInternal(ReadOnlySpan<byte> source, Span<char> buffer, bool lowerCase)
|
||||
{
|
||||
var sourceIndex = 0;
|
||||
var alphabet = lowerCase ? HexLowerAlphabet : HexUpperAlphabet;
|
||||
|
||||
for (var i = 0; i < buffer.Length; i += 2)
|
||||
{
|
||||
var b = source[sourceIndex++];
|
||||
buffer[i] = alphabet[b >> 4];
|
||||
buffer[i + 1] = alphabet[b & 0xF];
|
||||
}
|
||||
|
||||
return new string(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
OpenRA.Game/DescAttribute.cs
Normal file
22
OpenRA.Game/DescAttribute.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
// Mirrors DescriptionAttribute from System.ComponentModel but we don't want to have to use that everywhere.
|
||||
[AttributeUsage(AttributeTargets.All)]
|
||||
public sealed class DescAttribute(params string[] lines) : Attribute
|
||||
{
|
||||
public readonly string[] Lines = lines;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using OpenRA.FileFormats;
|
||||
@@ -27,9 +28,8 @@ namespace OpenRA
|
||||
{
|
||||
public readonly string Id;
|
||||
public readonly string Version;
|
||||
public readonly string Title;
|
||||
public readonly string LaunchPath;
|
||||
public readonly string[] LaunchArgs;
|
||||
public readonly ImmutableArray<string> LaunchArgs;
|
||||
public Sprite Icon { get; internal set; }
|
||||
public Sprite Icon2x { get; internal set; }
|
||||
public Sprite Icon3x { get; internal set; }
|
||||
@@ -41,7 +41,7 @@ namespace OpenRA
|
||||
|
||||
public class ExternalMods : IReadOnlyDictionary<string, ExternalMod>
|
||||
{
|
||||
readonly Dictionary<string, ExternalMod> mods = new();
|
||||
readonly Dictionary<string, ExternalMod> mods = [];
|
||||
readonly SheetBuilder sheetBuilder;
|
||||
|
||||
Sheet CreateSheet()
|
||||
@@ -66,6 +66,7 @@ namespace OpenRA
|
||||
// Several types of support directory types are available, depending on
|
||||
// how the player has installed and launched the game.
|
||||
// Read registration metadata from all of them
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
foreach (var source in GetSupportDirs(ModRegistration.User | ModRegistration.System))
|
||||
{
|
||||
var metadataPath = Path.Combine(source, "ModMetadata");
|
||||
@@ -76,7 +77,7 @@ namespace OpenRA
|
||||
{
|
||||
try
|
||||
{
|
||||
var yaml = MiniYaml.FromStream(File.OpenRead(path), path).First().Value;
|
||||
var yaml = MiniYaml.FromFile(path, stringPool: stringPool).First().Value;
|
||||
LoadMod(yaml, path);
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -94,17 +95,17 @@ namespace OpenRA
|
||||
|
||||
if (sheetBuilder != null)
|
||||
{
|
||||
var iconNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon");
|
||||
var iconNode = yaml.NodeWithKeyOrDefault("Icon");
|
||||
if (iconNode != null && !string.IsNullOrEmpty(iconNode.Value.Value))
|
||||
using (var stream = new MemoryStream(Convert.FromBase64String(iconNode.Value.Value)))
|
||||
mod.Icon = sheetBuilder.Add(new Png(stream));
|
||||
|
||||
var icon2xNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon2x");
|
||||
var icon2xNode = yaml.NodeWithKeyOrDefault("Icon2x");
|
||||
if (icon2xNode != null && !string.IsNullOrEmpty(icon2xNode.Value.Value))
|
||||
using (var stream = new MemoryStream(Convert.FromBase64String(icon2xNode.Value.Value)))
|
||||
mod.Icon2x = sheetBuilder.Add(new Png(stream), 1f / 2);
|
||||
|
||||
var icon3xNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon3x");
|
||||
var icon3xNode = yaml.NodeWithKeyOrDefault("Icon3x");
|
||||
if (icon3xNode != null && !string.IsNullOrEmpty(icon3xNode.Value.Value))
|
||||
using (var stream = new MemoryStream(Convert.FromBase64String(icon3xNode.Value.Value)))
|
||||
mod.Icon3x = sheetBuilder.Add(new Png(stream), 1f / 3);
|
||||
@@ -122,26 +123,29 @@ namespace OpenRA
|
||||
return;
|
||||
|
||||
var key = ExternalMod.MakeKey(mod);
|
||||
var yaml = new MiniYamlNode("Registration", new MiniYaml("", new List<MiniYamlNode>()
|
||||
{
|
||||
var yaml = new MiniYamlNode("Registration", new MiniYaml("",
|
||||
[
|
||||
new MiniYamlNode("Id", mod.Id),
|
||||
new MiniYamlNode("Version", mod.Metadata.Version),
|
||||
new MiniYamlNode("Title", mod.Metadata.Title),
|
||||
new MiniYamlNode("LaunchPath", launchPath),
|
||||
new MiniYamlNode("LaunchArgs", new[] { "Game.Mod=" + mod.Id }.Concat(launchArgs).JoinWith(", "))
|
||||
}));
|
||||
]));
|
||||
|
||||
var iconNodes = new List<MiniYamlNode>();
|
||||
|
||||
using (var stream = mod.Package.GetStream("icon.png"))
|
||||
if (stream != null)
|
||||
yaml.Value.Nodes.Add(new MiniYamlNode("Icon", Convert.ToBase64String(stream.ReadAllBytes())));
|
||||
iconNodes.Add(new MiniYamlNode("Icon", Convert.ToBase64String(stream.ReadAllBytes())));
|
||||
|
||||
using (var stream = mod.Package.GetStream("icon-2x.png"))
|
||||
if (stream != null)
|
||||
yaml.Value.Nodes.Add(new MiniYamlNode("Icon2x", Convert.ToBase64String(stream.ReadAllBytes())));
|
||||
iconNodes.Add(new MiniYamlNode("Icon2x", Convert.ToBase64String(stream.ReadAllBytes())));
|
||||
|
||||
using (var stream = mod.Package.GetStream("icon-3x.png"))
|
||||
if (stream != null)
|
||||
yaml.Value.Nodes.Add(new MiniYamlNode("Icon3x", Convert.ToBase64String(stream.ReadAllBytes())));
|
||||
iconNodes.Add(new MiniYamlNode("Icon3x", Convert.ToBase64String(stream.ReadAllBytes())));
|
||||
|
||||
yaml = yaml.WithValue(yaml.Value.WithNodesAppended(iconNodes));
|
||||
|
||||
var sources = new HashSet<string>();
|
||||
if (registration.HasFlag(ModRegistration.System))
|
||||
@@ -201,7 +205,7 @@ namespace OpenRA
|
||||
string modKey = null;
|
||||
try
|
||||
{
|
||||
var yaml = MiniYaml.FromStream(File.OpenRead(path), path).First().Value;
|
||||
var yaml = MiniYaml.FromFile(path).First().Value;
|
||||
var m = FieldLoader.Load<ExternalMod>(yaml);
|
||||
modKey = ExternalMod.MakeKey(m);
|
||||
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Support;
|
||||
using OpenRA.Traits;
|
||||
@@ -22,15 +25,24 @@ namespace OpenRA
|
||||
{
|
||||
public static class Exts
|
||||
{
|
||||
public static bool IsUppercase(this string str)
|
||||
/// <summary>Returns <see cref="Color"/> of the <paramref name="actor"/>, taking <see cref="Actor.EffectiveOwner"/> into account.</summary>
|
||||
public static Color OwnerColor(this Actor actor)
|
||||
{
|
||||
return string.Compare(str.ToUpperInvariant(), str, false) == 0;
|
||||
var effectiveOwner = actor.EffectiveOwner;
|
||||
if (effectiveOwner != null && effectiveOwner.Disguised && actor.World.RenderPlayer != null)
|
||||
return effectiveOwner.Owner.Color;
|
||||
|
||||
return actor.Owner.Color;
|
||||
}
|
||||
|
||||
public static T WithDefault<T>(T def, Func<T> f)
|
||||
public static string FormatInvariant(this string format, params object[] args)
|
||||
{
|
||||
try { return f(); }
|
||||
catch { return def; }
|
||||
return string.Format(CultureInfo.InvariantCulture, format, args);
|
||||
}
|
||||
|
||||
public static string FormatCurrent(this string format, params object[] args)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, format, args);
|
||||
}
|
||||
|
||||
public static Lazy<T> Lazy<T>(Func<T> p) { return new Lazy<T>(p); }
|
||||
@@ -67,7 +79,7 @@ namespace OpenRA
|
||||
return Math.Sign((v1.X - v0.X) * (p.Y - v0.Y) - (p.X - v0.X) * (v1.Y - v0.Y));
|
||||
}
|
||||
|
||||
public static bool PolygonContains(this int2[] polygon, int2 p)
|
||||
public static bool PolygonContains(this ImmutableArray<int2> polygon, int2 p)
|
||||
{
|
||||
var windingNumber = 0;
|
||||
|
||||
@@ -109,17 +121,11 @@ namespace OpenRA
|
||||
|
||||
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, V v)
|
||||
{
|
||||
#if NET5_0_OR_GREATER
|
||||
// SAFETY: Dictionary cannot be modified whilst the ref is alive.
|
||||
ref var value = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(d, k, out var exists);
|
||||
ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(d, k, out var exists);
|
||||
if (!exists)
|
||||
value = v;
|
||||
return value;
|
||||
#else
|
||||
if (!d.TryGetValue(k, out var ret))
|
||||
d.Add(k, ret = v);
|
||||
return ret;
|
||||
#endif
|
||||
}
|
||||
|
||||
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, Func<K, V> createFn)
|
||||
@@ -131,11 +137,35 @@ namespace OpenRA
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static T GetOrAdd<T>(this HashSet<T> set, T value)
|
||||
{
|
||||
if (!set.TryGetValue(value, out var ret))
|
||||
set.Add(ret = value);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static T GetOrAdd<T>(this HashSet<T> set, T value, Func<T, T> createFn)
|
||||
{
|
||||
if (!set.TryGetValue(value, out var ret))
|
||||
set.Add(ret = createFn(value));
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static int IndexOf<T>(this T[] array, T value)
|
||||
{
|
||||
return Array.IndexOf(array, value);
|
||||
}
|
||||
|
||||
public static T FirstOrDefault<T>(this T[] array, Predicate<T> match)
|
||||
{
|
||||
return Array.Find(array, match);
|
||||
}
|
||||
|
||||
public static T FirstOrDefault<T>(this List<T> list, Predicate<T> match)
|
||||
{
|
||||
return list.Find(match);
|
||||
}
|
||||
|
||||
public static T Random<T>(this IEnumerable<T> ts, MersenneTwister r)
|
||||
{
|
||||
return Random(ts, r, true);
|
||||
@@ -148,7 +178,7 @@ namespace OpenRA
|
||||
|
||||
static T Random<T>(IEnumerable<T> ts, MersenneTwister r, bool throws)
|
||||
{
|
||||
var xs = ts as ICollection<T>;
|
||||
var xs = ts as IReadOnlyCollection<T>;
|
||||
xs ??= ts.ToList();
|
||||
if (xs.Count == 0)
|
||||
{
|
||||
@@ -303,9 +333,9 @@ namespace OpenRA
|
||||
|
||||
// Adjust for other rounding modes
|
||||
if (round == ISqrtRoundMode.Nearest && remainder > root)
|
||||
root += 1;
|
||||
root++;
|
||||
else if (round == ISqrtRoundMode.Ceiling && root * root < number)
|
||||
root += 1;
|
||||
root++;
|
||||
|
||||
return root;
|
||||
}
|
||||
@@ -344,9 +374,9 @@ namespace OpenRA
|
||||
|
||||
// Adjust for other rounding modes
|
||||
if (round == ISqrtRoundMode.Nearest && remainder > root)
|
||||
root += 1;
|
||||
root++;
|
||||
else if (round == ISqrtRoundMode.Ceiling && root * root < number)
|
||||
root += 1;
|
||||
root++;
|
||||
|
||||
return root;
|
||||
}
|
||||
@@ -356,6 +386,11 @@ namespace OpenRA
|
||||
return number * 46341 / 32768;
|
||||
}
|
||||
|
||||
public static int MultiplyBySqrtTwoOverTwo(int number)
|
||||
{
|
||||
return (int)(number * 23170L / 32768L);
|
||||
}
|
||||
|
||||
public static int IntegerDivisionRoundingAwayFromZero(int dividend, int divisor)
|
||||
{
|
||||
var quotient = Math.DivRem(dividend, divisor, out var remainder);
|
||||
@@ -379,11 +414,6 @@ namespace OpenRA
|
||||
return ts.Except(exclusions);
|
||||
}
|
||||
|
||||
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source)
|
||||
{
|
||||
return new HashSet<T>(source);
|
||||
}
|
||||
|
||||
public static Dictionary<TKey, TSource> ToDictionaryWithConflictLog<TSource, TKey>(
|
||||
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
|
||||
string debugName, Func<TKey, string> logKey, Func<TSource, string> logValue)
|
||||
@@ -394,15 +424,26 @@ namespace OpenRA
|
||||
public static Dictionary<TKey, TElement> ToDictionaryWithConflictLog<TSource, TKey, TElement>(
|
||||
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector,
|
||||
string debugName, Func<TKey, string> logKey = null, Func<TElement, string> logValue = null)
|
||||
{
|
||||
var output = new Dictionary<TKey, TElement>();
|
||||
IntoDictionaryWithConflictLog(source, keySelector, elementSelector, debugName, output, logKey, logValue);
|
||||
return output;
|
||||
}
|
||||
|
||||
public static void IntoDictionaryWithConflictLog<TSource, TKey, TElement>(
|
||||
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector,
|
||||
string debugName, Dictionary<TKey, TElement> output,
|
||||
Func<TKey, string> logKey = null, Func<TElement, string> logValue = null)
|
||||
{
|
||||
// Fall back on ToString() if null functions are provided:
|
||||
logKey ??= s => s.ToString();
|
||||
logValue ??= s => s.ToString();
|
||||
|
||||
// Try to build a dictionary and log all duplicates found (if any):
|
||||
var dupKeys = new Dictionary<TKey, List<string>>();
|
||||
Dictionary<TKey, List<string>> dupKeys = null;
|
||||
var capacity = source is ICollection<TSource> collection ? collection.Count : 0;
|
||||
var d = new Dictionary<TKey, TElement>(capacity);
|
||||
output.Clear();
|
||||
output.EnsureCapacity(capacity);
|
||||
foreach (var item in source)
|
||||
{
|
||||
var key = keySelector(item);
|
||||
@@ -413,15 +454,16 @@ namespace OpenRA
|
||||
continue;
|
||||
|
||||
// Check for a key conflict:
|
||||
if (!d.TryAdd(key, element))
|
||||
if (!output.TryAdd(key, element))
|
||||
{
|
||||
dupKeys ??= [];
|
||||
if (!dupKeys.TryGetValue(key, out var dupKeyMessages))
|
||||
{
|
||||
// Log the initial conflicting value already inserted:
|
||||
dupKeyMessages = new List<string>
|
||||
{
|
||||
logValue(d[key])
|
||||
};
|
||||
dupKeyMessages =
|
||||
[
|
||||
logValue(output[key])
|
||||
];
|
||||
dupKeys.Add(key, dupKeyMessages);
|
||||
}
|
||||
|
||||
@@ -431,15 +473,14 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
// If any duplicates were found, throw a descriptive error
|
||||
if (dupKeys.Count > 0)
|
||||
if (dupKeys != null)
|
||||
{
|
||||
var badKeysFormatted = string.Join(", ", dupKeys.Select(p => $"{logKey(p.Key)}: [{string.Join(",", p.Value)}]"));
|
||||
var msg = $"{debugName}, duplicate values found for the following keys: {badKeysFormatted}";
|
||||
throw new ArgumentException(msg);
|
||||
var badKeysFormatted = new StringBuilder(
|
||||
$"{debugName}, duplicate values found for the following keys: ");
|
||||
foreach (var p in dupKeys)
|
||||
badKeysFormatted.Append(CultureInfo.InvariantCulture, $"{logKey(p.Key)}: [{string.Join(",", p.Value)}]");
|
||||
throw new ArgumentException(badKeysFormatted.ToString());
|
||||
}
|
||||
|
||||
// Return the dictionary we built:
|
||||
return d;
|
||||
}
|
||||
|
||||
public static Color ColorLerp(float t, Color c1, Color c2)
|
||||
@@ -460,50 +501,43 @@ namespace OpenRA
|
||||
return result;
|
||||
}
|
||||
|
||||
public static T[,] ResizeArray<T>(T[,] ts, T t, int width, int height)
|
||||
{
|
||||
var result = new T[width, height];
|
||||
for (var i = 0; i < width; i++)
|
||||
{
|
||||
for (var j = 0; j < height; j++)
|
||||
{
|
||||
// Workaround for broken ternary operators in certain versions of mono
|
||||
// (3.10 and certain versions of the 3.8 series): https://bugzilla.xamarin.com/show_bug.cgi?id=23319
|
||||
if (i <= ts.GetUpperBound(0) && j <= ts.GetUpperBound(1))
|
||||
result[i, j] = ts[i, j];
|
||||
else
|
||||
result[i, j] = t;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int ToBits(this IEnumerable<bool> bits)
|
||||
{
|
||||
var i = 0;
|
||||
var result = 0;
|
||||
foreach (var b in bits)
|
||||
if (b)
|
||||
result |= 1 << i++;
|
||||
else
|
||||
i++;
|
||||
if (i > 33)
|
||||
throw new InvalidOperationException("ToBits only accepts up to 32 values.");
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int ParseIntegerInvariant(string s)
|
||||
{
|
||||
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static byte ParseByte(string s)
|
||||
public static byte ParseByteInvariant(string s)
|
||||
{
|
||||
return byte.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static bool TryParseIntegerInvariant(string s, out int i)
|
||||
public static ushort ParseUshortInvariant(string s)
|
||||
{
|
||||
return ushort.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static short ParseInt16Invariant(string s)
|
||||
{
|
||||
return short.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static int ParseInt32Invariant(string s)
|
||||
{
|
||||
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static float ParseFloatOrPercentInvariant(string s)
|
||||
{
|
||||
var f = float.Parse(s.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo);
|
||||
return f * (s.Contains('%') ? 0.01f : 1f);
|
||||
}
|
||||
|
||||
public static bool TryParseByteInvariant(string s, out byte i)
|
||||
{
|
||||
return byte.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
||||
}
|
||||
|
||||
public static bool TryParseUshortInvariant(string s, out ushort i)
|
||||
{
|
||||
return ushort.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
||||
}
|
||||
|
||||
public static bool TryParseInt32Invariant(string s, out int i)
|
||||
{
|
||||
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
||||
}
|
||||
@@ -513,6 +547,47 @@ namespace OpenRA
|
||||
return long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
||||
}
|
||||
|
||||
public static bool TryParseFloatOrPercentInvariant(string s, out float f)
|
||||
{
|
||||
if (float.TryParse(s?.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out f))
|
||||
{
|
||||
f *= s.Contains('%') ? 0.01f : 1f;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string ToStringInvariant(this byte i)
|
||||
{
|
||||
return i.ToString(NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static string ToStringInvariant(this byte i, string format)
|
||||
{
|
||||
return i.ToString(format, NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static string ToStringInvariant(this int i)
|
||||
{
|
||||
return i.ToString(NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static string ToStringInvariant(this uint i)
|
||||
{
|
||||
return i.ToString(NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static string ToStringInvariant(this float f)
|
||||
{
|
||||
return f.ToString(NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static string ToStringInvariant(this int i, string format)
|
||||
{
|
||||
return i.ToString(format, NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static bool IsTraitEnabled<T>(this T trait)
|
||||
{
|
||||
return trait is not IDisabledTrait disabledTrait || !disabledTrait.IsTraitDisabled;
|
||||
@@ -576,7 +651,7 @@ namespace OpenRA
|
||||
Current = default;
|
||||
}
|
||||
|
||||
public LineSplitEnumerator GetEnumerator() => this;
|
||||
public readonly LineSplitEnumerator GetEnumerator() => this;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
@@ -590,7 +665,7 @@ namespace OpenRA
|
||||
if (index == -1)
|
||||
{
|
||||
// The remaining string is an empty string
|
||||
str = ReadOnlySpan<char>.Empty;
|
||||
str = [];
|
||||
Current = span;
|
||||
return true;
|
||||
}
|
||||
@@ -602,27 +677,4 @@ namespace OpenRA
|
||||
|
||||
public ReadOnlySpan<char> Current { get; private set; }
|
||||
}
|
||||
|
||||
public static class Enum<T>
|
||||
{
|
||||
public static T Parse(string s) { return (T)Enum.Parse(typeof(T), s); }
|
||||
public static T[] GetValues() { return (T[])Enum.GetValues(typeof(T)); }
|
||||
|
||||
public static bool TryParse(string s, bool ignoreCase, out T value)
|
||||
{
|
||||
// The string may be a comma delimited list of values
|
||||
var names = ignoreCase ? Enum.GetNames(typeof(T)).Select(x => x.ToLowerInvariant()) : Enum.GetNames(typeof(T));
|
||||
var values = ignoreCase ? s.Split(',').Select(x => x.Trim().ToLowerInvariant()) : s.Split(',').Select(x => x.Trim());
|
||||
|
||||
if (values.Any(x => !names.Contains(x)))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = (T)Enum.Parse(typeof(T), s, ignoreCase);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,55 +10,57 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public static class FieldSaver
|
||||
{
|
||||
public static MiniYaml Save(object o, bool includePrivateByDefault = false)
|
||||
public static MiniYaml Save(object o)
|
||||
{
|
||||
var nodes = new List<MiniYamlNode>();
|
||||
string root = null;
|
||||
|
||||
foreach (var info in FieldLoader.GetTypeLoadInfo(o.GetType(), includePrivateByDefault))
|
||||
foreach (var fieldInfo in FieldLoader.GetTypeLoadInfo(o.GetType()))
|
||||
{
|
||||
if (info.Attribute.DictionaryFromYamlKey)
|
||||
if (fieldInfo.Field.FieldType.IsGenericType && fieldInfo.Field.FieldType.IsAssignableTo(typeof(System.Collections.IDictionary)))
|
||||
{
|
||||
var dict = (System.Collections.IDictionary)info.Field.GetValue(o);
|
||||
var dict = (System.Collections.IDictionary)fieldInfo.Field.GetValue(o);
|
||||
var dictNodes = new List<MiniYamlNode>();
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
var key = ((System.Collections.DictionaryEntry)kvp).Key;
|
||||
var value = ((System.Collections.DictionaryEntry)kvp).Value;
|
||||
|
||||
nodes.Add(new MiniYamlNode(FormatValue(key), FormatValue(value)));
|
||||
dictNodes.Add(new MiniYamlNode(FormatValue(key), FormatValue(value)));
|
||||
}
|
||||
|
||||
nodes.Add(new MiniYamlNode(fieldInfo.YamlName, "", dictNodes));
|
||||
}
|
||||
else if (info.Attribute.FromYamlKey)
|
||||
root = FormatValue(o, info.Field);
|
||||
else
|
||||
nodes.Add(new MiniYamlNode(info.YamlName, FormatValue(o, info.Field)));
|
||||
nodes.Add(new MiniYamlNode(fieldInfo.YamlName, FormatValue(o, fieldInfo.Field)));
|
||||
}
|
||||
|
||||
return new MiniYaml(root, nodes);
|
||||
return new MiniYaml(null, nodes);
|
||||
}
|
||||
|
||||
public static MiniYaml SaveDifferences(object o, object from, bool includePrivateByDefault = false)
|
||||
public static MiniYaml SaveDifferences(object o, object from)
|
||||
{
|
||||
if (o.GetType() != from.GetType())
|
||||
throw new InvalidOperationException("FieldLoader: can't diff objects of different types");
|
||||
throw new InvalidOperationException("FieldSaver: can't diff objects of different types");
|
||||
|
||||
var fields = FieldLoader.GetTypeLoadInfo(o.GetType(), includePrivateByDefault)
|
||||
var fields = FieldLoader.GetTypeLoadInfo(o.GetType())
|
||||
.Where(info => FormatValue(o, info.Field) != FormatValue(from, info.Field));
|
||||
|
||||
return new MiniYaml(
|
||||
null,
|
||||
fields.Select(info => new MiniYamlNode(info.YamlName, FormatValue(o, info.Field))).ToList());
|
||||
fields.Select(info => new MiniYamlNode(info.YamlName, FormatValue(o, info.Field))));
|
||||
}
|
||||
|
||||
public static MiniYamlNode SaveField(object o, string field)
|
||||
@@ -78,13 +80,37 @@ namespace OpenRA
|
||||
if (t.IsArray && t.GetArrayRank() == 1)
|
||||
return ((Array)v).Cast<object>().Select(FormatValue).JoinWith(", ");
|
||||
|
||||
if (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(HashSet<>) || t.GetGenericTypeDefinition() == typeof(List<>)))
|
||||
if (t.IsGenericType &&
|
||||
t.GetGenericTypeDefinition() == typeof(ImmutableArray<>))
|
||||
{
|
||||
try
|
||||
{
|
||||
return ((System.Collections.IEnumerable)v).Cast<object>().Select(FormatValue).JoinWith(", ");
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
if (t.IsGenericType &&
|
||||
(t.GetGenericTypeDefinition() == typeof(List<>) ||
|
||||
t.GetGenericTypeDefinition() == typeof(HashSet<>) ||
|
||||
t.GetGenericTypeDefinition()
|
||||
.BaseTypes()
|
||||
.Select(bt => bt.IsGenericType ? bt.GetGenericTypeDefinition() : null)
|
||||
.Any(bt => bt == typeof(FrozenSet<>))))
|
||||
return ((System.Collections.IEnumerable)v).Cast<object>().Select(FormatValue).JoinWith(", ");
|
||||
|
||||
// This is only for documentation generation
|
||||
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
if (t.IsGenericType &&
|
||||
(t.GetGenericTypeDefinition() == typeof(Dictionary<,>) ||
|
||||
t.GetGenericTypeDefinition()
|
||||
.BaseTypes()
|
||||
.Select(bt => bt.IsGenericType ? bt.GetGenericTypeDefinition() : null)
|
||||
.Any(bt => bt == typeof(FrozenDictionary<,>))))
|
||||
{
|
||||
var result = "";
|
||||
var result = new StringBuilder();
|
||||
var dict = (System.Collections.IDictionary)v;
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
@@ -94,10 +120,10 @@ namespace OpenRA
|
||||
var formattedKey = FormatValue(key);
|
||||
var formattedValue = FormatValue(value);
|
||||
|
||||
result += $"{formattedKey}: {formattedValue}{Environment.NewLine}";
|
||||
result.Append(CultureInfo.InvariantCulture, $"{formattedKey}: {formattedValue}{Environment.NewLine}");
|
||||
}
|
||||
|
||||
return result;
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
if (v is DateTime d)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace OpenRA.Mods.Cnc.FileFormats
|
||||
namespace OpenRA.FileFormats
|
||||
{
|
||||
/// <summary>
|
||||
/// Static class that uses a lookup table to calculates CRC32
|
||||
@@ -22,8 +22,8 @@ namespace OpenRA.Mods.Cnc.FileFormats
|
||||
/// <summary>
|
||||
/// The CRC32 lookup table.
|
||||
/// </summary>
|
||||
static readonly uint[] LookUp = new uint[256]
|
||||
{
|
||||
static readonly uint[] LookUp =
|
||||
[
|
||||
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
|
||||
0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
|
||||
0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
|
||||
@@ -88,7 +88,7 @@ namespace OpenRA.Mods.Cnc.FileFormats
|
||||
0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
|
||||
0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
|
||||
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
|
||||
};
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// A CRC32 implementation that can be used on spans of bytes.
|
||||
@@ -118,5 +118,16 @@ namespace OpenRA.Mods.Cnc.FileFormats
|
||||
{
|
||||
return Calculate(data, 0xFFFFFFFF);
|
||||
}
|
||||
|
||||
// Chaining method: accepts the current CRC state
|
||||
public static uint Update(uint crc, ReadOnlySpan<byte> data)
|
||||
{
|
||||
for (var i = 0; i < data.Length; i++)
|
||||
crc = (crc >> 8) ^ LookUp[(crc & 0xFF) ^ data[i]];
|
||||
return crc;
|
||||
}
|
||||
|
||||
// Standard PNG start: 0xFFFFFFFF, Finalize: XOR 0xFFFFFFFF
|
||||
public static uint Finish(uint crc) => crc ^ 0xFFFFFFFF;
|
||||
}
|
||||
}
|
||||
@@ -10,28 +10,105 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using ICSharpCode.SharpZipLib.Checksum;
|
||||
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.FileFormats
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to connect several IDAT chunks into a continuous stream.
|
||||
/// </summary>
|
||||
sealed class PngIdatStream : Stream
|
||||
{
|
||||
readonly Stream baseStream;
|
||||
int remainingInChunk;
|
||||
bool eof;
|
||||
|
||||
public PngIdatStream(Stream baseStream, int initialLen)
|
||||
{
|
||||
this.baseStream = baseStream;
|
||||
remainingInChunk = initialLen;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) => Read(buffer.AsSpan(offset, count));
|
||||
|
||||
public override int Read(Span<byte> buffer)
|
||||
{
|
||||
if (eof || buffer.Length == 0)
|
||||
return 0;
|
||||
|
||||
var totalRead = 0;
|
||||
Span<byte> header = stackalloc byte[8];
|
||||
while (buffer.Length > 0)
|
||||
{
|
||||
if (remainingInChunk == 0)
|
||||
{
|
||||
// Skip CRC and read next chunk header.
|
||||
var r = baseStream.Seek(4, SeekOrigin.Current) + baseStream.Read(header);
|
||||
if (r < 12)
|
||||
throw new EndOfStreamException("Invalid PNG file - no end chunk found.");
|
||||
|
||||
// The PNG spec states that IDAT chunks must be chained together.
|
||||
if (BinaryPrimitives.ReadUInt32BigEndian(header[4..]) == Png.ChunkIDAT)
|
||||
remainingInChunk = BinaryPrimitives.ReadInt32BigEndian(header[..4]);
|
||||
else
|
||||
{
|
||||
// This is not an IDAT chunk. Discontinue reading.
|
||||
baseStream.Seek(-8, SeekOrigin.Current);
|
||||
eof = true;
|
||||
return totalRead;
|
||||
}
|
||||
}
|
||||
|
||||
var toRead = Math.Min(buffer.Length, remainingInChunk);
|
||||
var read = baseStream.Read(buffer[..toRead]);
|
||||
if (read == 0)
|
||||
throw new EndOfStreamException("Unexpected end of stream in IDAT chunk.");
|
||||
|
||||
remainingInChunk -= read;
|
||||
totalRead += read;
|
||||
buffer = buffer[read..];
|
||||
}
|
||||
|
||||
return totalRead;
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => throw new NotSupportedException();
|
||||
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
|
||||
public override void Flush() { }
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public class Png
|
||||
{
|
||||
static readonly byte[] Signature = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
|
||||
public const uint ChunkIHDR = 0x49484452;
|
||||
public const uint ChunkPLTE = 0x504C5445;
|
||||
public const uint ChunkIDAT = 0x49444154;
|
||||
public const uint ChunkIEND = 0x49454E44;
|
||||
public const uint ChunkTRNS = 0x74524E53;
|
||||
public const uint ChunkTEXT = 0x74455874;
|
||||
|
||||
static readonly byte[] Signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
||||
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
public Color[] Palette { get; }
|
||||
public byte[] Data { get; }
|
||||
public SpriteFrameType Type { get; }
|
||||
public Dictionary<string, string> EmbeddedData = new();
|
||||
public Dictionary<string, string> EmbeddedData = [];
|
||||
|
||||
public int PixelStride => Type == SpriteFrameType.Indexed8 ? 1 : Type == SpriteFrameType.Rgb24 ? 3 : 4;
|
||||
|
||||
@@ -42,164 +119,273 @@ namespace OpenRA.FileFormats
|
||||
|
||||
s.Position += 8;
|
||||
var headerParsed = false;
|
||||
var data = new List<byte>();
|
||||
var dataParsed = false;
|
||||
byte bitDepth = 8;
|
||||
Type = SpriteFrameType.Rgba32;
|
||||
|
||||
// Use a reusable 8-byte buffer for Length + Type.
|
||||
Span<byte> chunkHeader = stackalloc byte[8];
|
||||
|
||||
while (true)
|
||||
{
|
||||
var length = IPAddress.NetworkToHostOrder(s.ReadInt32());
|
||||
var type = Encoding.UTF8.GetString(s.ReadBytes(4));
|
||||
var content = s.ReadBytes(length);
|
||||
/*var crc = */s.ReadInt32();
|
||||
if (s.Read(chunkHeader) < 8)
|
||||
throw new EndOfStreamException("Invalid PNG file - no end chunk found.");
|
||||
|
||||
if (!headerParsed && type != "IHDR")
|
||||
var length = BinaryPrimitives.ReadInt32BigEndian(chunkHeader[..4]);
|
||||
var type = BinaryPrimitives.ReadUInt32BigEndian(chunkHeader[4..]);
|
||||
|
||||
if (!headerParsed && type != ChunkIHDR)
|
||||
throw new InvalidDataException("Invalid PNG file - header does not appear first.");
|
||||
|
||||
using (var ms = new MemoryStream(content))
|
||||
switch (type)
|
||||
{
|
||||
switch (type)
|
||||
case ChunkIHDR:
|
||||
{
|
||||
case "IHDR":
|
||||
if (headerParsed)
|
||||
throw new InvalidDataException("Invalid PNG file - duplicate header.");
|
||||
|
||||
#pragma warning disable CA2014 // Do not use stackalloc in loops
|
||||
Span<byte> buffer = stackalloc byte[13];
|
||||
#pragma warning restore CA2014 // Do not use stackalloc in loops
|
||||
s.ReadBytes(buffer);
|
||||
|
||||
Width = BinaryPrimitives.ReadInt32BigEndian(buffer[..4]);
|
||||
Height = BinaryPrimitives.ReadInt32BigEndian(buffer[4..8]);
|
||||
bitDepth = buffer[8];
|
||||
var colorType = (PngColorType)buffer[9];
|
||||
|
||||
if (IsPaletted(bitDepth, colorType))
|
||||
Type = SpriteFrameType.Indexed8;
|
||||
else if (colorType == PngColorType.Color)
|
||||
Type = SpriteFrameType.Rgb24;
|
||||
|
||||
var compression = buffer[10];
|
||||
|
||||
// filter = buffer[11]
|
||||
var interlace = buffer[12];
|
||||
|
||||
if (compression != 0)
|
||||
throw new InvalidDataException("Compression method not supported");
|
||||
|
||||
if (interlace != 0)
|
||||
throw new InvalidDataException("Interlacing not supported");
|
||||
|
||||
Data = new byte[Width * Height * PixelStride];
|
||||
|
||||
// Skip CRC.
|
||||
s.Seek(4, SeekOrigin.Current);
|
||||
headerParsed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case ChunkPLTE:
|
||||
{
|
||||
if (length % 3 != 0)
|
||||
throw new InvalidDataException("Invalid PLTE chunk length.");
|
||||
|
||||
var count = length / 3;
|
||||
Palette = new Color[count];
|
||||
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(length);
|
||||
try
|
||||
{
|
||||
if (headerParsed)
|
||||
throw new InvalidDataException("Invalid PNG file - duplicate header.");
|
||||
Width = IPAddress.NetworkToHostOrder(ms.ReadInt32());
|
||||
Height = IPAddress.NetworkToHostOrder(ms.ReadInt32());
|
||||
|
||||
var bitDepth = ms.ReadUInt8();
|
||||
var colorType = (PngColorType)ms.ReadUInt8();
|
||||
if (IsPaletted(bitDepth, colorType))
|
||||
Type = SpriteFrameType.Indexed8;
|
||||
else if (colorType == PngColorType.Color)
|
||||
Type = SpriteFrameType.Rgb24;
|
||||
|
||||
Data = new byte[Width * Height * PixelStride];
|
||||
|
||||
var compression = ms.ReadUInt8();
|
||||
/*var filter = */ms.ReadUInt8();
|
||||
var interlace = ms.ReadUInt8();
|
||||
|
||||
if (compression != 0)
|
||||
throw new InvalidDataException("Compression method not supported");
|
||||
|
||||
if (interlace != 0)
|
||||
throw new InvalidDataException("Interlacing not supported");
|
||||
|
||||
headerParsed = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "PLTE":
|
||||
{
|
||||
Palette = new Color[256];
|
||||
for (var i = 0; i < length / 3; i++)
|
||||
s.ReadBytes(buffer.AsSpan(0, length));
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var r = ms.ReadUInt8(); var g = ms.ReadUInt8(); var b = ms.ReadUInt8();
|
||||
Palette[i] = Color.FromArgb(r, g, b);
|
||||
var offset = i * 3;
|
||||
Palette[i] = Color.FromArgb(buffer[offset], buffer[offset + 1], buffer[offset + 2]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "tRNS":
|
||||
finally
|
||||
{
|
||||
if (Palette == null)
|
||||
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
|
||||
|
||||
for (var i = 0; i < length; i++)
|
||||
Palette[i] = Color.FromArgb(ms.ReadUInt8(), Palette[i]);
|
||||
|
||||
break;
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
case "IDAT":
|
||||
// Skip CRC.
|
||||
s.Seek(4, SeekOrigin.Current);
|
||||
break;
|
||||
}
|
||||
|
||||
case ChunkTRNS:
|
||||
{
|
||||
if (Palette == null)
|
||||
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
|
||||
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(length);
|
||||
try
|
||||
{
|
||||
data.AddRange(content);
|
||||
|
||||
break;
|
||||
s.ReadBytes(buffer.AsSpan(0, length));
|
||||
for (var i = 0; i < length && i < Palette.Length; i++)
|
||||
Palette[i] = Color.FromArgb(buffer[i], Palette[i]);
|
||||
}
|
||||
|
||||
case "tEXt":
|
||||
finally
|
||||
{
|
||||
var key = ms.ReadASCIIZ();
|
||||
EmbeddedData.Add(key, ms.ReadASCII(length - key.Length - 1));
|
||||
|
||||
break;
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
case "IEND":
|
||||
// Skip CRC.
|
||||
s.Seek(4, SeekOrigin.Current);
|
||||
break;
|
||||
}
|
||||
|
||||
case ChunkIDAT:
|
||||
if (dataParsed)
|
||||
throw new InvalidDataException("Invalid PNG file - discontinuous IDAT chunks.");
|
||||
|
||||
dataParsed = true;
|
||||
|
||||
ProcessIDAT(s, length, bitDepth);
|
||||
break;
|
||||
|
||||
case ChunkTEXT:
|
||||
{
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(length);
|
||||
try
|
||||
{
|
||||
using (var ns = new MemoryStream(data.ToArray()))
|
||||
{
|
||||
using (var ds = new InflaterInputStream(ns))
|
||||
{
|
||||
var pxStride = PixelStride;
|
||||
var rowStride = Width * pxStride;
|
||||
var span = buffer.AsSpan(0, length);
|
||||
s.ReadBytes(span);
|
||||
|
||||
Span<byte> prevLine = new byte[rowStride];
|
||||
for (var y = 0; y < Height; y++)
|
||||
{
|
||||
var filter = (PngFilter)ds.ReadUInt8();
|
||||
ds.ReadBytes(Data, y * rowStride, rowStride);
|
||||
var line = Data.AsSpan(y * rowStride, rowStride);
|
||||
// Find Null Terminator for ASCIIZ (Keyword).
|
||||
var nullIndex = span.IndexOf((byte)0);
|
||||
if (nullIndex == -1)
|
||||
return;
|
||||
|
||||
switch (filter)
|
||||
{
|
||||
case PngFilter.None:
|
||||
break;
|
||||
case PngFilter.Sub:
|
||||
for (var i = pxStride; i < rowStride; i++)
|
||||
line[i] += line[i - pxStride];
|
||||
break;
|
||||
case PngFilter.Up:
|
||||
for (var i = 0; i < rowStride; i++)
|
||||
line[i] += prevLine[i];
|
||||
break;
|
||||
case PngFilter.Average:
|
||||
for (var i = 0; i < pxStride; i++)
|
||||
line[i] += Average(0, prevLine[i]);
|
||||
for (var i = pxStride; i < rowStride; i++)
|
||||
line[i] += Average(line[i - pxStride], prevLine[i]);
|
||||
break;
|
||||
case PngFilter.Paeth:
|
||||
for (var i = 0; i < pxStride; i++)
|
||||
line[i] += Paeth(0, prevLine[i], 0);
|
||||
for (var i = pxStride; i < rowStride; i++)
|
||||
line[i] += Paeth(line[i - pxStride], prevLine[i], prevLine[i - pxStride]);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException("Unsupported Filter");
|
||||
}
|
||||
var key = Encoding.ASCII.GetString(span[..nullIndex]);
|
||||
var value = Encoding.ASCII.GetString(span[(nullIndex + 1)..]);
|
||||
|
||||
prevLine = line;
|
||||
}
|
||||
|
||||
static byte Average(byte a, byte b) => (byte)((a + b) / 2);
|
||||
|
||||
static byte Paeth(byte a, byte b, byte c)
|
||||
{
|
||||
var p = a + b - c;
|
||||
var pa = Math.Abs(p - a);
|
||||
var pb = Math.Abs(p - b);
|
||||
var pc = Math.Abs(p - c);
|
||||
|
||||
return (pa <= pb && pa <= pc) ? a :
|
||||
(pb <= pc) ? b : c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Type == SpriteFrameType.Indexed8 && Palette == null)
|
||||
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
|
||||
|
||||
return;
|
||||
EmbeddedData[key] = value;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
// Skip CRC.
|
||||
s.Seek(4, SeekOrigin.Current);
|
||||
break;
|
||||
}
|
||||
|
||||
case ChunkIEND:
|
||||
{
|
||||
if (Type == SpriteFrameType.Indexed8 && Palette == null)
|
||||
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
|
||||
|
||||
// Skip CRC.
|
||||
s.Seek(4, SeekOrigin.Current);
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
// Skip unknown chunk + CRC.
|
||||
s.Seek(length + 4, SeekOrigin.Current);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessIDAT(Stream s, int firstChunkLength, byte bitDepth)
|
||||
{
|
||||
var pxStride = PixelStride;
|
||||
var rowStride = Width * pxStride;
|
||||
var pixelsPerByte = 8 / bitDepth;
|
||||
var sourceRowStride = Exts.IntegerDivisionRoundingAwayFromZero(rowStride, pixelsPerByte);
|
||||
|
||||
// Custom stream that stitches IDAT chunks together without copying.
|
||||
using var idatStream = new PngIdatStream(s, firstChunkLength);
|
||||
using var ds = new ZLibStream(idatStream, CompressionMode.Decompress);
|
||||
|
||||
// Rent buffer from pool to avoid allocation churn.
|
||||
var prevLineBuffer = ArrayPool<byte>.Shared.Rent(rowStride);
|
||||
var prevLine = prevLineBuffer.AsSpan(0, rowStride);
|
||||
prevLine.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
for (var y = 0; y < Height; y++)
|
||||
{
|
||||
var filterByte = ds.ReadByte();
|
||||
if (filterByte == -1)
|
||||
break;
|
||||
|
||||
var filter = (PngFilter)filterByte;
|
||||
var line = Data.AsSpan(y * rowStride, rowStride);
|
||||
|
||||
ds.ReadBytes(line[..sourceRowStride]);
|
||||
|
||||
// If the source has a bit depth of 1, 2 or 4 it packs multiple pixels per byte.
|
||||
// Unpack to bit depth of 8, yielding 1 pixel per byte.
|
||||
// This makes life easier for consumers of palleted data.
|
||||
if (bitDepth < 8)
|
||||
{
|
||||
var mask = (1 << bitDepth) - 1;
|
||||
for (var i = sourceRowStride - 1; i >= 0; i--)
|
||||
{
|
||||
var packed = line[i];
|
||||
for (var j = 0; j < pixelsPerByte; j++)
|
||||
{
|
||||
var dest = i * pixelsPerByte + j;
|
||||
if (dest < line.Length) // Guard against last byte being only partially packed
|
||||
line[dest] = (byte)((packed >> (8 - (j + 1) * bitDepth)) & mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (filter)
|
||||
{
|
||||
case PngFilter.None:
|
||||
break;
|
||||
case PngFilter.Sub:
|
||||
for (var i = pxStride; i < line.Length; i++)
|
||||
line[i] += line[i - pxStride];
|
||||
break;
|
||||
case PngFilter.Up:
|
||||
for (var i = 0; i < line.Length; i++)
|
||||
line[i] += prevLine[i];
|
||||
break;
|
||||
case PngFilter.Average:
|
||||
for (var i = 0; i < pxStride; i++)
|
||||
line[i] += Average(0, prevLine[i]);
|
||||
for (var i = pxStride; i < line.Length; i++)
|
||||
line[i] += Average(line[i - pxStride], prevLine[i]);
|
||||
break;
|
||||
case PngFilter.Paeth:
|
||||
for (var i = 0; i < pxStride; i++)
|
||||
line[i] += Paeth(0, prevLine[i], 0);
|
||||
for (var i = pxStride; i < line.Length; i++)
|
||||
line[i] += Paeth(line[i - pxStride], prevLine[i], prevLine[i - pxStride]);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException("Unsupported Filter");
|
||||
}
|
||||
|
||||
prevLine = line;
|
||||
}
|
||||
|
||||
// Drain remaining Zlib footer bytes if necessary.
|
||||
Span<byte> drain = stackalloc byte[16];
|
||||
while (ds.Read(drain) > 0) { }
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(prevLineBuffer);
|
||||
}
|
||||
|
||||
static byte Average(byte a, byte b) => (byte)((a + b) / 2);
|
||||
|
||||
static byte Paeth(byte a, byte b, byte c)
|
||||
{
|
||||
var p = a + b - c;
|
||||
var pa = Math.Abs(p - a);
|
||||
var pb = Math.Abs(p - b);
|
||||
var pc = Math.Abs(p - c);
|
||||
|
||||
return (pa <= pb && pa <= pc) ? a :
|
||||
(pb <= pc) ? b : c;
|
||||
}
|
||||
}
|
||||
|
||||
public Png(byte[] data, SpriteFrameType type, int width, int height, Color[] palette = null,
|
||||
Dictionary<string, string> embeddedData = null)
|
||||
{
|
||||
@@ -234,14 +420,27 @@ namespace OpenRA.FileFormats
|
||||
// Convert to big endian
|
||||
Data = new byte[data.Length];
|
||||
var stride = PixelStride;
|
||||
for (var i = 0; i < width * height; i++)
|
||||
var elements = width * height * stride;
|
||||
var src = data.AsSpan();
|
||||
var dst = Data.AsSpan();
|
||||
if (type == SpriteFrameType.Bgra32)
|
||||
{
|
||||
Data[stride * i] = data[stride * i + 2];
|
||||
Data[stride * i + 1] = data[stride * i + 1];
|
||||
Data[stride * i + 2] = data[stride * i + 0];
|
||||
|
||||
if (type == SpriteFrameType.Bgra32)
|
||||
Data[stride * i + 3] = data[stride * i + 3];
|
||||
for (var i = 0; i < elements; i += 4)
|
||||
{
|
||||
dst[i + 0] = src[i + 2];
|
||||
dst[i + 1] = src[i + 1];
|
||||
dst[i + 2] = src[i + 0];
|
||||
dst[i + 3] = src[i + 3];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < elements; i += 3)
|
||||
{
|
||||
dst[i + 0] = src[i + 2];
|
||||
dst[i + 1] = src[i + 1];
|
||||
dst[i + 2] = src[i + 0];
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -258,7 +457,8 @@ namespace OpenRA.FileFormats
|
||||
public static bool Verify(Stream s)
|
||||
{
|
||||
var pos = s.Position;
|
||||
var isPng = Signature.Aggregate(true, (current, t) => current && s.ReadUInt8() == t);
|
||||
Span<byte> sigBuffer = stackalloc byte[8];
|
||||
var isPng = s.Read(sigBuffer) == 8 && sigBuffer.SequenceEqual(Signature);
|
||||
s.Position = pos;
|
||||
return isPng;
|
||||
}
|
||||
@@ -269,7 +469,7 @@ namespace OpenRA.FileFormats
|
||||
|
||||
static bool IsPaletted(byte bitDepth, PngColorType colorType)
|
||||
{
|
||||
if (bitDepth == 8 && colorType == (PngColorType.Indexed | PngColorType.Color))
|
||||
if (bitDepth <= 8 && colorType == (PngColorType.Indexed | PngColorType.Color))
|
||||
return true;
|
||||
|
||||
if (bitDepth == 8 && colorType == (PngColorType.Color | PngColorType.Alpha))
|
||||
@@ -281,28 +481,33 @@ namespace OpenRA.FileFormats
|
||||
throw new InvalidDataException("Unknown pixel format");
|
||||
}
|
||||
|
||||
static void WritePngChunk(Stream output, string type, Stream input)
|
||||
static void WritePngChunk(MemoryStream output, uint type, MemoryStream input)
|
||||
{
|
||||
input.Position = 0;
|
||||
Span<byte> header = stackalloc byte[8];
|
||||
BinaryPrimitives.WriteInt32BigEndian(header[..4], (int)input.Length);
|
||||
BinaryPrimitives.WriteUInt32BigEndian(header[4..], type);
|
||||
|
||||
var typeBytes = Encoding.ASCII.GetBytes(type);
|
||||
output.Write(IPAddress.HostToNetworkOrder((int)input.Length));
|
||||
output.WriteArray(typeBytes);
|
||||
if (!input.TryGetBuffer(out var dataSegment))
|
||||
dataSegment = new ArraySegment<byte>(input.ToArray());
|
||||
|
||||
var data = input.ReadAllBytes();
|
||||
output.WriteArray(data);
|
||||
ReadOnlySpan<byte> data = dataSegment.AsSpan(0, (int)input.Length);
|
||||
|
||||
var crc32 = new Crc32();
|
||||
crc32.Update(typeBytes);
|
||||
crc32.Update(data);
|
||||
output.Write(IPAddress.NetworkToHostOrder((int)crc32.Value));
|
||||
output.Write(header);
|
||||
output.Write(data);
|
||||
|
||||
var crc = 0xFFFFFFFF;
|
||||
crc = CRC32.Update(crc, header[4..]);
|
||||
crc = CRC32.Update(crc, data);
|
||||
var finalCrc = CRC32.Finish(crc);
|
||||
|
||||
output.Write(IPAddress.NetworkToHostOrder((int)finalCrc));
|
||||
}
|
||||
|
||||
public byte[] Save()
|
||||
public byte[] Save(CompressionLevel compression = CompressionLevel.SmallestSize)
|
||||
{
|
||||
using (var output = new MemoryStream())
|
||||
{
|
||||
output.WriteArray(Signature);
|
||||
output.Write(Signature);
|
||||
using (var header = new MemoryStream())
|
||||
{
|
||||
header.Write(IPAddress.HostToNetworkOrder(Width));
|
||||
@@ -317,7 +522,7 @@ namespace OpenRA.FileFormats
|
||||
header.WriteByte(0); // Filter
|
||||
header.WriteByte(0); // Interlacing
|
||||
|
||||
WritePngChunk(output, "IHDR", header);
|
||||
WritePngChunk(output, ChunkIHDR, header);
|
||||
}
|
||||
|
||||
var alphaPalette = false;
|
||||
@@ -333,7 +538,7 @@ namespace OpenRA.FileFormats
|
||||
alphaPalette |= c.A > 0;
|
||||
}
|
||||
|
||||
WritePngChunk(output, "PLTE", palette);
|
||||
WritePngChunk(output, ChunkPLTE, palette);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,46 +549,44 @@ namespace OpenRA.FileFormats
|
||||
foreach (var c in Palette)
|
||||
alpha.WriteByte(c.A);
|
||||
|
||||
WritePngChunk(output, "tRNS", alpha);
|
||||
WritePngChunk(output, ChunkTRNS, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
using (var data = new MemoryStream())
|
||||
{
|
||||
using (var compressed = new DeflaterOutputStream(data))
|
||||
using (var compressed = new ZLibStream(data, compression, true))
|
||||
{
|
||||
var rowStride = Width * PixelStride;
|
||||
for (var y = 0; y < Height; y++)
|
||||
{
|
||||
// Write uncompressed scanlines for simplicity
|
||||
compressed.WriteByte(0);
|
||||
// Assuming no filtering for simplicity
|
||||
const byte FilterType = 0;
|
||||
compressed.WriteByte(FilterType);
|
||||
compressed.Write(Data, y * rowStride, rowStride);
|
||||
}
|
||||
|
||||
compressed.Flush();
|
||||
compressed.Finish();
|
||||
|
||||
WritePngChunk(output, "IDAT", data);
|
||||
}
|
||||
|
||||
WritePngChunk(output, ChunkIDAT, data);
|
||||
}
|
||||
|
||||
foreach (var kv in EmbeddedData)
|
||||
{
|
||||
using (var text = new MemoryStream())
|
||||
{
|
||||
text.WriteArray(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
|
||||
WritePngChunk(output, "tEXt", text);
|
||||
text.Write(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
|
||||
WritePngChunk(output, ChunkTEXT, text);
|
||||
}
|
||||
}
|
||||
|
||||
WritePngChunk(output, "IEND", new MemoryStream());
|
||||
WritePngChunk(output, ChunkIEND, new MemoryStream());
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(string path)
|
||||
public void Save(string path, CompressionLevel compression = CompressionLevel.SmallestSize)
|
||||
{
|
||||
File.WriteAllBytes(path, Save());
|
||||
File.WriteAllBytes(path, Save(compression));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,7 @@ namespace OpenRA.FileFormats
|
||||
|
||||
public ReplayMetadata(GameInformation info)
|
||||
{
|
||||
if (info == null)
|
||||
throw new ArgumentNullException(nameof(info));
|
||||
ArgumentNullException.ThrowIfNull(info);
|
||||
|
||||
GameInfo = info;
|
||||
}
|
||||
@@ -47,8 +46,8 @@ namespace OpenRA.FileFormats
|
||||
throw new NotSupportedException($"Metadata version {version} is not supported");
|
||||
|
||||
// Read game info (max 100K limit as a safeguard against corrupted files)
|
||||
var data = fs.ReadString(Encoding.UTF8, 1024 * 100);
|
||||
GameInfo = GameInformation.Deserialize(data);
|
||||
var data = fs.ReadLengthPrefixedString(Encoding.UTF8, 1024 * 100);
|
||||
GameInfo = GameInformation.Deserialize(data, path);
|
||||
}
|
||||
|
||||
public void Write(BinaryWriter writer)
|
||||
@@ -62,7 +61,7 @@ namespace OpenRA.FileFormats
|
||||
{
|
||||
// Write lobby info data
|
||||
writer.Flush();
|
||||
dataLength += writer.BaseStream.WriteString(Encoding.UTF8, GameInfo.Serialize());
|
||||
dataLength += writer.BaseStream.WriteLengthPrefixedString(Encoding.UTF8, GameInfo.Serialize());
|
||||
}
|
||||
|
||||
// Write total length & end marker
|
||||
|
||||
@@ -23,22 +23,22 @@ namespace OpenRA.FileSystem
|
||||
bool TryGetPackageContaining(string path, out IReadOnlyPackage package, out string filename);
|
||||
bool TryOpen(string filename, out Stream s);
|
||||
bool Exists(string filename);
|
||||
bool IsExternalModFile(string filename);
|
||||
bool IsExternalFile(string filename);
|
||||
}
|
||||
|
||||
public class FileSystem : IReadOnlyFileSystem
|
||||
{
|
||||
public IEnumerable<IReadOnlyPackage> MountedPackages => mountedPackages.Keys;
|
||||
readonly Dictionary<IReadOnlyPackage, int> mountedPackages = new();
|
||||
readonly Dictionary<string, IReadOnlyPackage> explicitMounts = new();
|
||||
readonly Dictionary<IReadOnlyPackage, int> mountedPackages = [];
|
||||
readonly Dictionary<string, IReadOnlyPackage> explicitMounts = [];
|
||||
readonly string modID;
|
||||
|
||||
// Mod packages that should not be disposed
|
||||
readonly List<IReadOnlyPackage> modPackages = new();
|
||||
readonly List<IReadOnlyPackage> modPackages = [];
|
||||
readonly IReadOnlyDictionary<string, Manifest> installedMods;
|
||||
readonly IPackageLoader[] packageLoaders;
|
||||
|
||||
Cache<string, List<IReadOnlyPackage>> fileIndex = new(_ => new List<IReadOnlyPackage>());
|
||||
Cache<string, List<IReadOnlyPackage>> fileIndex = new(_ => []);
|
||||
|
||||
public FileSystem(string modID, IReadOnlyDictionary<string, Manifest> installedMods, IPackageLoader[] packageLoaders)
|
||||
{
|
||||
@@ -83,14 +83,14 @@ namespace OpenRA.FileSystem
|
||||
|
||||
public void Mount(string name, string explicitName = null)
|
||||
{
|
||||
var optional = name.StartsWith("~", StringComparison.Ordinal);
|
||||
var optional = name.StartsWith('~');
|
||||
if (optional)
|
||||
name = name[1..];
|
||||
|
||||
try
|
||||
{
|
||||
IReadOnlyPackage package;
|
||||
if (name.StartsWith("$", StringComparison.Ordinal))
|
||||
if (name.StartsWith('$'))
|
||||
{
|
||||
name = name[1..];
|
||||
|
||||
@@ -109,10 +109,8 @@ namespace OpenRA.FileSystem
|
||||
|
||||
Mount(package, explicitName);
|
||||
}
|
||||
catch
|
||||
catch when (optional)
|
||||
{
|
||||
if (!optional)
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,9 +159,7 @@ namespace OpenRA.FileSystem
|
||||
explicitMounts.Remove(key);
|
||||
|
||||
// Mod packages aren't owned by us, so we shouldn't dispose them
|
||||
if (modPackages.Contains(package))
|
||||
modPackages.Remove(package);
|
||||
else
|
||||
if (!modPackages.Remove(package))
|
||||
package.Dispose();
|
||||
}
|
||||
else
|
||||
@@ -182,14 +178,16 @@ namespace OpenRA.FileSystem
|
||||
explicitMounts.Clear();
|
||||
modPackages.Clear();
|
||||
|
||||
fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
|
||||
fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => []);
|
||||
}
|
||||
|
||||
public void LoadFromManifest(Manifest manifest)
|
||||
public void TrimExcess()
|
||||
{
|
||||
UnmountAll();
|
||||
foreach (var kv in manifest.Packages)
|
||||
Mount(kv.Key, kv.Value);
|
||||
mountedPackages.TrimExcess();
|
||||
explicitMounts.TrimExcess();
|
||||
modPackages.TrimExcess();
|
||||
foreach (var packages in fileIndex.Values)
|
||||
packages.TrimExcess();
|
||||
}
|
||||
|
||||
Stream GetFromCache(string filename)
|
||||
@@ -226,14 +224,11 @@ namespace OpenRA.FileSystem
|
||||
public bool TryOpen(string filename, out Stream s)
|
||||
{
|
||||
var explicitSplit = filename.IndexOf('|');
|
||||
if (explicitSplit > 0)
|
||||
if (explicitSplit > 0 && explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
|
||||
{
|
||||
if (explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
|
||||
{
|
||||
s = explicitPackage.GetStream(filename[(explicitSplit + 1)..]);
|
||||
if (s != null)
|
||||
return true;
|
||||
}
|
||||
s = explicitPackage.GetStream(filename[(explicitSplit + 1)..]);
|
||||
if (s != null)
|
||||
return true;
|
||||
}
|
||||
|
||||
s = GetFromCache(filename);
|
||||
@@ -262,64 +257,20 @@ namespace OpenRA.FileSystem
|
||||
public bool Exists(string filename)
|
||||
{
|
||||
var explicitSplit = filename.IndexOf('|');
|
||||
if (explicitSplit > 0)
|
||||
if (explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
|
||||
if (explicitPackage.Contains(filename[(explicitSplit + 1)..]))
|
||||
return true;
|
||||
if (explicitSplit > 0 &&
|
||||
explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage) &&
|
||||
explicitPackage.Contains(filename[(explicitSplit + 1)..]))
|
||||
return true;
|
||||
|
||||
return fileIndex.ContainsKey(filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the given filename references an external mod via an explicit mount.
|
||||
/// Returns true if the given filename references any file outside the mod mount.
|
||||
/// </summary>
|
||||
public bool IsExternalModFile(string filename)
|
||||
public bool IsExternalFile(string filename)
|
||||
{
|
||||
var explicitSplit = filename.IndexOf('|');
|
||||
if (explicitSplit < 0)
|
||||
return false;
|
||||
|
||||
if (!explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
|
||||
return false;
|
||||
|
||||
if (installedMods[modID].Package == explicitPackage)
|
||||
return false;
|
||||
|
||||
return modPackages.Contains(explicitPackage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a filesystem for an assembly, accounting for explicit and mod mounts.
|
||||
/// Assemblies must exist in the native OS file system (not inside an OpenRA-defined package).
|
||||
/// </summary>
|
||||
public static string ResolveAssemblyPath(string path, Manifest manifest, InstalledMods installedMods)
|
||||
{
|
||||
var explicitSplit = path.IndexOf('|');
|
||||
if (explicitSplit > 0 && !path.StartsWith("^"))
|
||||
{
|
||||
var parent = path[..explicitSplit];
|
||||
var filename = path[(explicitSplit + 1)..];
|
||||
|
||||
var parentPath = manifest.Packages.FirstOrDefault(kv => kv.Value == parent).Key;
|
||||
if (parentPath == null)
|
||||
return null;
|
||||
|
||||
if (parentPath.StartsWith("$", StringComparison.Ordinal))
|
||||
{
|
||||
if (!installedMods.TryGetValue(parentPath[1..], out var mod))
|
||||
return null;
|
||||
|
||||
if (mod.Package is not Folder)
|
||||
return null;
|
||||
|
||||
path = Path.Combine(mod.Package.Name, filename);
|
||||
}
|
||||
else
|
||||
path = Path.Combine(parentPath, filename);
|
||||
}
|
||||
|
||||
var resolvedPath = Platform.ResolvePath(path);
|
||||
return File.Exists(resolvedPath) ? resolvedPath : null;
|
||||
return !filename.StartsWith($"{modID}|", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public static string ResolveCaseInsensitivePath(string path)
|
||||
@@ -335,7 +286,8 @@ namespace OpenRA.FileSystem
|
||||
if (name == ".")
|
||||
continue;
|
||||
|
||||
resolved = Directory.GetFileSystemEntries(resolved).FirstOrDefault(e => e.Equals(Path.Combine(resolved, name), StringComparison.InvariantCultureIgnoreCase));
|
||||
resolved = Directory.GetFileSystemEntries(resolved)
|
||||
.FirstOrDefault(e => e.Equals(Path.Combine(resolved, name), StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (resolved == null)
|
||||
return null;
|
||||
|
||||
@@ -35,13 +35,17 @@ namespace OpenRA.FileSystem
|
||||
return Directory.GetFiles(Name, "*", SearchOption.TopDirectoryOnly)
|
||||
.Concat(Directory.GetDirectories(Name))
|
||||
.Select(Path.GetFileName)
|
||||
.OrderBy(f => f);
|
||||
.Order();
|
||||
}
|
||||
}
|
||||
|
||||
public Stream GetStream(string filename)
|
||||
{
|
||||
try { return File.OpenRead(Path.Combine(Name, filename)); }
|
||||
var combined = Path.Combine(Name, filename);
|
||||
if (!File.Exists(combined))
|
||||
return null;
|
||||
|
||||
try { return File.OpenRead(combined); }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
@@ -80,7 +84,7 @@ namespace OpenRA.FileSystem
|
||||
// in FileSystem.OpenPackage. Their internal name therefore contains the
|
||||
// full parent path too. We need to be careful to not add a second path
|
||||
// prefix to these hacked packages.
|
||||
var filePath = filename.StartsWith(Name) ? filename : Path.Combine(Name, filename);
|
||||
var filePath = filename.StartsWith(Name, StringComparison.Ordinal) ? filename : Path.Combine(Name, filename);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
|
||||
using (var s = File.Create(filePath))
|
||||
@@ -94,7 +98,7 @@ namespace OpenRA.FileSystem
|
||||
// in FileSystem.OpenPackage. Their internal name therefore contains the
|
||||
// full parent path too. We need to be careful to not add a second path
|
||||
// prefix to these hacked packages.
|
||||
var filePath = filename.StartsWith(Name) ? filename : Path.Combine(Name, filename);
|
||||
var filePath = filename.StartsWith(Name, StringComparison.Ordinal) ? filename : Path.Combine(Name, filename);
|
||||
if (Directory.Exists(filePath))
|
||||
Directory.Delete(filePath, true);
|
||||
else if (File.Exists(filePath))
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace OpenRA.FileSystem
|
||||
{
|
||||
const uint ZipSignature = 0x04034b50;
|
||||
|
||||
class ReadOnlyZipFile : IReadOnlyPackage
|
||||
public class ReadOnlyZipFile : IReadOnlyPackage
|
||||
{
|
||||
public string Name { get; protected set; }
|
||||
protected ZipFile pkg;
|
||||
@@ -68,6 +68,7 @@ namespace OpenRA.FileSystem
|
||||
public void Dispose()
|
||||
{
|
||||
pkg?.Close();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
|
||||
@@ -93,15 +94,15 @@ namespace OpenRA.FileSystem
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage
|
||||
public sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage
|
||||
{
|
||||
readonly MemoryStream pkgStream = new();
|
||||
|
||||
public ReadWriteZipFile(string filename, bool create = false)
|
||||
public ReadWriteZipFile(string filename = null, bool create = false)
|
||||
{
|
||||
// SharpZipLib breaks when asked to update archives loaded from outside streams or files
|
||||
// We can work around this by creating a clean in-memory-only file, cutting all outside references
|
||||
if (!create)
|
||||
if (!string.IsNullOrEmpty(filename) && !create)
|
||||
{
|
||||
using (var copy = new MemoryStream(File.ReadAllBytes(filename)))
|
||||
{
|
||||
@@ -113,14 +114,33 @@ namespace OpenRA.FileSystem
|
||||
pkgStream.Position = 0;
|
||||
pkg = new ZipFile(pkgStream);
|
||||
Name = filename;
|
||||
|
||||
// Remove subfields that can break ZIP updating.
|
||||
foreach (ZipEntry entry in pkg)
|
||||
entry.ExtraData = null;
|
||||
}
|
||||
|
||||
public ReadWriteZipFile(byte[] data)
|
||||
{
|
||||
using (var copy = new MemoryStream(data))
|
||||
{
|
||||
pkgStream.Capacity = (int)copy.Length;
|
||||
copy.CopyTo(pkgStream);
|
||||
}
|
||||
|
||||
pkgStream.Position = 0;
|
||||
pkg = new ZipFile(pkgStream);
|
||||
Name = null;
|
||||
|
||||
// Remove subfields that can break ZIP updating.
|
||||
foreach (ZipEntry entry in pkg)
|
||||
entry.ExtraData = null;
|
||||
}
|
||||
|
||||
void Commit()
|
||||
{
|
||||
var pos = pkgStream.Position;
|
||||
pkgStream.Position = 0;
|
||||
File.WriteAllBytes(Name, pkgStream.ReadBytes((int)pkgStream.Length));
|
||||
pkgStream.Position = pos;
|
||||
if (!string.IsNullOrEmpty(Name))
|
||||
File.WriteAllBytes(Name, pkgStream.ToArray());
|
||||
}
|
||||
|
||||
public void Update(string filename, byte[] contents)
|
||||
@@ -147,7 +167,7 @@ namespace OpenRA.FileSystem
|
||||
|
||||
public ZipFolder(ReadOnlyZipFile parent, string path)
|
||||
{
|
||||
if (path.EndsWith("/", StringComparison.Ordinal))
|
||||
if (path.EndsWith('/'))
|
||||
path = path[..^1];
|
||||
|
||||
Name = path;
|
||||
|
||||
202
OpenRA.Game/FluentBundle.cs
Normal file
202
OpenRA.Game/FluentBundle.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using Linguini.Bundle;
|
||||
using Linguini.Bundle.Builder;
|
||||
using Linguini.Shared.Types.Bundle;
|
||||
using Linguini.Syntax.Parser;
|
||||
using Linguini.Syntax.Parser.Error;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public sealed class FluentReferenceAttribute : Attribute
|
||||
{
|
||||
public readonly bool Optional;
|
||||
public readonly string[] RequiredVariableNames;
|
||||
public readonly LintDictionaryReference DictionaryReference;
|
||||
|
||||
public FluentReferenceAttribute() { }
|
||||
|
||||
public FluentReferenceAttribute(params string[] requiredVariableNames)
|
||||
{
|
||||
RequiredVariableNames = requiredVariableNames;
|
||||
}
|
||||
|
||||
public FluentReferenceAttribute(LintDictionaryReference dictionaryReference = LintDictionaryReference.None)
|
||||
{
|
||||
DictionaryReference = dictionaryReference;
|
||||
}
|
||||
|
||||
public FluentReferenceAttribute(bool optional)
|
||||
{
|
||||
Optional = optional;
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class IncludeStaticFluentReferencesAttribute : Attribute
|
||||
{
|
||||
public readonly Type[] Types;
|
||||
|
||||
public IncludeStaticFluentReferencesAttribute(params Type[] types)
|
||||
{
|
||||
Types = types;
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class IncludeChromeLogicArgsFluentReferencesAttribute : Attribute
|
||||
{
|
||||
public readonly string[] MethodNames;
|
||||
|
||||
public IncludeChromeLogicArgsFluentReferencesAttribute(params string[] methodNames)
|
||||
{
|
||||
MethodNames = methodNames;
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public sealed class IncludeFluentReferencesAttribute : Attribute
|
||||
{
|
||||
public readonly LintDictionaryReference DictionaryReference;
|
||||
|
||||
public IncludeFluentReferencesAttribute() { }
|
||||
|
||||
public IncludeFluentReferencesAttribute(LintDictionaryReference dictionaryReference = LintDictionaryReference.None)
|
||||
{
|
||||
DictionaryReference = dictionaryReference;
|
||||
}
|
||||
}
|
||||
|
||||
public class FluentBundle
|
||||
{
|
||||
readonly Linguini.Bundle.FluentBundle bundle;
|
||||
|
||||
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem)
|
||||
: this(culture, paths, fileSystem, error => Log.Write("debug", error.Message)) { }
|
||||
|
||||
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem, string text)
|
||||
: this(culture, paths, fileSystem, text, error => Log.Write("debug", error.Message)) { }
|
||||
|
||||
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem, Action<ParseError> onError)
|
||||
: this(culture, paths, fileSystem, null, onError) { }
|
||||
|
||||
public FluentBundle(string culture, string text, Action<ParseError> onError)
|
||||
: this(culture, default, null, text, onError) { }
|
||||
|
||||
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem, string text, Action<ParseError> onError)
|
||||
{
|
||||
bundle = LinguiniBuilder.Builder()
|
||||
.CultureInfo(new CultureInfo(culture))
|
||||
.SkipResources()
|
||||
.SetUseIsolating(false)
|
||||
.UseConcurrent()
|
||||
.UncheckedBuild();
|
||||
|
||||
if (paths != null)
|
||||
{
|
||||
foreach (var path in paths)
|
||||
{
|
||||
var stream = fileSystem.Open(path);
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var parser = new LinguiniParser(reader);
|
||||
var resource = parser.Parse();
|
||||
foreach (var error in resource.Errors)
|
||||
onError(error);
|
||||
|
||||
bundle.AddResourceOverriding(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
var parser = new LinguiniParser(text);
|
||||
var resource = parser.Parse();
|
||||
foreach (var error in resource.Errors)
|
||||
onError(error);
|
||||
|
||||
bundle.AddResourceOverriding(resource);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetMessage(string key, object[] args = null)
|
||||
{
|
||||
if (!TryGetMessage(key, out var message, args))
|
||||
message = key;
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public bool TryGetMessage(string key, out string value, object[] args = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(key);
|
||||
|
||||
try
|
||||
{
|
||||
if (!HasMessage(key))
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
Dictionary<string, IFluentType> fluentArgs = null;
|
||||
if (args != null)
|
||||
{
|
||||
if (args.Length % 2 != 0)
|
||||
throw new ArgumentException("Expected a comma separated list of name, value arguments " +
|
||||
"but the number of arguments is not a multiple of two", nameof(args));
|
||||
|
||||
fluentArgs = [];
|
||||
for (var i = 0; i < args.Length; i += 2)
|
||||
{
|
||||
var argKey = args[i] as string;
|
||||
if (string.IsNullOrEmpty(argKey))
|
||||
throw new ArgumentException($"Expected the argument at index {i} to be a non-empty string", nameof(args));
|
||||
|
||||
var argValue = args[i + 1];
|
||||
if (argValue == null)
|
||||
throw new ArgumentNullException(nameof(args), $"Expected the argument at index {i + 1} to be a non-null value");
|
||||
|
||||
fluentArgs.Add(argKey, argValue.ToFluentType());
|
||||
}
|
||||
}
|
||||
|
||||
var result = bundle.TryGetAttrMessage(key, fluentArgs, out var errors, out value);
|
||||
foreach (var error in errors)
|
||||
Log.Write("debug", $"FluentBundle of {key}: {error}");
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Log.Write("debug", $"FluentBundle of {key}: threw exception");
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasMessage(string key)
|
||||
{
|
||||
return bundle.HasAttrMessage(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using Linguini.Shared.Types.Bundle;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public static class TranslationExts
|
||||
public static class FluentExts
|
||||
{
|
||||
public static IFluentType ToFluentType(this object value)
|
||||
{
|
||||
94
OpenRA.Game/FluentProvider.cs
Normal file
94
OpenRA.Game/FluentProvider.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using OpenRA.FileSystem;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public static class FluentProvider
|
||||
{
|
||||
// Ensure thread-safety.
|
||||
static readonly object SyncObject = new();
|
||||
static FluentBundle modFluentBundle;
|
||||
static FluentBundle mapFluentBundle;
|
||||
|
||||
public static void Initialize(Manifest manifest, IReadOnlyFileSystem fileSystem)
|
||||
{
|
||||
lock (SyncObject)
|
||||
{
|
||||
modFluentBundle = new FluentBundle(manifest.FluentCulture, manifest.FluentMessages, fileSystem);
|
||||
if (fileSystem is Map map && map.FluentMessageDefinitions != null)
|
||||
{
|
||||
var files = ImmutableArray<string>.Empty;
|
||||
if (map.FluentMessageDefinitions.Value != null)
|
||||
files = FieldLoader.GetValue<ImmutableArray<string>>("value", map.FluentMessageDefinitions.Value);
|
||||
|
||||
string text = null;
|
||||
if (map.FluentMessageDefinitions.Nodes.Length > 0)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
foreach (var node in map.FluentMessageDefinitions.Nodes)
|
||||
if (node.Key == "base64")
|
||||
builder.Append(Encoding.UTF8.GetString(Convert.FromBase64String(node.Value.Value)));
|
||||
|
||||
text = builder.ToString();
|
||||
}
|
||||
|
||||
mapFluentBundle = new FluentBundle(manifest.FluentCulture, files, fileSystem, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetMessage(string key, params object[] args)
|
||||
{
|
||||
lock (SyncObject)
|
||||
{
|
||||
// By prioritizing mod-level fluent bundles we prevent maps from overwriting string keys. We do not want to
|
||||
// allow maps to change the UI nor any other strings not exposed to the map.
|
||||
if (modFluentBundle.TryGetMessage(key, out var message, args))
|
||||
return message;
|
||||
|
||||
if (mapFluentBundle != null)
|
||||
return mapFluentBundle.GetMessage(key, args);
|
||||
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryGetMessage(string key, out string message, params object[] args)
|
||||
{
|
||||
lock (SyncObject)
|
||||
{
|
||||
// By prioritizing mod-level bundle we prevent maps from overwriting string keys. We do not want to
|
||||
// allow maps to change the UI nor any other strings not exposed to the map.
|
||||
if (modFluentBundle.TryGetMessage(key, out message, args))
|
||||
return true;
|
||||
|
||||
if (mapFluentBundle != null && mapFluentBundle.TryGetMessage(key, out message, args))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Should only be used by <see cref="MapPreview"/>.</summary>
|
||||
internal static bool TryGetModMessage(string key, out string message, params object[] args)
|
||||
{
|
||||
lock (SyncObject)
|
||||
{
|
||||
return modFluentBundle.TryGetMessage(key, out message, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenRA
|
||||
@@ -23,15 +24,15 @@ namespace OpenRA
|
||||
public class Fonts : IGlobalModData
|
||||
{
|
||||
[FieldLoader.LoadUsing(nameof(LoadFonts))]
|
||||
public readonly Dictionary<string, FontData> FontList;
|
||||
public readonly FrozenDictionary<string, FontData> FontList;
|
||||
|
||||
static object LoadFonts(MiniYaml y)
|
||||
{
|
||||
var ret = new Dictionary<string, FontData>();
|
||||
var ret = new Dictionary<string, FontData>(y.Nodes.Length);
|
||||
foreach (var node in y.Nodes)
|
||||
ret.Add(node.Key, FieldLoader.Load<FontData>(node.Value));
|
||||
|
||||
return ret;
|
||||
return ret.ToFrozenDictionary();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@@ -27,9 +28,10 @@ using OpenRA.Widgets;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
[IncludeStaticFluentReferences(typeof(Server.Server), typeof(Player), typeof(UnitOrders), typeof(OrderManager))]
|
||||
public static class Game
|
||||
{
|
||||
[TranslationReference("filename")]
|
||||
[FluentReference("filename")]
|
||||
const string SavedScreenshot = "notification-saved-screenshot";
|
||||
|
||||
public const int TimestepJankThreshold = 250; // Don't catch up for delays larger than 250ms
|
||||
@@ -86,8 +88,9 @@ namespace OpenRA
|
||||
|
||||
static void JoinInner(OrderManager om)
|
||||
{
|
||||
// Refresh TextNotificationsManager before the game starts.
|
||||
// Refresh static classes before the game starts.
|
||||
TextNotificationsManager.Clear();
|
||||
UnitOrders.Clear();
|
||||
|
||||
// HACK: The shellmap World and OrderManager are owned by the main menu's WorldRenderer instead of Game.
|
||||
// This allows us to switch Game.OrderManager from the shellmap to the new network connection when joining
|
||||
@@ -165,6 +168,7 @@ namespace OpenRA
|
||||
{
|
||||
return ModData.WidgetLoader.LoadWidget(new WidgetArgs(args)
|
||||
{
|
||||
{ "modData", ModData },
|
||||
{ "world", world },
|
||||
{ "orderManager", OrderManager },
|
||||
{ "worldRenderer", worldRenderer },
|
||||
@@ -179,7 +183,17 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public static event Action BeforeGameStart = () => { };
|
||||
internal static void StartGame(string mapUID, WorldType type)
|
||||
public static event Action AfterGameStart = () => { };
|
||||
internal static void StartGame(string uid, WorldType type)
|
||||
{
|
||||
var preview = ModData.MapCache[uid];
|
||||
if (preview.Status != MapStatus.Available)
|
||||
throw new InvalidDataException($"Invalid map uid: {uid}");
|
||||
|
||||
StartGame(preview.ToMap(), type);
|
||||
}
|
||||
|
||||
internal static void StartGame(Map map, WorldType type)
|
||||
{
|
||||
// Dispose of the old world before creating a new one.
|
||||
worldRenderer?.Dispose();
|
||||
@@ -188,7 +202,23 @@ namespace OpenRA
|
||||
BeforeGameStart();
|
||||
|
||||
using (new PerfTimer("NewWorld"))
|
||||
OrderManager.World = new World(mapUID, ModData, OrderManager, type);
|
||||
{
|
||||
ModData.PrepareMap(map);
|
||||
|
||||
// The depth buffer needs to be initialized with enough range to cover:
|
||||
// - the height of the screen
|
||||
// - the z-offset of tiles from MaxTerrainHeight below the bottom of the screen (pushed into view)
|
||||
// - additional z-offset from actors on top of MaxTerrainHeight terrain
|
||||
// - a small margin so that tiles rendered partially above the top edge of the screen aren't pushed behind the clip plane
|
||||
// We need an offset of mapGrid.MaximumTerrainHeight * mapGrid.TileSize.Height / 2 to cover the terrain height
|
||||
// and choose to use mapGrid.MaximumTerrainHeight * mapGrid.TileSize.Height / 4 for each of the actor and top-edge cases
|
||||
var margin = 0;
|
||||
if (map.Grid.EnableDepthBuffer)
|
||||
margin = map.Rules.TerrainInfo.TileSize.Height * map.Grid.MaximumTerrainHeight;
|
||||
|
||||
Renderer.SetDepthMargin(margin);
|
||||
OrderManager.World = new World(map, ModData, OrderManager, type);
|
||||
}
|
||||
|
||||
OrderManager.World.GameOver += FinishBenchmark;
|
||||
|
||||
@@ -222,6 +252,12 @@ namespace OpenRA
|
||||
// Much better to clean up now then to drop frames during gameplay for GC pauses.
|
||||
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
|
||||
GC.Collect();
|
||||
|
||||
// PostLoadComplete is designed for anything that should trigger at the very end of loading.
|
||||
// e.g. audio notifications that the game is starting.
|
||||
OrderManager.World.PostLoadComplete(worldRenderer);
|
||||
|
||||
AfterGameStart();
|
||||
}
|
||||
|
||||
public static void RestartGame()
|
||||
@@ -336,7 +372,7 @@ namespace OpenRA
|
||||
var explicitModPaths = Array.Empty<string>();
|
||||
if (modID != null && (File.Exists(modID) || Directory.Exists(modID)))
|
||||
{
|
||||
explicitModPaths = new[] { modID };
|
||||
explicitModPaths = [modID];
|
||||
modID = Path.GetFileNameWithoutExtension(modID);
|
||||
}
|
||||
|
||||
@@ -351,6 +387,48 @@ namespace OpenRA
|
||||
Log.AddChannel("nat", "nat.log");
|
||||
Log.AddChannel("client", "client.log");
|
||||
|
||||
Nat.Initialize();
|
||||
|
||||
var modSearchArg = args.GetValue("Engine.ModSearchPaths", null);
|
||||
var modSearchPaths = modSearchArg != null ?
|
||||
FieldLoader.GetValue<ImmutableArray<string>>("Engine.ModsPath", modSearchArg) :
|
||||
[Path.Combine(Platform.EngineDir, "mods")];
|
||||
|
||||
Mods = new InstalledMods(modSearchPaths, explicitModPaths);
|
||||
Console.WriteLine("Internal mods:");
|
||||
foreach (var mod in Mods)
|
||||
Console.WriteLine($"\t{mod.Key} ({mod.Value.Metadata.Version})");
|
||||
|
||||
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
|
||||
|
||||
ExternalMods = new ExternalMods();
|
||||
|
||||
if (modID == null)
|
||||
throw new InvalidOperationException("Game.Mod argument missing.");
|
||||
|
||||
if (Mods.TryGetValue(modID, out var manifest))
|
||||
{
|
||||
var launchPath = args.GetValue("Engine.LaunchPath", null);
|
||||
var launchArgs = new List<string>();
|
||||
|
||||
// Sanitize input from platform-specific launchers
|
||||
// Process.Start requires paths to not be quoted, even if they contain spaces
|
||||
if (launchPath != null && launchPath[0] == '"' && launchPath[^1] == '"')
|
||||
launchPath = launchPath[1..^1];
|
||||
|
||||
// Metadata registration requires an explicit launch path
|
||||
if (launchPath != null)
|
||||
ExternalMods.Register(Mods[modID], launchPath, launchArgs, ModRegistration.User);
|
||||
|
||||
ExternalMods.ClearInvalidRegistrations(ModRegistration.User);
|
||||
}
|
||||
else
|
||||
throw new InvalidOperationException($"Unknown or invalid mod '{modID}'.");
|
||||
|
||||
Console.WriteLine("External mods:");
|
||||
foreach (var mod in ExternalMods)
|
||||
Console.WriteLine($"\t{mod.Key} ({mod.Value.Version})");
|
||||
|
||||
var platforms = new[] { Settings.Game.Platform, "Default", null };
|
||||
foreach (var p in platforms)
|
||||
{
|
||||
@@ -360,23 +438,8 @@ namespace OpenRA
|
||||
Settings.Game.Platform = p;
|
||||
try
|
||||
{
|
||||
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + p + ".dll");
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
var loader = new AssemblyLoader(rendererPath);
|
||||
var platformType = loader.LoadDefaultAssembly().GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
|
||||
|
||||
#else
|
||||
// NOTE: This is currently the only use of System.Reflection in this file, so would give an unused using error if we import it above
|
||||
var assembly = System.Reflection.Assembly.LoadFile(rendererPath);
|
||||
var platformType = assembly.GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
|
||||
#endif
|
||||
|
||||
if (platformType == null)
|
||||
throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation.");
|
||||
|
||||
var platform = (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null);
|
||||
Renderer = new Renderer(platform, Settings.Graphics);
|
||||
var platform = CreatePlatform(p);
|
||||
Renderer = new Renderer(platform, Settings.Graphics, manifest.RendererConstants.VertexBatchSize);
|
||||
Sound = new Sound(platform, Settings.Sound);
|
||||
|
||||
break;
|
||||
@@ -392,47 +455,23 @@ namespace OpenRA
|
||||
}
|
||||
}
|
||||
|
||||
Nat.Initialize();
|
||||
|
||||
var modSearchArg = args.GetValue("Engine.ModSearchPaths", null);
|
||||
var modSearchPaths = modSearchArg != null ?
|
||||
FieldLoader.GetValue<string[]>("Engine.ModsPath", modSearchArg) :
|
||||
new[] { Path.Combine(Platform.EngineDir, "mods") };
|
||||
|
||||
Mods = new InstalledMods(modSearchPaths, explicitModPaths);
|
||||
Console.WriteLine("Internal mods:");
|
||||
foreach (var mod in Mods)
|
||||
Console.WriteLine($"\t{mod.Key}: {mod.Value.Metadata.Title} ({mod.Value.Metadata.Version})");
|
||||
|
||||
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
|
||||
|
||||
ExternalMods = new ExternalMods();
|
||||
|
||||
if (modID != null && Mods.TryGetValue(modID, out _))
|
||||
{
|
||||
var launchPath = args.GetValue("Engine.LaunchPath", null);
|
||||
var launchArgs = new List<string>();
|
||||
|
||||
// Sanitize input from platform-specific launchers
|
||||
// Process.Start requires paths to not be quoted, even if they contain spaces
|
||||
if (launchPath != null && launchPath.First() == '"' && launchPath.Last() == '"')
|
||||
launchPath = launchPath[1..^1];
|
||||
|
||||
// Metadata registration requires an explicit launch path
|
||||
if (launchPath != null)
|
||||
ExternalMods.Register(Mods[modID], launchPath, launchArgs, ModRegistration.User);
|
||||
|
||||
ExternalMods.ClearInvalidRegistrations(ModRegistration.User);
|
||||
}
|
||||
|
||||
Console.WriteLine("External mods:");
|
||||
foreach (var mod in ExternalMods)
|
||||
Console.WriteLine($"\t{mod.Key}: {mod.Value.Title} ({mod.Value.Version})");
|
||||
|
||||
InitializeMod(modID, args);
|
||||
InitializeMod(manifest, args);
|
||||
}
|
||||
|
||||
public static void InitializeMod(string mod, Arguments args)
|
||||
public static IPlatform CreatePlatform(string platformName)
|
||||
{
|
||||
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + platformName + ".dll");
|
||||
|
||||
var loader = new AssemblyLoader(rendererPath);
|
||||
var platformType = loader.LoadDefaultAssembly().GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
|
||||
|
||||
if (platformType == null)
|
||||
throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation.");
|
||||
|
||||
return (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null);
|
||||
}
|
||||
|
||||
public static void InitializeMod(Manifest manifest, Arguments args)
|
||||
{
|
||||
// Clear static state if we have switched mods
|
||||
LobbyInfoChanged = () => { };
|
||||
@@ -456,38 +495,29 @@ namespace OpenRA
|
||||
|
||||
ModData = null;
|
||||
|
||||
if (mod == null)
|
||||
throw new InvalidOperationException("Game.Mod argument missing.");
|
||||
|
||||
if (!Mods.ContainsKey(mod))
|
||||
throw new InvalidOperationException($"Unknown or invalid mod '{mod}'.");
|
||||
|
||||
Console.WriteLine($"Loading mod: {mod}");
|
||||
Console.WriteLine($"Loading mod: {manifest.Id}");
|
||||
|
||||
Sound.StopVideo();
|
||||
|
||||
ModData = new ModData(Mods[mod], Mods, true);
|
||||
ModData = new ModData(manifest, Mods, true);
|
||||
|
||||
LocalPlayerProfile = new LocalPlayerProfile(Path.Combine(Platform.SupportDir, Settings.Game.AuthProfile), ModData.Manifest.Get<PlayerDatabase>());
|
||||
LocalPlayerProfile = new LocalPlayerProfile(Path.Combine(Platform.SupportDir, Settings.Game.AuthProfile), ModData.GetOrCreate<PlayerDatabase>());
|
||||
|
||||
if (!ModData.LoadScreen.BeforeLoad())
|
||||
if (!ModData.LoadScreen.BeforeLoad(ModData))
|
||||
return;
|
||||
|
||||
ModData.InitializeLoaders(ModData.DefaultFileSystem);
|
||||
Renderer.InitializeFonts(ModData);
|
||||
|
||||
using (new PerfTimer("LoadMaps"))
|
||||
ModData.MapCache.LoadMaps();
|
||||
|
||||
var grid = ModData.Manifest.Contains<MapGrid>() ? ModData.Manifest.Get<MapGrid>() : null;
|
||||
Renderer.InitializeDepthBuffer(grid);
|
||||
ModData.MapCache.LoadMaps(ModData);
|
||||
|
||||
Cursor?.Dispose();
|
||||
Cursor = new CursorManager(ModData.CursorProvider);
|
||||
Cursor = new CursorManager(ModData);
|
||||
|
||||
var metadata = ModData.Manifest.Metadata;
|
||||
if (!string.IsNullOrEmpty(metadata.WindowTitle))
|
||||
Renderer.Window.SetWindowTitle(metadata.WindowTitle);
|
||||
if (!string.IsNullOrEmpty(metadata.WindowTitleTranslated))
|
||||
Renderer.Window.SetWindowTitle(metadata.WindowTitleTranslated);
|
||||
|
||||
PerfHistory.Items["render"].HasNormalTick = false;
|
||||
PerfHistory.Items["batches"].HasNormalTick = false;
|
||||
@@ -501,10 +531,16 @@ namespace OpenRA
|
||||
ModData.LoadScreen.StartGame(args);
|
||||
}
|
||||
|
||||
public static void LoadEditor(string mapUid)
|
||||
public static void LoadEditor(string uid)
|
||||
{
|
||||
JoinLocal();
|
||||
StartGame(mapUid, WorldType.Editor);
|
||||
StartGame(uid, WorldType.Editor);
|
||||
}
|
||||
|
||||
public static void LoadEditor(Map map)
|
||||
{
|
||||
JoinLocal();
|
||||
StartGame(map, WorldType.Editor);
|
||||
}
|
||||
|
||||
public static void LoadShellMap()
|
||||
@@ -523,10 +559,11 @@ namespace OpenRA
|
||||
.Where(m => m.Status == MapStatus.Available && m.Visibility.HasFlag(MapVisibility.Shellmap))
|
||||
.Select(m => m.Uid);
|
||||
|
||||
if (!shellmaps.Any())
|
||||
var shellmap = shellmaps.RandomOrDefault(CosmeticRandom);
|
||||
if (shellmap == null)
|
||||
throw new InvalidDataException("No valid shellmaps available");
|
||||
|
||||
return shellmaps.Random(CosmeticRandom);
|
||||
return shellmap;
|
||||
}
|
||||
|
||||
public static void SwitchToExternalMod(ExternalMod mod, string[] launchArguments = null, Action onFailed = null)
|
||||
@@ -577,11 +614,11 @@ namespace OpenRA
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
var filename = TimestampedFilename(true);
|
||||
var path = Path.Combine(directory, string.Concat(filename, ".png"));
|
||||
var path = Path.Combine(directory, $"{filename}.png");
|
||||
Log.Write("debug", "Taking screenshot " + path);
|
||||
|
||||
Renderer.SaveScreenshot(path);
|
||||
TextNotificationsManager.Debug(TranslationProvider.GetString(SavedScreenshot, Translation.Arguments("filename", filename)));
|
||||
TextNotificationsManager.Debug(FluentProvider.GetMessage(SavedScreenshot, "filename", filename));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,7 +637,10 @@ namespace OpenRA
|
||||
|
||||
if (orderManager.LastTickTime.ShouldAdvance(tick))
|
||||
{
|
||||
using (new PerfSample("tick_time"))
|
||||
if (orderManager.GameStarted && orderManager.LocalFrameNumber == 0)
|
||||
PerfHistory.Reset(); // Remove history that occurred whilst the new game was loading.
|
||||
|
||||
using (var sample = new PerfSample("tick_time"))
|
||||
{
|
||||
orderManager.LastTickTime.AdvanceTickTime(tick);
|
||||
|
||||
@@ -609,7 +649,11 @@ namespace OpenRA
|
||||
Sync.RunUnsynced(world, orderManager.TickImmediate);
|
||||
|
||||
if (world == null)
|
||||
{
|
||||
if (orderManager.GameStarted)
|
||||
PerfHistory.Reset(); // Remove old history when a new game starts.
|
||||
return;
|
||||
}
|
||||
|
||||
if (orderManager.TryTick())
|
||||
{
|
||||
@@ -617,7 +661,7 @@ namespace OpenRA
|
||||
|
||||
world.Tick();
|
||||
|
||||
PerfHistory.Tick();
|
||||
PerfHistory.Tick(!world.Paused);
|
||||
}
|
||||
|
||||
// Wait until we have done our first world Tick before TickRendering
|
||||
@@ -663,7 +707,7 @@ namespace OpenRA
|
||||
// Prepare renderables (i.e. render voxels) before calling BeginFrame
|
||||
using (new PerfSample("render_prepare"))
|
||||
{
|
||||
Renderer.WorldModelRenderer.BeginFrame();
|
||||
worldRenderer?.BeginFrame();
|
||||
|
||||
// World rendering is disabled while the loading screen is displayed
|
||||
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
|
||||
@@ -673,7 +717,7 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
Ui.PrepareRenderables();
|
||||
Renderer.WorldModelRenderer.EndFrame();
|
||||
worldRenderer?.EndFrame();
|
||||
}
|
||||
|
||||
// worldRenderer is null during the initial install/download screen
|
||||
@@ -681,7 +725,7 @@ namespace OpenRA
|
||||
// Use worldRenderer.World instead of OrderManager.World to avoid a rendering mismatch while processing orders
|
||||
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
|
||||
{
|
||||
Renderer.BeginWorld(worldRenderer.Viewport.Rectangle);
|
||||
Renderer.BeginWorld(worldRenderer.Viewport.CenterLocation, worldRenderer.Viewport.ViewportSize);
|
||||
Sound.SetListenerPosition(worldRenderer.Viewport.CenterPosition);
|
||||
using (new PerfSample("render_world"))
|
||||
worldRenderer.Draw();
|
||||
@@ -696,15 +740,12 @@ namespace OpenRA
|
||||
|
||||
Ui.Draw();
|
||||
|
||||
if (ModData != null && ModData.CursorProvider != null)
|
||||
if (HideCursor)
|
||||
Cursor?.SetCursor(null);
|
||||
else
|
||||
{
|
||||
if (HideCursor)
|
||||
Cursor.SetCursor(null);
|
||||
else
|
||||
{
|
||||
Cursor.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default");
|
||||
Cursor.Render(Renderer);
|
||||
}
|
||||
Cursor?.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default");
|
||||
Cursor?.Render(Renderer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,12 +759,13 @@ namespace OpenRA
|
||||
}
|
||||
}
|
||||
|
||||
PerfHistory.Items["render"].Tick();
|
||||
PerfHistory.Items["batches"].Tick();
|
||||
PerfHistory.Items["render_world"].Tick();
|
||||
PerfHistory.Items["render_widgets"].Tick();
|
||||
PerfHistory.Items["render_flip"].Tick();
|
||||
PerfHistory.Items["terrain_lighting"].Tick();
|
||||
var isActive = !(worldRenderer?.World.Paused ?? true);
|
||||
PerfHistory.Items["render"].Tick(isActive);
|
||||
PerfHistory.Items["batches"].Tick(isActive);
|
||||
PerfHistory.Items["render_world"].Tick(isActive);
|
||||
PerfHistory.Items["render_widgets"].Tick(isActive);
|
||||
PerfHistory.Items["render_flip"].Tick(isActive);
|
||||
PerfHistory.Items["terrain_lighting"].Tick(isActive);
|
||||
}
|
||||
|
||||
static void Loop()
|
||||
@@ -777,7 +819,7 @@ namespace OpenRA
|
||||
var logicWorld = worldRenderer?.World;
|
||||
|
||||
// ReplayTimestep = 0 means the replay is paused: we need to keep logicInterval as UI.Timestep to avoid breakage
|
||||
if (logicWorld != null && !(logicWorld.IsReplay && logicWorld.ReplayTimestep == 0))
|
||||
if (logicWorld != null && (!logicWorld.IsReplay || logicWorld.ReplayTimestep != 0))
|
||||
logicInterval = logicWorld == OrderManager.World ? OrderManager.SuggestedTimestep : logicWorld.Timestep;
|
||||
|
||||
// Ideal time between screen updates
|
||||
@@ -820,33 +862,45 @@ namespace OpenRA
|
||||
|
||||
var haveSomeTimeUntilNextLogic = now < nextLogic;
|
||||
var isTimeToRender = now >= nextRender;
|
||||
if (!Renderer.WindowIsSuspended && ((isTimeToRender && haveSomeTimeUntilNextLogic) || forceRender))
|
||||
if (!Renderer.WindowIsSuspended)
|
||||
{
|
||||
nextRender = now + renderInterval;
|
||||
if (isTimeToRender || forceRender)
|
||||
{
|
||||
if (haveSomeTimeUntilNextLogic || forceRender)
|
||||
RenderTick();
|
||||
|
||||
// Pick the minimum allowed FPS (the lower between 'minReplayFPS'
|
||||
// and the user's max frame rate) and convert it to maximum time
|
||||
// allowed between screen updates.
|
||||
// We do this before rendering to include the time rendering takes
|
||||
// in this interval.
|
||||
var maxRenderInterval = Math.Max(1000 / MinReplayFps, renderInterval);
|
||||
forcedNextRender = now + maxRenderInterval;
|
||||
nextRender = now + renderInterval;
|
||||
|
||||
RenderTick();
|
||||
renderBeforeNextTick = false;
|
||||
// Pick the minimum allowed FPS (the lower between 'minReplayFPS'
|
||||
// and the user's max frame rate) and convert it to maximum time
|
||||
// allowed between screen updates.
|
||||
// We do this before rendering to include the time rendering takes
|
||||
// in this interval.
|
||||
var maxRenderInterval = Math.Max(1000 / MinReplayFps, renderInterval);
|
||||
forcedNextRender = now + maxRenderInterval;
|
||||
|
||||
renderBeforeNextTick = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate a render tick if it was time to render but we skip actually rendering
|
||||
if (Renderer.WindowIsSuspended && isTimeToRender)
|
||||
else
|
||||
{
|
||||
// Make sure that nextUpdate is set to a proper minimum interval
|
||||
nextRender = now + renderInterval;
|
||||
// Simulate a render tick if it was time to render but we skip actually rendering
|
||||
if (isTimeToRender || forceRender)
|
||||
{
|
||||
// Make sure that nextUpdate is set to a proper minimum interval
|
||||
nextRender = now + renderInterval;
|
||||
|
||||
// Still process SDL events to allow a restore to come through
|
||||
Renderer.Window.PumpInput(new NullInputHandler());
|
||||
// Still process SDL events to allow a restore to come through
|
||||
Renderer.Window.PumpInput(new NullInputHandler());
|
||||
|
||||
// Ensure that we still logic tick despite not rendering
|
||||
renderBeforeNextTick = false;
|
||||
// Ensure that we still logic tick despite not rendering
|
||||
renderBeforeNextTick = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Avoid busy wait.
|
||||
Thread.Sleep((int)(nextRender - now));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -912,21 +966,22 @@ namespace OpenRA
|
||||
{
|
||||
var endpoints = new List<IPEndPoint>
|
||||
{
|
||||
new IPEndPoint(IPAddress.IPv6Any, settings.ListenPort),
|
||||
new IPEndPoint(IPAddress.Any, settings.ListenPort)
|
||||
new(IPAddress.IPv6Any, settings.ListenPort),
|
||||
new(IPAddress.Any, settings.ListenPort)
|
||||
};
|
||||
server = new Server.Server(endpoints, settings, ModData, ServerType.Multiplayer);
|
||||
|
||||
return server.GetEndpointForLocalConnection();
|
||||
}
|
||||
|
||||
public static ConnectionTarget CreateLocalServer(string map)
|
||||
public static ConnectionTarget CreateLocalServer(string map, bool isSkirmish = false)
|
||||
{
|
||||
var settings = new ServerSettings()
|
||||
{
|
||||
Name = "Skirmish Game",
|
||||
Map = map,
|
||||
AdvertiseOnline = false
|
||||
AdvertiseOnline = false,
|
||||
AdvertiseOnLocalNetwork = !isSkirmish
|
||||
};
|
||||
|
||||
// Always connect to local games using the same loopback connection
|
||||
@@ -934,9 +989,9 @@ namespace OpenRA
|
||||
// This would break the Restart button, which relies on the PlayerIndex always being the same for local servers
|
||||
var endpoints = new List<IPEndPoint>
|
||||
{
|
||||
new IPEndPoint(IPAddress.Loopback, 0)
|
||||
new(IPAddress.Loopback, 0)
|
||||
};
|
||||
server = new Server.Server(endpoints, settings, ModData, ServerType.Local);
|
||||
server = new Server.Server(endpoints, settings, ModData, isSkirmish ? ServerType.Skirmish : ServerType.Local);
|
||||
|
||||
return server.GetEndpointForLocalConnection();
|
||||
}
|
||||
@@ -964,7 +1019,7 @@ namespace OpenRA
|
||||
Order.Command($"state {Session.ClientState.Ready}")
|
||||
};
|
||||
|
||||
var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.Package.Name) == launchMap);
|
||||
var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.Path) == launchMap);
|
||||
if (map == null)
|
||||
throw new ArgumentException($"Could not find map '{launchMap}'.");
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Network;
|
||||
@@ -19,6 +20,9 @@ namespace OpenRA
|
||||
{
|
||||
public class GameInformation
|
||||
{
|
||||
[FluentReference("name", "number")]
|
||||
const string EnumeratedBotName = "enumerated-bot-name";
|
||||
|
||||
public string Mod;
|
||||
public string Version;
|
||||
|
||||
@@ -36,26 +40,41 @@ namespace OpenRA
|
||||
public TimeSpan Duration => EndTimeUtc > StartTimeUtc ? EndTimeUtc - StartTimeUtc : TimeSpan.Zero;
|
||||
|
||||
public IList<Player> Players { get; }
|
||||
public HashSet<int> DisabledSpawnPoints = new();
|
||||
public MapPreview MapPreview => Game.ModData.MapCache[MapUid];
|
||||
public FrozenSet<int> DisabledSpawnPoints = FrozenSet<int>.Empty;
|
||||
|
||||
public IEnumerable<Player> HumanPlayers { get { return Players.Where(p => p.IsHuman); } }
|
||||
public bool IsSinglePlayer => HumanPlayers.Count() == 1;
|
||||
|
||||
[FieldLoader.Ignore]
|
||||
public MapGenerationArgs MapGenerationArgs;
|
||||
|
||||
readonly Dictionary<OpenRA.Player, Player> playersByRuntime;
|
||||
|
||||
public GameInformation()
|
||||
{
|
||||
Players = new List<Player>();
|
||||
playersByRuntime = new Dictionary<OpenRA.Player, Player>();
|
||||
Players = [];
|
||||
playersByRuntime = [];
|
||||
}
|
||||
|
||||
public static GameInformation Deserialize(string data)
|
||||
public MapPreview GetMapPreview(ModData modData)
|
||||
{
|
||||
var preview = modData.MapCache[MapUid];
|
||||
if (preview.Status != MapStatus.Available && MapGenerationArgs != null)
|
||||
{
|
||||
preview.UpdateFromGenerationArgs(MapGenerationArgs);
|
||||
preview.Generate();
|
||||
}
|
||||
|
||||
return preview;
|
||||
}
|
||||
|
||||
public static GameInformation Deserialize(string data, string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = new GameInformation();
|
||||
|
||||
var nodes = MiniYaml.FromString(data);
|
||||
var nodes = MiniYaml.FromString(data, path);
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var keyParts = node.Key.Split('@');
|
||||
@@ -69,6 +88,10 @@ namespace OpenRA
|
||||
case "Player":
|
||||
info.Players.Add(FieldLoader.Load<Player>(node.Value));
|
||||
break;
|
||||
|
||||
case "MapGenerationArgs":
|
||||
info.MapGenerationArgs = FieldLoader.Load<MapGenerationArgs>(node.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,23 +108,24 @@ namespace OpenRA
|
||||
{
|
||||
var nodes = new List<MiniYamlNode>
|
||||
{
|
||||
new MiniYamlNode("Root", FieldSaver.Save(this))
|
||||
new("Root", FieldSaver.Save(this))
|
||||
};
|
||||
|
||||
for (var i = 0; i < Players.Count; i++)
|
||||
nodes.Add(new MiniYamlNode($"Player@{i}", FieldSaver.Save(Players[i])));
|
||||
|
||||
if (MapGenerationArgs != null)
|
||||
nodes.Add(new MiniYamlNode("MapGenerationArgs", new MiniYaml("", MapGenerationArgs.Serialize())));
|
||||
|
||||
return nodes.WriteToString();
|
||||
}
|
||||
|
||||
/// <summary>Adds the player information at start-up.</summary>
|
||||
public void AddPlayer(OpenRA.Player runtimePlayer, Session lobbyInfo)
|
||||
{
|
||||
if (runtimePlayer == null)
|
||||
throw new ArgumentNullException(nameof(runtimePlayer));
|
||||
ArgumentNullException.ThrowIfNull(runtimePlayer);
|
||||
|
||||
if (lobbyInfo == null)
|
||||
throw new ArgumentNullException(nameof(lobbyInfo));
|
||||
ArgumentNullException.ThrowIfNull(lobbyInfo);
|
||||
|
||||
// We don't care about spectators and map players
|
||||
if (runtimePlayer.NonCombatant || !runtimePlayer.Playable)
|
||||
@@ -118,11 +142,12 @@ namespace OpenRA
|
||||
Name = runtimePlayer.PlayerName,
|
||||
IsHuman = !runtimePlayer.IsBot,
|
||||
IsBot = runtimePlayer.IsBot,
|
||||
BotType = runtimePlayer.BotType,
|
||||
FactionName = runtimePlayer.Faction.Name,
|
||||
FactionId = runtimePlayer.Faction.InternalName,
|
||||
DisplayFactionName = runtimePlayer.DisplayFaction.Name,
|
||||
DisplayFactionId = runtimePlayer.DisplayFaction.InternalName,
|
||||
Color = runtimePlayer.Color,
|
||||
Color = OpenRA.Player.GetColor(runtimePlayer),
|
||||
Team = client.Team,
|
||||
Handicap = client.Handicap,
|
||||
SpawnPoint = runtimePlayer.SpawnPoint,
|
||||
@@ -143,6 +168,19 @@ namespace OpenRA
|
||||
return player;
|
||||
}
|
||||
|
||||
public string ResolvedPlayerName(Player player)
|
||||
{
|
||||
if (player.IsBot)
|
||||
{
|
||||
var number = Players.Where(p => p.BotType == player.BotType).ToList().IndexOf(player) + 1;
|
||||
return FluentProvider.GetMessage(EnumeratedBotName,
|
||||
"name", FluentProvider.GetMessage(player.Name),
|
||||
"number", number);
|
||||
}
|
||||
|
||||
return player.Name;
|
||||
}
|
||||
|
||||
public class Player
|
||||
{
|
||||
#region Start-up information
|
||||
@@ -153,6 +191,7 @@ namespace OpenRA
|
||||
public string Name;
|
||||
public bool IsHuman;
|
||||
public bool IsBot;
|
||||
public string BotType;
|
||||
|
||||
/// <summary>The faction's display name.</summary>
|
||||
public string FactionName;
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace OpenRA
|
||||
/// </summary>
|
||||
public class ActorInfo
|
||||
{
|
||||
public const string AbstractActorPrefix = "^";
|
||||
public const char AbstractActorPrefix = '^';
|
||||
public const char TraitInstanceSeparator = '@';
|
||||
|
||||
/// <summary>
|
||||
@@ -32,8 +32,8 @@ namespace OpenRA
|
||||
/// You can remove inherited traits by adding a - in front of them as in -TraitName: to inherit everything, but this trait.
|
||||
/// </summary>
|
||||
public readonly string Name;
|
||||
readonly TypeDictionary traits = new();
|
||||
List<TraitInfo> constructOrderCache = null;
|
||||
readonly TypeDictionary traits = [];
|
||||
TraitInfo[] constructOrderCache = null;
|
||||
|
||||
public ActorInfo(ObjectCreator creator, string name, MiniYaml node)
|
||||
{
|
||||
@@ -130,6 +130,7 @@ namespace OpenRA
|
||||
|
||||
// Continue resolving traits as long as possible.
|
||||
// Each time we resolve some traits, this means dependencies for other traits may then be possible to satisfy in the next pass.
|
||||
#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection
|
||||
var readyToResolve = more.ToList();
|
||||
while (readyToResolve.Count != 0)
|
||||
{
|
||||
@@ -138,6 +139,7 @@ namespace OpenRA
|
||||
readyToResolve.Clear();
|
||||
readyToResolve.AddRange(more);
|
||||
}
|
||||
#pragma warning restore CA1851
|
||||
|
||||
if (unresolved.Count != 0)
|
||||
{
|
||||
@@ -160,7 +162,7 @@ namespace OpenRA
|
||||
throw new YamlException(exceptionString);
|
||||
}
|
||||
|
||||
constructOrderCache = resolved.Select(r => r.Trait).ToList();
|
||||
constructOrderCache = resolved.Select(r => r.Trait).ToArray();
|
||||
return constructOrderCache;
|
||||
}
|
||||
|
||||
@@ -185,7 +187,7 @@ namespace OpenRA
|
||||
public bool HasTraitInfo<T>() where T : ITraitInfoInterface { return traits.Contains<T>(); }
|
||||
public T TraitInfo<T>() where T : ITraitInfoInterface { return traits.Get<T>(); }
|
||||
public T TraitInfoOrDefault<T>() where T : ITraitInfoInterface { return traits.GetOrDefault<T>(); }
|
||||
public IEnumerable<T> TraitInfos<T>() where T : ITraitInfoInterface { return traits.WithInterface<T>(); }
|
||||
public IReadOnlyCollection<T> TraitInfos<T>() where T : ITraitInfoInterface { return traits.WithInterface<T>(); }
|
||||
|
||||
public BitSet<TargetableType> GetAllTargetTypes()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using OpenRA.FileSystem;
|
||||
@@ -124,7 +125,7 @@ namespace OpenRA
|
||||
{
|
||||
var actors = MergeOrDefault("Manifest,Rules", fs, m.Rules, null, null,
|
||||
k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value),
|
||||
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal));
|
||||
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix));
|
||||
|
||||
var weapons = MergeOrDefault("Manifest,Weapons", fs, m.Weapons, null, null,
|
||||
k => new WeaponInfo(k.Value));
|
||||
@@ -182,7 +183,7 @@ namespace OpenRA
|
||||
{
|
||||
var actors = MergeOrDefault("Rules", fileSystem, m.Rules, mapRules, dr.Actors,
|
||||
k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value),
|
||||
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal));
|
||||
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix));
|
||||
|
||||
var weapons = MergeOrDefault("Weapons", fileSystem, m.Weapons, mapWeapons, dr.Weapons,
|
||||
k => new WeaponInfo(k.Value));
|
||||
@@ -226,10 +227,10 @@ namespace OpenRA
|
||||
|
||||
static bool AnyCustomYaml(MiniYaml yaml)
|
||||
{
|
||||
return yaml != null && (yaml.Value != null || yaml.Nodes.Count > 0);
|
||||
return yaml != null && (yaml.Value != null || yaml.Nodes.Length > 0);
|
||||
}
|
||||
|
||||
static bool AnyFlaggedTraits(ModData modData, List<MiniYamlNode> actors)
|
||||
static bool AnyFlaggedTraits(ModData modData, IEnumerable<MiniYamlNode> actors)
|
||||
{
|
||||
foreach (var actorNode in actors)
|
||||
{
|
||||
@@ -260,18 +261,18 @@ namespace OpenRA
|
||||
return true;
|
||||
|
||||
// Any trait overrides that aren't explicitly whitelisted are flagged
|
||||
if (mapRules != null)
|
||||
{
|
||||
if (AnyFlaggedTraits(modData, mapRules.Nodes))
|
||||
return true;
|
||||
if (mapRules == null)
|
||||
return false;
|
||||
|
||||
if (mapRules.Value != null)
|
||||
{
|
||||
var mapFiles = FieldLoader.GetValue<string[]>("value", mapRules.Value);
|
||||
foreach (var f in mapFiles)
|
||||
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
|
||||
return true;
|
||||
}
|
||||
if (AnyFlaggedTraits(modData, mapRules.Nodes))
|
||||
return true;
|
||||
|
||||
if (mapRules.Value != null)
|
||||
{
|
||||
var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", mapRules.Value);
|
||||
foreach (var f in mapFiles)
|
||||
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -10,55 +10,56 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace OpenRA.GameRules
|
||||
{
|
||||
public class SoundInfo
|
||||
{
|
||||
public readonly Dictionary<string, string[]> Variants = new();
|
||||
public readonly Dictionary<string, string[]> Prefixes = new();
|
||||
public readonly Dictionary<string, string[]> Voices = new();
|
||||
public readonly Dictionary<string, string[]> Notifications = new();
|
||||
public readonly FrozenDictionary<string, ImmutableArray<string>> Variants = FrozenDictionary<string, ImmutableArray<string>>.Empty;
|
||||
public readonly FrozenDictionary<string, ImmutableArray<string>> Prefixes = FrozenDictionary<string, ImmutableArray<string>>.Empty;
|
||||
public readonly FrozenDictionary<string, ImmutableArray<string>> Voices = FrozenDictionary<string, ImmutableArray<string>>.Empty;
|
||||
public readonly FrozenDictionary<string, ImmutableArray<string>> Notifications = FrozenDictionary<string, ImmutableArray<string>>.Empty;
|
||||
public readonly string DefaultVariant = ".aud";
|
||||
public readonly string DefaultPrefix = "";
|
||||
public readonly HashSet<string> DisableVariants = new();
|
||||
public readonly HashSet<string> DisablePrefixes = new();
|
||||
public readonly FrozenSet<string> DisableVariants = FrozenSet<string>.Empty;
|
||||
public readonly FrozenSet<string> DisablePrefixes = FrozenSet<string>.Empty;
|
||||
|
||||
public readonly Lazy<Dictionary<string, SoundPool>> VoicePools;
|
||||
public readonly Lazy<Dictionary<string, SoundPool>> NotificationsPools;
|
||||
public readonly Lazy<FrozenDictionary<string, SoundPool>> VoicePools;
|
||||
public readonly Lazy<FrozenDictionary<string, SoundPool>> NotificationsPools;
|
||||
|
||||
public SoundInfo(MiniYaml y)
|
||||
{
|
||||
FieldLoader.Load(this, y);
|
||||
|
||||
VoicePools = Exts.Lazy(() => Voices.ToDictionary(a => a.Key, a => new SoundPool(1f, SoundPool.DefaultInterruptType, a.Value)));
|
||||
VoicePools = Exts.Lazy(() => Voices.ToFrozenDictionary(a => a.Key, a => new SoundPool(1f, SoundPool.DefaultInterruptType, a.Value)));
|
||||
NotificationsPools = Exts.Lazy(() => ParseSoundPool(y, "Notifications"));
|
||||
}
|
||||
|
||||
static Dictionary<string, SoundPool> ParseSoundPool(MiniYaml y, string key)
|
||||
static FrozenDictionary<string, SoundPool> ParseSoundPool(MiniYaml y, string key)
|
||||
{
|
||||
var ret = new Dictionary<string, SoundPool>();
|
||||
var classifiction = y.Nodes.First(x => x.Key == key);
|
||||
var classifiction = y.NodeWithKey(key);
|
||||
var ret = new Dictionary<string, SoundPool>(classifiction.Value.Nodes.Length);
|
||||
foreach (var t in classifiction.Value.Nodes)
|
||||
{
|
||||
var volumeModifier = 1f;
|
||||
var volumeModifierNode = t.Value.Nodes.FirstOrDefault(x => x.Key == nameof(SoundPool.VolumeModifier));
|
||||
var volumeModifierNode = t.Value.NodeWithKeyOrDefault(nameof(SoundPool.VolumeModifier));
|
||||
if (volumeModifierNode != null)
|
||||
volumeModifier = FieldLoader.GetValue<float>(volumeModifierNode.Key, volumeModifierNode.Value.Value);
|
||||
|
||||
var interruptType = SoundPool.DefaultInterruptType;
|
||||
var interruptTypeNode = t.Value.Nodes.FirstOrDefault(x => x.Key == nameof(SoundPool.InterruptType));
|
||||
var interruptTypeNode = t.Value.NodeWithKeyOrDefault(nameof(SoundPool.InterruptType));
|
||||
if (interruptTypeNode != null)
|
||||
interruptType = FieldLoader.GetValue<SoundPool.InterruptType>(interruptTypeNode.Key, interruptTypeNode.Value.Value);
|
||||
|
||||
var names = FieldLoader.GetValue<string[]>(t.Key, t.Value.Value);
|
||||
var names = FieldLoader.GetValue<ImmutableArray<string>>(t.Key, t.Value.Value);
|
||||
var sp = new SoundPool(volumeModifier, interruptType, names);
|
||||
ret.Add(t.Key, sp);
|
||||
}
|
||||
|
||||
return ret;
|
||||
return ret.ToFrozenDictionary();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,10 +69,10 @@ namespace OpenRA.GameRules
|
||||
public const InterruptType DefaultInterruptType = InterruptType.DoNotPlay;
|
||||
public readonly float VolumeModifier;
|
||||
public readonly InterruptType Type;
|
||||
readonly string[] clips;
|
||||
readonly List<string> liveclips = new();
|
||||
readonly ImmutableArray<string> clips;
|
||||
readonly List<string> liveclips = [];
|
||||
|
||||
public SoundPool(float volumeModifier, InterruptType interruptType, params string[] clips)
|
||||
public SoundPool(float volumeModifier, InterruptType interruptType, ImmutableArray<string> clips)
|
||||
{
|
||||
VolumeModifier = volumeModifier;
|
||||
Type = interruptType;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using OpenRA.Effects;
|
||||
using OpenRA.Primitives;
|
||||
@@ -36,7 +37,7 @@ namespace OpenRA.GameRules
|
||||
public class WarheadArgs
|
||||
{
|
||||
public WeaponInfo Weapon;
|
||||
public int[] DamageModifiers = Array.Empty<int>();
|
||||
public int[] DamageModifiers = [];
|
||||
public WPos? Source;
|
||||
public WRot ImpactOrientation;
|
||||
public WPos ImpactPosition;
|
||||
@@ -82,13 +83,13 @@ namespace OpenRA.GameRules
|
||||
public readonly WVec FollowingBurstTargetOffset = WVec.Zero;
|
||||
|
||||
[Desc("The sound played each time the weapon is fired.")]
|
||||
public readonly string[] Report = null;
|
||||
public readonly ImmutableArray<string> Report = default;
|
||||
|
||||
[Desc("Sound played only on first burst in a salvo.")]
|
||||
public readonly string[] StartBurstReport = null;
|
||||
public readonly ImmutableArray<string> StartBurstReport = default;
|
||||
|
||||
[Desc("The sound played when the weapon is reloaded.")]
|
||||
public readonly string[] AfterFireSound = null;
|
||||
public readonly ImmutableArray<string> AfterFireSound = default;
|
||||
|
||||
[Desc("Delay in ticks to play reloading sound.")]
|
||||
public readonly int AfterFireSoundDelay = 0;
|
||||
@@ -116,7 +117,7 @@ namespace OpenRA.GameRules
|
||||
|
||||
[Desc("Delay in ticks between firing shots from the same ammo magazine. If one entry, it will be used for all bursts.",
|
||||
"If multiple entries, their number needs to match Burst - 1.")]
|
||||
public readonly int[] BurstDelays = { 5 };
|
||||
public readonly ImmutableArray<int> BurstDelays = [5];
|
||||
|
||||
[Desc("The minimum range the weapon can fire.")]
|
||||
public readonly WDist MinRange = WDist.Zero;
|
||||
@@ -128,7 +129,7 @@ namespace OpenRA.GameRules
|
||||
public readonly IProjectileInfo Projectile;
|
||||
|
||||
[FieldLoader.LoadUsing(nameof(LoadWarheads))]
|
||||
public readonly List<IWarhead> Warheads = new();
|
||||
public readonly ImmutableArray<IWarhead> Warheads = [];
|
||||
|
||||
/// <summary>
|
||||
/// This constructor is used solely for documentation generation.
|
||||
@@ -139,13 +140,14 @@ namespace OpenRA.GameRules
|
||||
{
|
||||
// Resolve any weapon-level yaml inheritance or removals
|
||||
// HACK: The "Defaults" sequence syntax prevents us from doing this generally during yaml parsing
|
||||
content.Nodes = MiniYaml.Merge(new[] { content.Nodes });
|
||||
content = content.WithNodes(MiniYaml.Merge([content.Nodes]));
|
||||
FieldLoader.Load(this, content);
|
||||
}
|
||||
|
||||
static object LoadProjectile(MiniYaml yaml)
|
||||
{
|
||||
if (!yaml.ToDictionary().TryGetValue("Projectile", out var proj))
|
||||
var proj = yaml.NodeWithKeyOrDefault("Projectile")?.Value;
|
||||
if (proj == null)
|
||||
return null;
|
||||
|
||||
var ret = Game.CreateObject<IProjectileInfo>(proj.Value + "Info");
|
||||
@@ -159,7 +161,7 @@ namespace OpenRA.GameRules
|
||||
static object LoadWarheads(MiniYaml yaml)
|
||||
{
|
||||
var retList = new List<IWarhead>();
|
||||
foreach (var node in yaml.Nodes.Where(n => n.Key.StartsWith("Warhead")))
|
||||
foreach (var node in yaml.Nodes.Where(n => n.Key.StartsWith("Warhead", StringComparison.Ordinal)))
|
||||
{
|
||||
var ret = Game.CreateObject<IWarhead>(node.Value.Value + "Warhead");
|
||||
if (ret == null)
|
||||
@@ -169,7 +171,7 @@ namespace OpenRA.GameRules
|
||||
retList.Add(ret);
|
||||
}
|
||||
|
||||
return retList;
|
||||
return retList.ToImmutableArray();
|
||||
}
|
||||
|
||||
public bool IsValidTarget(BitSet<TargetableType> targetTypes)
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public class GameSpeed
|
||||
{
|
||||
[TranslationReference]
|
||||
[FluentReference]
|
||||
[FieldLoader.Require]
|
||||
public readonly string Name;
|
||||
|
||||
@@ -32,13 +32,14 @@ namespace OpenRA
|
||||
[FieldLoader.Require]
|
||||
public readonly string DefaultSpeed;
|
||||
|
||||
[IncludeFluentReferences(LintDictionaryReference.Values)]
|
||||
[FieldLoader.LoadUsing(nameof(LoadSpeeds))]
|
||||
public readonly Dictionary<string, GameSpeed> Speeds;
|
||||
|
||||
static object LoadSpeeds(MiniYaml y)
|
||||
{
|
||||
var ret = new Dictionary<string, GameSpeed>();
|
||||
var speedsNode = y.Nodes.FirstOrDefault(n => n.Key == "Speeds");
|
||||
var speedsNode = y.NodeWithKeyOrDefault("Speeds");
|
||||
if (speedsNode == null)
|
||||
throw new YamlException("Error parsing GameSpeeds: Missing Speeds node!");
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Support;
|
||||
@@ -22,6 +23,7 @@ namespace OpenRA.Graphics
|
||||
public string Name { get; private set; }
|
||||
public bool IsDecoration { get; set; }
|
||||
|
||||
readonly Map map;
|
||||
readonly SequenceSet sequences;
|
||||
readonly Func<WAngle> facingFunc;
|
||||
readonly Func<bool> paused;
|
||||
@@ -43,6 +45,7 @@ namespace OpenRA.Graphics
|
||||
|
||||
public Animation(World world, string name, Func<WAngle> facingFunc, Func<bool> paused)
|
||||
{
|
||||
map = world.Map;
|
||||
sequences = world.Map.Sequences;
|
||||
Name = name.ToLowerInvariant();
|
||||
this.facingFunc = facingFunc;
|
||||
@@ -58,18 +61,23 @@ namespace OpenRA.Graphics
|
||||
var tintModifiers = CurrentSequence.IgnoreWorldTint ? TintModifiers.IgnoreWorldTint : TintModifiers.None;
|
||||
var alpha = CurrentSequence.GetAlpha(CurrentFrame);
|
||||
var (image, rotation) = CurrentSequence.GetSpriteWithRotation(CurrentFrame, facingFunc());
|
||||
var imageRenderable = new SpriteRenderable(image, pos, offset, CurrentSequence.ZOffset + zOffset, palette, CurrentSequence.Scale, alpha, float3.Ones, tintModifiers, IsDecoration,
|
||||
rotation);
|
||||
var imageRenderable = new SpriteRenderable(
|
||||
image, pos, offset, CurrentSequence.ZOffset + zOffset, palette,
|
||||
CurrentSequence.Scale, alpha, float3.Ones, tintModifiers, IsDecoration, rotation);
|
||||
|
||||
var shadow = CurrentSequence.GetShadow(CurrentFrame, facingFunc());
|
||||
if (shadow != null)
|
||||
{
|
||||
var shadowRenderable = new SpriteRenderable(shadow, pos, offset, CurrentSequence.ShadowZOffset + zOffset, palette, CurrentSequence.Scale, 1f, float3.Ones, tintModifiers,
|
||||
var height = map.DistanceAboveTerrain(pos).Length;
|
||||
|
||||
var shadowRenderable = new SpriteRenderable(
|
||||
shadow, pos, offset - new WVec(0, 0, height), CurrentSequence.ShadowZOffset + zOffset + height, palette,
|
||||
CurrentSequence.Scale, 1f, float3.Ones, tintModifiers,
|
||||
true, rotation);
|
||||
return new IRenderable[] { shadowRenderable, imageRenderable };
|
||||
return [shadowRenderable, imageRenderable];
|
||||
}
|
||||
|
||||
return new IRenderable[] { imageRenderable };
|
||||
return [imageRenderable];
|
||||
}
|
||||
|
||||
public IRenderable[] RenderUI(WorldRenderer wr, int2 pos, in WVec offset, int zOffset, PaletteReference palette, float scale = 1f, float rotation = 0f)
|
||||
@@ -85,10 +93,10 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
var shadowPos = pos - new int2((int)(scale * shadow.Size.X / 2), (int)(scale * shadow.Size.Y / 2));
|
||||
var shadowRenderable = new UISpriteRenderable(shadow, WPos.Zero + offset, shadowPos, CurrentSequence.ShadowZOffset + zOffset, palette, scale, 1f, rotation);
|
||||
return new IRenderable[] { shadowRenderable, imageRenderable };
|
||||
return [shadowRenderable, imageRenderable];
|
||||
}
|
||||
|
||||
return new IRenderable[] { imageRenderable };
|
||||
return [imageRenderable];
|
||||
}
|
||||
|
||||
public Rectangle ScreenBounds(WorldRenderer wr, WPos pos, in WVec offset)
|
||||
@@ -243,9 +251,9 @@ namespace OpenRA.Graphics
|
||||
return sequences.GetSequence(Name, sequenceName);
|
||||
}
|
||||
|
||||
public string GetRandomExistingSequence(string[] sequences, MersenneTwister random)
|
||||
public string GetRandomExistingSequence(ImmutableArray<string> sequences, MersenneTwister random)
|
||||
{
|
||||
return sequences.Where(s => HasSequence(s)).RandomOrDefault(random);
|
||||
return sequences.Where(HasSequence).RandomOrDefault(random);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Primitives;
|
||||
@@ -47,9 +49,9 @@ namespace OpenRA.Graphics
|
||||
public readonly string Image2x = null;
|
||||
public readonly string Image3x = null;
|
||||
|
||||
public readonly int[] PanelRegion = null;
|
||||
public readonly ImmutableArray<int> PanelRegion = default;
|
||||
public readonly PanelSides PanelSides = PanelSides.All;
|
||||
public readonly Dictionary<string, Rectangle> Regions = new();
|
||||
public readonly FrozenDictionary<string, Rectangle> Regions = FrozenDictionary<string, Rectangle>.Empty;
|
||||
}
|
||||
|
||||
public static IReadOnlyDictionary<string, Collection> Collections => collections;
|
||||
@@ -71,17 +73,18 @@ namespace OpenRA.Graphics
|
||||
dpiScale = Game.Renderer.WindowScale;
|
||||
|
||||
fileSystem = modData.DefaultFileSystem;
|
||||
collections = new Dictionary<string, Collection>();
|
||||
cachedSheets = new Dictionary<string, (Sheet, int)>();
|
||||
cachedSprites = new Dictionary<string, Dictionary<string, Sprite>>();
|
||||
cachedPanelSprites = new Dictionary<string, Sprite[]>();
|
||||
cachedCollectionSheets = new Dictionary<Collection, (Sheet, int)>();
|
||||
collections = [];
|
||||
cachedSheets = [];
|
||||
cachedSprites = [];
|
||||
cachedPanelSprites = [];
|
||||
cachedCollectionSheets = [];
|
||||
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
var chrome = MiniYaml.Merge(modData.Manifest.Chrome
|
||||
.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s)));
|
||||
.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool)));
|
||||
|
||||
foreach (var c in chrome)
|
||||
if (!c.Key.StartsWith("^", StringComparison.Ordinal))
|
||||
if (!c.Key.StartsWith('^'))
|
||||
LoadCollection(c.Key, c.Value);
|
||||
}
|
||||
|
||||
@@ -169,7 +172,7 @@ namespace OpenRA.Graphics
|
||||
var sheetDensity = SheetForCollection(collection);
|
||||
if (cachedCollection == null)
|
||||
{
|
||||
cachedCollection = new Dictionary<string, Sprite>();
|
||||
cachedCollection = [];
|
||||
cachedSprites.Add(collectionName, cachedCollection);
|
||||
}
|
||||
|
||||
@@ -227,19 +230,23 @@ namespace OpenRA.Graphics
|
||||
(PanelSides.Bottom | PanelSides.Right, new Rectangle(pr[0] + pr[2] + pr[4], pr[1] + pr[3] + pr[5], pr[6], pr[7]))
|
||||
};
|
||||
|
||||
sprites = sides.Select(x => ps.HasSide(x.PanelSides) ? new Sprite(sheetDensity.Sheet, sheetDensity.Density * x.Bounds, TextureChannel.RGBA, 1f / sheetDensity.Density) : null)
|
||||
sprites = sides
|
||||
.Select(x =>
|
||||
ps.HasSide(x.PanelSides)
|
||||
? new Sprite(sheetDensity.Sheet, sheetDensity.Density * x.Bounds, TextureChannel.RGBA, 1f / sheetDensity.Density)
|
||||
: null)
|
||||
.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
// PERF: We don't need to search for images if there are no definitions.
|
||||
// PERF: It's more efficient to send an empty array rather than an array of 9 nulls.
|
||||
if (!collection.Regions.Any())
|
||||
return Array.Empty<Sprite>();
|
||||
if (collection.Regions.Count == 0)
|
||||
return [];
|
||||
|
||||
// Support manual definitions for unusual dialog layouts
|
||||
sprites = new[]
|
||||
{
|
||||
sprites =
|
||||
[
|
||||
TryGetImage(collectionName, "corner-tl"),
|
||||
TryGetImage(collectionName, "border-t"),
|
||||
TryGetImage(collectionName, "corner-tr"),
|
||||
@@ -249,7 +256,7 @@ namespace OpenRA.Graphics
|
||||
TryGetImage(collectionName, "corner-bl"),
|
||||
TryGetImage(collectionName, "border-b"),
|
||||
TryGetImage(collectionName, "corner-br")
|
||||
};
|
||||
];
|
||||
}
|
||||
|
||||
cachedPanelSprites.Add(collectionName, sprites);
|
||||
|
||||
@@ -11,11 +11,13 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public sealed class CursorManager
|
||||
public sealed class CursorManager : IDisposable
|
||||
{
|
||||
sealed class Cursor
|
||||
{
|
||||
@@ -28,8 +30,8 @@ namespace OpenRA.Graphics
|
||||
public IHardwareCursor[] Cursors;
|
||||
}
|
||||
|
||||
readonly Dictionary<string, Cursor> cursors = new();
|
||||
readonly SheetBuilder sheetBuilder;
|
||||
readonly Dictionary<string, Cursor> cursors = [];
|
||||
public readonly SheetBuilder SheetBuilder;
|
||||
readonly GraphicSettings graphicSettings;
|
||||
|
||||
Cursor cursor;
|
||||
@@ -38,16 +40,35 @@ namespace OpenRA.Graphics
|
||||
readonly bool hardwareCursorsDisabled = false;
|
||||
bool hardwareCursorsDoubled = false;
|
||||
|
||||
public CursorManager(CursorProvider cursorProvider)
|
||||
public CursorManager(ModData modData)
|
||||
{
|
||||
hardwareCursorsDisabled = Game.Settings.Graphics.DisableHardwareCursors;
|
||||
|
||||
graphicSettings = Game.Settings.Graphics;
|
||||
sheetBuilder = new SheetBuilder(SheetType.BGRA);
|
||||
foreach (var kv in cursorProvider.Cursors)
|
||||
hardwareCursorsDisabled = graphicSettings.DisableHardwareCursors;
|
||||
SheetBuilder = new SheetBuilder(SheetType.BGRA, modData.Manifest.RendererConstants.CursorSheetSize);
|
||||
|
||||
// Overwrite previous definitions if there are duplicates
|
||||
var pals = new Dictionary<string, IProvidesCursorPaletteInfo>();
|
||||
foreach (var p in modData.DefaultRules.Actors[SystemActors.World].TraitInfos<IProvidesCursorPaletteInfo>())
|
||||
if (p.Palette != null)
|
||||
pals[p.Palette] = p;
|
||||
|
||||
var paletteCache = new Cache<string, ImmutablePalette>(p => pals[p].ReadPalette(modData.DefaultFileSystem));
|
||||
var frameCache = new FrameCache(modData.DefaultFileSystem, modData.SpriteLoaders);
|
||||
|
||||
// Sort the cursors for better packing onto the sheet.
|
||||
foreach (var kv in modData.Cursors)
|
||||
{
|
||||
var frames = kv.Value.Frames;
|
||||
var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? cursorProvider.Palettes[kv.Value.Palette] : null;
|
||||
var cursorSprites = frameCache[kv.Value.Src];
|
||||
var length = kv.Value.Length ?? cursorSprites.Length - kv.Value.Start;
|
||||
|
||||
if (kv.Value.Start > cursorSprites.Length)
|
||||
throw new YamlException($"Cursor {kv.Value.Name}: {nameof(kv.Value.Start)} is greater than the length of the sprite sequence.");
|
||||
|
||||
if (kv.Value.Length > cursorSprites.Length)
|
||||
throw new YamlException($"Cursor {kv.Value.Name}: {nameof(kv.Value.Length)} is greater than the length of the sprite sequence.");
|
||||
|
||||
var frames = cursorSprites.Skip(kv.Value.Start).Take(length).ToArray();
|
||||
var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? paletteCache[kv.Value.Palette] : null;
|
||||
|
||||
var c = new Cursor
|
||||
{
|
||||
@@ -78,7 +99,7 @@ namespace OpenRA.Graphics
|
||||
type = SpriteFrameType.Bgra32;
|
||||
}
|
||||
|
||||
c.Sprites[c.Length++] = sheetBuilder.Add(data, type, f.Size, 0, hotspot);
|
||||
c.Sprites[c.Length++] = SheetBuilder.Add(data, type, f.Size, 0, hotspot);
|
||||
|
||||
// Bounds relative to the hotspot
|
||||
c.Bounds = Rectangle.Union(c.Bounds, new Rectangle(hotspot, f.Size));
|
||||
@@ -90,12 +111,12 @@ namespace OpenRA.Graphics
|
||||
cursors.Add(kv.Key, c);
|
||||
}
|
||||
|
||||
CreateOrUpdateHardwareCursors();
|
||||
|
||||
foreach (var s in sheetBuilder.AllSheets)
|
||||
s.ReleaseBuffer();
|
||||
|
||||
Update();
|
||||
// Allow the utility to create a cursor manager.
|
||||
if (Game.Renderer != null)
|
||||
{
|
||||
CreateOrUpdateHardwareCursors();
|
||||
Update();
|
||||
}
|
||||
}
|
||||
|
||||
void CreateOrUpdateHardwareCursors()
|
||||
@@ -128,6 +149,8 @@ namespace OpenRA.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
SheetBuilder.Current?.ReleaseBuffer();
|
||||
|
||||
hardwareCursorsDoubled = graphicSettings.CursorDouble;
|
||||
}
|
||||
|
||||
@@ -229,6 +252,10 @@ namespace OpenRA.Graphics
|
||||
|
||||
var width = frame.Size.Width;
|
||||
var height = frame.Size.Height;
|
||||
|
||||
if (width == 0 || height == 0)
|
||||
return [];
|
||||
|
||||
var data = new byte[4 * width * height];
|
||||
unsafe
|
||||
{
|
||||
@@ -273,11 +300,8 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
for (var i = 0; i < c.Cursors.Length; i++)
|
||||
{
|
||||
if (c.Cursors[i] != null)
|
||||
{
|
||||
c.Cursors[i].Dispose();
|
||||
c.Cursors[i] = null;
|
||||
}
|
||||
c.Cursors[i]?.Dispose();
|
||||
c.Cursors[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,7 +311,7 @@ namespace OpenRA.Graphics
|
||||
ClearHardwareCursors();
|
||||
|
||||
cursors.Clear();
|
||||
sheetBuilder.Dispose();
|
||||
SheetBuilder.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public sealed class CursorProvider
|
||||
{
|
||||
public readonly IReadOnlyDictionary<string, CursorSequence> Cursors;
|
||||
public readonly IReadOnlyDictionary<string, ImmutablePalette> Palettes;
|
||||
|
||||
public CursorProvider(ModData modData)
|
||||
{
|
||||
var fileSystem = modData.DefaultFileSystem;
|
||||
var sequenceYaml = MiniYaml.Merge(modData.Manifest.Cursors.Select(
|
||||
s => MiniYaml.FromStream(fileSystem.Open(s), s)));
|
||||
|
||||
var nodesDict = new MiniYaml(null, sequenceYaml).ToDictionary();
|
||||
|
||||
// Overwrite previous definitions if there are duplicates
|
||||
var pals = new Dictionary<string, IProvidesCursorPaletteInfo>();
|
||||
foreach (var p in modData.DefaultRules.Actors[SystemActors.World].TraitInfos<IProvidesCursorPaletteInfo>())
|
||||
if (p.Palette != null)
|
||||
pals[p.Palette] = p;
|
||||
|
||||
Palettes = nodesDict["Cursors"].Nodes.Select(n => n.Value.Value)
|
||||
.Where(p => p != null)
|
||||
.Distinct()
|
||||
.ToDictionary(p => p, p => pals[p].ReadPalette(modData.DefaultFileSystem));
|
||||
|
||||
var frameCache = new FrameCache(fileSystem, modData.SpriteLoaders);
|
||||
var cursors = new Dictionary<string, CursorSequence>();
|
||||
foreach (var s in nodesDict["Cursors"].Nodes)
|
||||
foreach (var sequence in s.Value.Nodes)
|
||||
cursors.Add(sequence.Key, new CursorSequence(frameCache, sequence.Key, s.Key, s.Value.Value, sequence.Value));
|
||||
|
||||
Cursors = cursors;
|
||||
}
|
||||
|
||||
public bool HasCursorSequence(string cursor)
|
||||
{
|
||||
return Cursors.ContainsKey(cursor);
|
||||
}
|
||||
|
||||
public CursorSequence GetCursorSequence(string cursor)
|
||||
{
|
||||
try { return Cursors[cursor]; }
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
throw new InvalidOperationException($"Cursor does not have a sequence `{cursor}`");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,60 +9,44 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public class CursorSequence
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly string Src;
|
||||
public readonly int Start;
|
||||
public readonly int Length;
|
||||
public readonly int? Length;
|
||||
public readonly string Palette;
|
||||
public readonly int2 Hotspot;
|
||||
|
||||
public readonly ISpriteFrame[] Frames;
|
||||
|
||||
public CursorSequence(FrameCache cache, string name, string cursorSrc, string palette, MiniYaml info)
|
||||
public CursorSequence(string name, string cursorSrc, string palette, MiniYaml info)
|
||||
{
|
||||
var d = info.ToDictionary();
|
||||
|
||||
Start = Exts.ParseIntegerInvariant(d["Start"].Value);
|
||||
Palette = palette;
|
||||
Name = name;
|
||||
Src = cursorSrc;
|
||||
|
||||
var cursorSprites = cache[cursorSrc];
|
||||
Frames = cursorSprites.Skip(Start).ToArray();
|
||||
|
||||
if ((d.TryGetValue("Length", out var yaml) && yaml.Value == "*") ||
|
||||
(d.TryGetValue("End", out yaml) && yaml.Value == "*"))
|
||||
Length = Frames.Length;
|
||||
else if (d.TryGetValue("Length", out yaml))
|
||||
Length = Exts.ParseIntegerInvariant(yaml.Value);
|
||||
else if (d.TryGetValue("End", out yaml))
|
||||
Length = Exts.ParseIntegerInvariant(yaml.Value) - Start;
|
||||
else
|
||||
Length = 1;
|
||||
|
||||
Frames = Frames.Take(Length).ToArray();
|
||||
|
||||
if (Start > cursorSprites.Length)
|
||||
throw new YamlException($"Cursor {name}: {nameof(Start)} is greater than the length of the sprite sequence.");
|
||||
|
||||
if (Length > cursorSprites.Length)
|
||||
throw new YamlException($"Cursor {name}: {nameof(Length)} is greater than the length of the sprite sequence.");
|
||||
|
||||
if (d.TryGetValue("X", out yaml))
|
||||
if (d.TryGetValue("X", out var yaml))
|
||||
{
|
||||
Exts.TryParseIntegerInvariant(yaml.Value, out var x);
|
||||
Exts.TryParseInt32Invariant(yaml.Value, out var x);
|
||||
Hotspot = Hotspot.WithX(x);
|
||||
}
|
||||
|
||||
if (d.TryGetValue("Y", out yaml))
|
||||
{
|
||||
Exts.TryParseIntegerInvariant(yaml.Value, out var y);
|
||||
Exts.TryParseInt32Invariant(yaml.Value, out var y);
|
||||
Hotspot = Hotspot.WithY(y);
|
||||
}
|
||||
|
||||
Start = Exts.ParseInt32Invariant(d["Start"].Value);
|
||||
if (d.TryGetValue("Length", out yaml))
|
||||
Length = yaml.Value != "*" ? Exts.ParseInt32Invariant(yaml.Value) : null;
|
||||
else if (d.TryGetValue("End", out yaml) && yaml.Value == "*")
|
||||
Length = yaml.Value != "*" ? Exts.ParseInt32Invariant(yaml.Value) - Start : null;
|
||||
else
|
||||
Length = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ namespace OpenRA.Graphics
|
||||
public ITexture ColorShifts { get; }
|
||||
|
||||
public int Height { get; private set; }
|
||||
readonly Dictionary<string, ImmutablePalette> palettes = new();
|
||||
readonly Dictionary<string, MutablePalette> mutablePalettes = new();
|
||||
readonly Dictionary<string, int> indices = new();
|
||||
byte[] buffer = Array.Empty<byte>();
|
||||
float[] colorShiftBuffer = Array.Empty<float>();
|
||||
readonly Dictionary<string, ImmutablePalette> palettes = [];
|
||||
readonly Dictionary<string, MutablePalette> mutablePalettes = [];
|
||||
readonly Dictionary<string, int> indices = [];
|
||||
byte[] buffer = [];
|
||||
float[] colorShiftBuffer = [];
|
||||
|
||||
public HardwarePalette()
|
||||
{
|
||||
@@ -85,7 +85,10 @@ namespace OpenRA.Graphics
|
||||
public void ReplacePalette(string name, IPalette p)
|
||||
{
|
||||
if (mutablePalettes.ContainsKey(name))
|
||||
{
|
||||
palettes[name] = new ImmutablePalette(p);
|
||||
CopyPaletteToBuffer(indices[name], mutablePalettes[name] = new MutablePalette(p));
|
||||
}
|
||||
else if (palettes.ContainsKey(name))
|
||||
CopyPaletteToBuffer(indices[name], palettes[name] = new ImmutablePalette(p));
|
||||
else
|
||||
|
||||
56
OpenRA.Game/Graphics/MarkerTileRenderable.cs
Normal file
56
OpenRA.Game/Graphics/MarkerTileRenderable.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Linq;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public class MarkerTileRenderable : IRenderable, IFinalizedRenderable
|
||||
{
|
||||
readonly CPos pos;
|
||||
readonly Color color;
|
||||
|
||||
public MarkerTileRenderable(CPos pos, Color color)
|
||||
{
|
||||
this.pos = pos;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public WPos Pos => WPos.Zero;
|
||||
public int ZOffset => 0;
|
||||
public bool IsDecoration => true;
|
||||
|
||||
public IRenderable WithZOffset(int newOffset) { return this; }
|
||||
|
||||
public IRenderable OffsetBy(in WVec vec)
|
||||
{
|
||||
return new MarkerTileRenderable(pos, color);
|
||||
}
|
||||
|
||||
public IRenderable AsDecoration() { return this; }
|
||||
|
||||
public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; }
|
||||
public void Render(WorldRenderer wr)
|
||||
{
|
||||
var map = wr.World.Map;
|
||||
var r = map.Grid.Ramps[map.Ramp[pos]];
|
||||
var wpos = map.CenterOfCell(pos) - new WVec(0, 0, r.CenterHeightOffset);
|
||||
|
||||
var corners = r.Corners.Select(corner => wr.Viewport.WorldToViewPx(wr.Screen3DPosition(wpos + corner))).ToList();
|
||||
|
||||
Game.Renderer.RgbaColorRenderer.FillRect(corners[0], corners[1], corners[2], corners[3], color);
|
||||
}
|
||||
|
||||
public void RenderDebugGeometry(WorldRenderer wr) { }
|
||||
public Rectangle ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; }
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,8 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
@@ -30,65 +29,23 @@ namespace OpenRA.Graphics
|
||||
Rectangle AggregateBounds { get; }
|
||||
}
|
||||
|
||||
public readonly struct ModelRenderData
|
||||
public interface IModelWidget
|
||||
{
|
||||
public readonly int Start;
|
||||
public readonly int Count;
|
||||
public readonly Sheet Sheet;
|
||||
|
||||
public ModelRenderData(int start, int count, Sheet sheet)
|
||||
{
|
||||
Start = start;
|
||||
Count = count;
|
||||
Sheet = sheet;
|
||||
}
|
||||
string Palette { get; }
|
||||
float Scale { get; }
|
||||
void Setup(Func<bool> isVisible, Func<string> getPalette, Func<string> getPlayerPalette,
|
||||
Func<float> getScale, Func<IModel> getVoxel, Func<WRot> getRotation);
|
||||
}
|
||||
|
||||
public interface IModelCache : IDisposable
|
||||
public readonly record struct ModelRenderData(int Start, int Count, Sheet Sheet);
|
||||
|
||||
public interface IModelCacheInfo : ITraitInfoInterface { }
|
||||
|
||||
public interface IModelCache
|
||||
{
|
||||
IModel GetModel(string model);
|
||||
IModel GetModelSequence(string model, string sequence);
|
||||
bool HasModelSequence(string model, string sequence);
|
||||
IVertexBuffer<Vertex> VertexBuffer { get; }
|
||||
}
|
||||
|
||||
public interface IModelSequenceLoader
|
||||
{
|
||||
Action<string> OnMissingModelError { get; set; }
|
||||
IModelCache CacheModels(IReadOnlyFileSystem fileSystem, ModData modData, IReadOnlyDictionary<string, MiniYamlNode> modelDefinitions);
|
||||
}
|
||||
|
||||
public class PlaceholderModelSequenceLoader : IModelSequenceLoader
|
||||
{
|
||||
public Action<string> OnMissingModelError { get; set; }
|
||||
|
||||
sealed class PlaceholderModelCache : IModelCache
|
||||
{
|
||||
public IVertexBuffer<Vertex> VertexBuffer => throw new NotImplementedException();
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public IModel GetModel(string model)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IModel GetModelSequence(string model, string sequence)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool HasModelSequence(string model, string sequence)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public PlaceholderModelSequenceLoader(ModData modData) { }
|
||||
|
||||
public IModelCache CacheModels(IReadOnlyFileSystem fileSystem, ModData modData, IReadOnlyDictionary<string, MiniYamlNode> modelDefinitions)
|
||||
{
|
||||
return new PlaceholderModelCache();
|
||||
}
|
||||
IVertexBuffer<ModelVertex> VertexBuffer { get; }
|
||||
}
|
||||
}
|
||||
|
||||
53
OpenRA.Game/Graphics/ModelVertex.cs
Normal file
53
OpenRA.Game/Graphics/ModelVertex.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly struct ModelVertex
|
||||
{
|
||||
// 3d position
|
||||
public readonly float X, Y, Z;
|
||||
|
||||
// Primary and secondary texture coordinates or RGBA color
|
||||
public readonly float S, T, U, V;
|
||||
|
||||
// Palette and channel flags
|
||||
public readonly float P, C;
|
||||
|
||||
public ModelVertex(in float3 xyz, float s, float t, float u, float v, float p, float c)
|
||||
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, p, c) { }
|
||||
|
||||
public ModelVertex(float x, float y, float z, float s, float t, float u, float v, float p, float c)
|
||||
{
|
||||
X = x; Y = y; Z = z;
|
||||
S = s; T = t;
|
||||
U = u; V = v;
|
||||
P = p; C = c;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ModelShaderBindings : ShaderBindings
|
||||
{
|
||||
public ModelShaderBindings()
|
||||
: base("model")
|
||||
{ }
|
||||
|
||||
public override ShaderVertexAttribute[] Attributes { get; } =
|
||||
[
|
||||
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
|
||||
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
|
||||
new ShaderVertexAttribute("aVertexTexMetadata", ShaderVertexAttributeType.Float, 2, 28),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
@@ -30,7 +31,7 @@ namespace OpenRA.Graphics
|
||||
|
||||
public static Color GetColor(this IPalette palette, int index)
|
||||
{
|
||||
return Color.FromArgb((int)palette[index]);
|
||||
return Color.FromArgb(palette[index]);
|
||||
}
|
||||
|
||||
public static IPalette AsReadOnly(this IPalette palette)
|
||||
@@ -64,18 +65,18 @@ namespace OpenRA.Graphics
|
||||
Buffer.BlockCopy(colors, 0, destination, destinationOffset * 4, Palette.Size * 4);
|
||||
}
|
||||
|
||||
public ImmutablePalette(string filename, int[] remapTransparent, int[] remap)
|
||||
public ImmutablePalette(string filename, ImmutableArray<int> remapTransparent, ImmutableArray<int> remap)
|
||||
{
|
||||
using (var s = File.OpenRead(filename))
|
||||
LoadFromStream(s, remapTransparent, remap);
|
||||
}
|
||||
|
||||
public ImmutablePalette(Stream s, int[] remapTransparent, int[] remapShadow)
|
||||
public ImmutablePalette(Stream s, ImmutableArray<int> remapTransparent, ImmutableArray<int> remapShadow)
|
||||
{
|
||||
LoadFromStream(s, remapTransparent, remapShadow);
|
||||
}
|
||||
|
||||
void LoadFromStream(Stream s, int[] remapTransparent, int[] remapShadow)
|
||||
void LoadFromStream(Stream s, ImmutableArray<int> remapTransparent, ImmutableArray<int> remapShadow)
|
||||
{
|
||||
using (var reader = new BinaryReader(s))
|
||||
for (var i = 0; i < Palette.Size; i++)
|
||||
@@ -103,7 +104,7 @@ namespace OpenRA.Graphics
|
||||
: this(p)
|
||||
{
|
||||
for (var i = 0; i < Palette.Size; i++)
|
||||
colors[i] = (uint)r.GetRemappedColor(this.GetColor(i), i).ToArgb();
|
||||
colors[i] = r.GetRemappedColor(this.GetColor(i), i).ToArgb();
|
||||
}
|
||||
|
||||
public ImmutablePalette(IPalette p)
|
||||
@@ -142,7 +143,7 @@ namespace OpenRA.Graphics
|
||||
|
||||
public void SetColor(int index, Color color)
|
||||
{
|
||||
colors[index] = (uint)color.ToArgb();
|
||||
colors[index] = color.ToArgb();
|
||||
}
|
||||
|
||||
public void SetFromPalette(IPalette p)
|
||||
@@ -153,7 +154,7 @@ namespace OpenRA.Graphics
|
||||
public void ApplyRemap(IPaletteRemap r)
|
||||
{
|
||||
for (var i = 0; i < Palette.Size; i++)
|
||||
colors[i] = (uint)r.GetRemappedColor(this.GetColor(i), i).ToArgb();
|
||||
colors[i] = r.GetRemappedColor(this.GetColor(i), i).ToArgb();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,19 +13,17 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
public sealed class PaletteReference
|
||||
{
|
||||
readonly float index;
|
||||
readonly HardwarePalette hardwarePalette;
|
||||
|
||||
public readonly string Name;
|
||||
public IPalette Palette { get; internal set; }
|
||||
public float TextureIndex => index / hardwarePalette.Height;
|
||||
public float TextureMidIndex => (index + 0.5f) / hardwarePalette.Height;
|
||||
public int TextureIndex { get; }
|
||||
|
||||
public PaletteReference(string name, int index, IPalette palette, HardwarePalette hardwarePalette)
|
||||
{
|
||||
Name = name;
|
||||
Palette = palette;
|
||||
this.index = index;
|
||||
TextureIndex = index;
|
||||
this.hardwarePalette = hardwarePalette;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,13 @@ namespace OpenRA
|
||||
Automatic,
|
||||
ANGLE,
|
||||
Modern,
|
||||
Embedded,
|
||||
Legacy
|
||||
Embedded
|
||||
}
|
||||
|
||||
public interface IPlatform
|
||||
{
|
||||
IPlatformWindow CreateWindow(Size size, WindowMode windowMode, float scaleModifier, int batchSize, int videoDisplay, GLProfile profile, bool enableLegacyGL);
|
||||
IPlatformWindow CreateWindow(
|
||||
Size size, WindowMode windowMode, float scaleModifier, int vertexBatchSize, int indexBatchSize, int videoDisplay, GLProfile profile);
|
||||
ISoundEngine CreateSound(string device);
|
||||
IFont CreateFont(byte[] data);
|
||||
}
|
||||
@@ -66,6 +66,7 @@ namespace OpenRA
|
||||
void PumpInput(IInputHandler inputHandler);
|
||||
string GetClipboardText();
|
||||
bool SetClipboardText(string text);
|
||||
bool TryOpenUrl(string url);
|
||||
|
||||
void GrabWindowMouseFocus();
|
||||
void ReleaseWindowMouseFocus();
|
||||
@@ -83,16 +84,19 @@ namespace OpenRA
|
||||
|
||||
public interface IGraphicsContext : IDisposable
|
||||
{
|
||||
IVertexBuffer<Vertex> CreateVertexBuffer(int size);
|
||||
Vertex[] CreateVertices(int size);
|
||||
IVertexBuffer<T> CreateEmptyVertexBuffer<T>(int size) where T : struct;
|
||||
IVertexBuffer<T> CreateVertexBuffer<T>(T[] data, bool dynamic = true) where T : struct;
|
||||
T[] CreateVertices<T>(int size) where T : struct;
|
||||
IIndexBuffer CreateIndexBuffer(uint[] indices);
|
||||
ITexture CreateTexture();
|
||||
IFrameBuffer CreateFrameBuffer(Size s);
|
||||
IFrameBuffer CreateFrameBuffer(Size s, Color clearColor);
|
||||
IShader CreateShader(string name);
|
||||
IShader CreateShader(IShaderBindings shaderBindings);
|
||||
void EnableScissor(int x, int y, int width, int height);
|
||||
void DisableScissor();
|
||||
void Present();
|
||||
void DrawPrimitives(PrimitiveType pt, int firstVertex, int numVertices);
|
||||
void DrawElements(int numIndices, int offset);
|
||||
void Clear();
|
||||
void EnableDepthBuffer();
|
||||
void DisableDepthBuffer();
|
||||
@@ -102,7 +106,14 @@ namespace OpenRA
|
||||
string GLVersion { get; }
|
||||
}
|
||||
|
||||
public interface IVertexBuffer<T> : IDisposable
|
||||
public interface IRenderer
|
||||
{
|
||||
void BeginFrame();
|
||||
void EndFrame();
|
||||
void SetPalette(HardwarePalette palette);
|
||||
}
|
||||
|
||||
public interface IVertexBuffer<T> : IDisposable where T : struct
|
||||
{
|
||||
void Bind();
|
||||
void SetData(T[] vertices, int length);
|
||||
@@ -114,16 +125,32 @@ namespace OpenRA
|
||||
void SetData(T[] vertices, int offset, int start, int length);
|
||||
}
|
||||
|
||||
public interface IIndexBuffer : IDisposable
|
||||
{
|
||||
void Bind();
|
||||
}
|
||||
|
||||
public interface IShader
|
||||
{
|
||||
void SetBool(string name, bool value);
|
||||
void SetVec(string name, float x);
|
||||
void SetVec(string name, float x, float y);
|
||||
void SetVec(string name, float x, float y, float z);
|
||||
void SetVec(string name, float[] vec, int length);
|
||||
void SetVec(string name, ReadOnlyMemory<float> vec, int length);
|
||||
void SetTexture(string param, ITexture texture);
|
||||
void SetMatrix(string param, float[] mtx);
|
||||
void PrepareRender();
|
||||
void Bind();
|
||||
}
|
||||
|
||||
public interface IShaderBindings
|
||||
{
|
||||
string VertexShaderName { get; }
|
||||
string VertexShaderCode { get; }
|
||||
string FragmentShaderName { get; }
|
||||
string FragmentShaderCode { get; }
|
||||
int Stride { get; }
|
||||
ShaderVertexAttribute[] Attributes { get; }
|
||||
}
|
||||
|
||||
public enum TextureScaleFilter { Nearest, Linear }
|
||||
@@ -132,6 +159,7 @@ namespace OpenRA
|
||||
{
|
||||
void SetData(byte[] colors, int width, int height);
|
||||
void SetFloatData(float[] data, int width, int height);
|
||||
void SetDataFromReadBuffer(Rectangle rect);
|
||||
byte[] GetData();
|
||||
Size Size { get; }
|
||||
TextureScaleFilter ScaleFilter { get; set; }
|
||||
@@ -153,12 +181,6 @@ namespace OpenRA
|
||||
TriangleList,
|
||||
}
|
||||
|
||||
public readonly struct Range<T>
|
||||
{
|
||||
public readonly T Start, End;
|
||||
public Range(T start, T end) { Start = start; End = end; }
|
||||
}
|
||||
|
||||
public enum WindowMode
|
||||
{
|
||||
Windowed,
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Immutable;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public class PlayerColorRemap : IPaletteRemap
|
||||
{
|
||||
readonly int[] remapIndices;
|
||||
readonly ImmutableArray<int> remapIndices;
|
||||
readonly float hue;
|
||||
readonly float saturation;
|
||||
readonly float value;
|
||||
|
||||
public PlayerColorRemap(int[] remapIndices, Color color)
|
||||
public PlayerColorRemap(ImmutableArray<int> remapIndices, Color color)
|
||||
{
|
||||
this.remapIndices = remapIndices;
|
||||
|
||||
|
||||
45
OpenRA.Game/Graphics/RenderPostProcessPassVertex.cs
Normal file
45
OpenRA.Game/Graphics/RenderPostProcessPassVertex.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly record struct RenderPostProcessPassVertex(float X, float Y);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly record struct RenderPostProcessPassTexturedVertex(float X, float Y, float S, float T);
|
||||
|
||||
public sealed class RenderPostProcessPassShaderBindings : ShaderBindings
|
||||
{
|
||||
public RenderPostProcessPassShaderBindings(string name)
|
||||
: base("postprocess", "postprocess_" + name) { }
|
||||
|
||||
public override ShaderVertexAttribute[] Attributes { get; } =
|
||||
[
|
||||
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0)
|
||||
];
|
||||
}
|
||||
|
||||
public sealed class RenderPostProcessPassTexturedShaderBindings : ShaderBindings
|
||||
{
|
||||
public RenderPostProcessPassTexturedShaderBindings(string name)
|
||||
: base("postprocess_textured", "postprocess_textured_" + name)
|
||||
{ }
|
||||
|
||||
public override ShaderVertexAttribute[] Attributes { get; } =
|
||||
[
|
||||
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0),
|
||||
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 2, 8),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ namespace OpenRA.Graphics
|
||||
static readonly float3 Offset = new(0.5f, 0.5f, 0f);
|
||||
|
||||
readonly SpriteRenderer parent;
|
||||
readonly Vertex[] vertices = new Vertex[6];
|
||||
readonly Vertex[] vertices = new Vertex[4];
|
||||
|
||||
public RgbaColorRenderer(SpriteRenderer parent)
|
||||
{
|
||||
@@ -45,14 +45,12 @@ namespace OpenRA.Graphics
|
||||
var eb = endColor.B / 255.0f;
|
||||
var ea = endColor.A / 255.0f;
|
||||
|
||||
vertices[0] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0, 0);
|
||||
vertices[1] = new Vertex(start + corner + Offset, sr, sg, sb, sa, 0, 0);
|
||||
vertices[2] = new Vertex(end + corner + Offset, er, eg, eb, ea, 0, 0);
|
||||
vertices[3] = new Vertex(end + corner + Offset, er, eg, eb, ea, 0, 0);
|
||||
vertices[4] = new Vertex(end - corner + Offset, er, eg, eb, ea, 0, 0);
|
||||
vertices[5] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0, 0);
|
||||
vertices[0] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0);
|
||||
vertices[1] = new Vertex(start + corner + Offset, sr, sg, sb, sa, 0);
|
||||
vertices[2] = new Vertex(end + corner + Offset, er, eg, eb, ea, 0);
|
||||
vertices[3] = new Vertex(end - corner + Offset, er, eg, eb, ea, 0);
|
||||
|
||||
parent.DrawRGBAVertices(vertices, blendMode);
|
||||
parent.DrawRGBAQuad(vertices, blendMode);
|
||||
}
|
||||
|
||||
public void DrawLine(in float3 start, in float3 end, float width, Color color, BlendMode blendMode = BlendMode.Alpha)
|
||||
@@ -66,13 +64,11 @@ namespace OpenRA.Graphics
|
||||
var b = color.B / 255.0f;
|
||||
var a = color.A / 255.0f;
|
||||
|
||||
vertices[0] = new Vertex(start - corner + Offset, r, g, b, a, 0, 0);
|
||||
vertices[1] = new Vertex(start + corner + Offset, r, g, b, a, 0, 0);
|
||||
vertices[2] = new Vertex(end + corner + Offset, r, g, b, a, 0, 0);
|
||||
vertices[3] = new Vertex(end + corner + Offset, r, g, b, a, 0, 0);
|
||||
vertices[4] = new Vertex(end - corner + Offset, r, g, b, a, 0, 0);
|
||||
vertices[5] = new Vertex(start - corner + Offset, r, g, b, a, 0, 0);
|
||||
parent.DrawRGBAVertices(vertices, blendMode);
|
||||
vertices[0] = new Vertex(start - corner + Offset, r, g, b, a, 0);
|
||||
vertices[1] = new Vertex(start + corner + Offset, r, g, b, a, 0);
|
||||
vertices[2] = new Vertex(end + corner + Offset, r, g, b, a, 0);
|
||||
vertices[3] = new Vertex(end - corner + Offset, r, g, b, a, 0);
|
||||
parent.DrawRGBAQuad(vertices, blendMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -157,13 +153,11 @@ namespace OpenRA.Graphics
|
||||
var cd = closed || i < limit - 1 ? IntersectionOf(end - corner, dir, end - nextCorner, nextDir) : end - corner;
|
||||
|
||||
// Fill segment
|
||||
vertices[0] = new Vertex(ca + Offset, r, g, b, a, 0, 0);
|
||||
vertices[1] = new Vertex(cb + Offset, r, g, b, a, 0, 0);
|
||||
vertices[2] = new Vertex(cc + Offset, r, g, b, a, 0, 0);
|
||||
vertices[3] = new Vertex(cc + Offset, r, g, b, a, 0, 0);
|
||||
vertices[4] = new Vertex(cd + Offset, r, g, b, a, 0, 0);
|
||||
vertices[5] = new Vertex(ca + Offset, r, g, b, a, 0, 0);
|
||||
parent.DrawRGBAVertices(vertices, blendMode);
|
||||
vertices[0] = new Vertex(ca + Offset, r, g, b, a, 0);
|
||||
vertices[1] = new Vertex(cb + Offset, r, g, b, a, 0);
|
||||
vertices[2] = new Vertex(cc + Offset, r, g, b, a, 0);
|
||||
vertices[3] = new Vertex(cd + Offset, r, g, b, a, 0);
|
||||
parent.DrawRGBAQuad(vertices, blendMode);
|
||||
|
||||
// Advance line segment
|
||||
end = next;
|
||||
@@ -197,21 +191,7 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
var tr = new float3(br.X, tl.Y, tl.Z);
|
||||
var bl = new float3(tl.X, br.Y, br.Z);
|
||||
DrawPolygon(new[] { tl, tr, br, bl }, width, color, blendMode);
|
||||
}
|
||||
|
||||
public void FillTriangle(in float3 a, in float3 b, in float3 c, Color color, BlendMode blendMode = BlendMode.Alpha)
|
||||
{
|
||||
color = Util.PremultiplyAlpha(color);
|
||||
var cr = color.R / 255.0f;
|
||||
var cg = color.G / 255.0f;
|
||||
var cb = color.B / 255.0f;
|
||||
var ca = color.A / 255.0f;
|
||||
|
||||
vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0);
|
||||
vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0, 0);
|
||||
vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
|
||||
parent.DrawRGBAVertices(vertices, blendMode);
|
||||
DrawPolygon([tl, tr, br, bl], width, color, blendMode);
|
||||
}
|
||||
|
||||
public void FillRect(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha)
|
||||
@@ -229,25 +209,22 @@ namespace OpenRA.Graphics
|
||||
var cb = color.B / 255.0f;
|
||||
var ca = color.A / 255.0f;
|
||||
|
||||
vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0);
|
||||
vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0, 0);
|
||||
vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
|
||||
vertices[3] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
|
||||
vertices[4] = new Vertex(d + Offset, cr, cg, cb, ca, 0, 0);
|
||||
vertices[5] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0);
|
||||
parent.DrawRGBAVertices(vertices, blendMode);
|
||||
vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0);
|
||||
vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0);
|
||||
vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0);
|
||||
vertices[3] = new Vertex(d + Offset, cr, cg, cb, ca, 0);
|
||||
parent.DrawRGBAQuad(vertices, blendMode);
|
||||
}
|
||||
|
||||
public void FillRect(in float3 a, in float3 b, in float3 c, in float3 d, Color topLeftColor, Color topRightColor, Color bottomRightColor, Color bottomLeftColor, BlendMode blendMode = BlendMode.Alpha)
|
||||
public void FillRect(in float3 a, in float3 b, in float3 c, in float3 d,
|
||||
Color topLeftColor, Color topRightColor, Color bottomRightColor, Color bottomLeftColor, BlendMode blendMode = BlendMode.Alpha)
|
||||
{
|
||||
vertices[0] = VertexWithColor(a + Offset, topLeftColor);
|
||||
vertices[1] = VertexWithColor(b + Offset, topRightColor);
|
||||
vertices[2] = VertexWithColor(c + Offset, bottomRightColor);
|
||||
vertices[3] = VertexWithColor(c + Offset, bottomRightColor);
|
||||
vertices[4] = VertexWithColor(d + Offset, bottomLeftColor);
|
||||
vertices[5] = VertexWithColor(a + Offset, topLeftColor);
|
||||
vertices[3] = VertexWithColor(d + Offset, bottomLeftColor);
|
||||
|
||||
parent.DrawRGBAVertices(vertices, blendMode);
|
||||
parent.DrawRGBAQuad(vertices, blendMode);
|
||||
}
|
||||
|
||||
static Vertex VertexWithColor(in float3 xyz, Color color)
|
||||
@@ -258,7 +235,7 @@ namespace OpenRA.Graphics
|
||||
var cb = color.B / 255.0f;
|
||||
var ca = color.A / 255.0f;
|
||||
|
||||
return new Vertex(xyz, cr, cg, cb, ca, 0, 0);
|
||||
return new Vertex(xyz, cr, cg, cb, ca, 0);
|
||||
}
|
||||
|
||||
public void FillEllipse(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha)
|
||||
|
||||
@@ -37,23 +37,23 @@ namespace OpenRA.Graphics
|
||||
|
||||
public interface ISpriteSequenceLoader
|
||||
{
|
||||
int BgraSheetSize { get; }
|
||||
int IndexedSheetSize { get; }
|
||||
IReadOnlyDictionary<string, ISpriteSequence> ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode node);
|
||||
}
|
||||
|
||||
public sealed class SequenceSet : IDisposable
|
||||
{
|
||||
public readonly string TileSet;
|
||||
readonly ModData modData;
|
||||
readonly string tileSet;
|
||||
readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, ISpriteSequence>> images;
|
||||
public SpriteCache SpriteCache { get; }
|
||||
|
||||
public SequenceSet(IReadOnlyFileSystem fileSystem, ModData modData, string tileSet, MiniYaml additionalSequences)
|
||||
{
|
||||
this.modData = modData;
|
||||
this.tileSet = tileSet;
|
||||
SpriteCache = new SpriteCache(fileSystem, modData.SpriteLoaders, modData.SpriteSequenceLoader.BgraSheetSize, modData.SpriteSequenceLoader.IndexedSheetSize);
|
||||
TileSet = tileSet;
|
||||
|
||||
var rc = modData.Manifest.RendererConstants;
|
||||
SpriteCache = new SpriteCache(fileSystem, modData.SpriteLoaders, rc.SequenceBgraSheetSize, rc.SequenceIndexedSheetSize);
|
||||
using (new Support.PerfTimer("LoadSequences"))
|
||||
images = Load(fileSystem, additionalSequences);
|
||||
}
|
||||
@@ -94,10 +94,10 @@ namespace OpenRA.Graphics
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
// Nodes starting with ^ are inheritable but never loaded directly
|
||||
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal))
|
||||
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix))
|
||||
continue;
|
||||
|
||||
images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node);
|
||||
images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, TileSet, SpriteCache, node);
|
||||
}
|
||||
|
||||
return images;
|
||||
|
||||
56
OpenRA.Game/Graphics/ShaderBindings.cs
Normal file
56
OpenRA.Game/Graphics/ShaderBindings.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public enum ShaderVertexAttributeType
|
||||
{
|
||||
// Assign the underlying OpenGL type values
|
||||
// to simplify enum use in the shader
|
||||
Float = 0x1406, // GL_FLOAT
|
||||
Int = 0x1404, // GL_INT
|
||||
UInt = 0x1405 // GL_UNSIGNED_INT
|
||||
}
|
||||
|
||||
public readonly record struct ShaderVertexAttribute(string Name, ShaderVertexAttributeType Type, int Components, int Offset);
|
||||
|
||||
public abstract class ShaderBindings : IShaderBindings
|
||||
{
|
||||
public string VertexShaderName { get; }
|
||||
public string VertexShaderCode { get; }
|
||||
public string FragmentShaderName { get; }
|
||||
public string FragmentShaderCode { get; }
|
||||
public int Stride { get; }
|
||||
|
||||
public abstract ShaderVertexAttribute[] Attributes { get; }
|
||||
|
||||
protected ShaderBindings(string name)
|
||||
: this(name, name) { }
|
||||
|
||||
protected ShaderBindings(string vertexName, string fragmentName)
|
||||
{
|
||||
Stride = Attributes.Sum(a => a.Components * 4);
|
||||
VertexShaderName = vertexName;
|
||||
VertexShaderCode = GetShaderCode(VertexShaderName + ".vert");
|
||||
FragmentShaderName = fragmentName;
|
||||
FragmentShaderCode = GetShaderCode(FragmentShaderName + ".frag");
|
||||
}
|
||||
|
||||
public static string GetShaderCode(string filename)
|
||||
{
|
||||
var filepath = Path.Combine(Platform.EngineDir, "glsl", filename);
|
||||
return File.ReadAllText(filepath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +132,7 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
if (!Buffered)
|
||||
return;
|
||||
|
||||
dirty = true;
|
||||
releaseBufferOnCommit = true;
|
||||
|
||||
@@ -140,6 +141,29 @@ namespace OpenRA.Graphics
|
||||
GetTexture();
|
||||
}
|
||||
|
||||
public bool ReleaseBufferAndTryTransferTo(Sheet destination)
|
||||
{
|
||||
if (Size != destination.Size)
|
||||
throw new ArgumentException("Destination sheet does not have the same size", nameof(destination));
|
||||
|
||||
var buffer = data;
|
||||
ReleaseBuffer();
|
||||
|
||||
// We aren't committing data to the GPU, so let's not delete our data.
|
||||
if (Game.Renderer == null)
|
||||
return false;
|
||||
|
||||
// Only transfer if the destination has no data that would be lost by overwriting.
|
||||
if (buffer != null && destination.data == null && destination.texture == null)
|
||||
{
|
||||
Array.Clear(buffer, 0, buffer.Length);
|
||||
destination.data = buffer;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
texture?.Dispose();
|
||||
|
||||
@@ -16,7 +16,6 @@ using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
[Serializable]
|
||||
public class SheetOverflowException : Exception
|
||||
{
|
||||
public SheetOverflowException(string message)
|
||||
@@ -34,7 +33,7 @@ namespace OpenRA.Graphics
|
||||
public sealed class SheetBuilder : IDisposable
|
||||
{
|
||||
public readonly SheetType Type;
|
||||
readonly List<Sheet> sheets = new();
|
||||
readonly List<Sheet> sheets = [];
|
||||
readonly Func<Sheet> allocateSheet;
|
||||
readonly int margin;
|
||||
int rowHeight = 0;
|
||||
@@ -66,9 +65,6 @@ namespace OpenRA.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
public SheetBuilder(SheetType t)
|
||||
: this(t, Game.Settings.Graphics.SheetSize) { }
|
||||
|
||||
public SheetBuilder(SheetType t, int sheetSize, int margin = 1)
|
||||
: this(t, () => AllocateSheet(t, sheetSize), margin) { }
|
||||
|
||||
@@ -76,22 +72,26 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
CurrentChannel = t == SheetType.Indexed ? TextureChannel.Red : TextureChannel.RGBA;
|
||||
Type = t;
|
||||
Current = allocateSheet();
|
||||
sheets.Add(Current);
|
||||
this.allocateSheet = allocateSheet;
|
||||
this.margin = margin;
|
||||
}
|
||||
|
||||
public Sprite Add(ISpriteFrame frame) { return Add(frame.Data, frame.Type, frame.Size, 0, frame.Offset); }
|
||||
public Sprite Add(byte[] src, SpriteFrameType type, Size size) { return Add(src, type, size, 0, float3.Zero); }
|
||||
public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset)
|
||||
public Sprite Add(ISpriteFrame frame, bool premultiplied = false) { return Add(frame.Data, frame.Type, frame.Size, 0, frame.Offset, premultiplied); }
|
||||
public Sprite Add(byte[] src, SpriteFrameType type, Size size, bool premultiplied = false) { return Add(src, type, size, 0, float3.Zero, premultiplied); }
|
||||
public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset, bool premultiplied = false)
|
||||
{
|
||||
if (Current == null)
|
||||
{
|
||||
Current = allocateSheet();
|
||||
sheets.Add(Current);
|
||||
}
|
||||
|
||||
// Don't bother allocating empty sprites
|
||||
if (size.Width == 0 || size.Height == 0)
|
||||
return new Sprite(Current, Rectangle.Empty, 0, spriteOffset, CurrentChannel, BlendMode.Alpha);
|
||||
|
||||
var rect = Allocate(size, zRamp, spriteOffset);
|
||||
Util.FastCopyIntoChannel(rect, src, type);
|
||||
Util.FastCopyIntoChannel(rect, src, type, premultiplied);
|
||||
Current.CommitBufferedData();
|
||||
return rect;
|
||||
}
|
||||
@@ -116,6 +116,12 @@ namespace OpenRA.Graphics
|
||||
public Sprite Allocate(Size imageSize, float scale = 1f) { return Allocate(imageSize, 0, float3.Zero, scale); }
|
||||
public Sprite Allocate(Size imageSize, float zRamp, in float3 spriteOffset, float scale = 1f)
|
||||
{
|
||||
if (Current == null)
|
||||
{
|
||||
Current = allocateSheet();
|
||||
sheets.Add(Current);
|
||||
}
|
||||
|
||||
if (imageSize.Width + p.X + margin > Current.Size.Width)
|
||||
{
|
||||
p = new int2(0, p.Y + rowHeight + margin);
|
||||
@@ -130,8 +136,13 @@ namespace OpenRA.Graphics
|
||||
var next = NextChannel(CurrentChannel);
|
||||
if (next == null)
|
||||
{
|
||||
Current.ReleaseBuffer();
|
||||
var previous = Current;
|
||||
Current = allocateSheet();
|
||||
|
||||
// Reuse the backing buffer between sheets where possible.
|
||||
// This avoids allocating additional buffers which the GC must clean up.
|
||||
previous.ReleaseBufferAndTryTransferTo(Current);
|
||||
|
||||
sheets.Add(Current);
|
||||
CurrentChannel = Type == SheetType.Indexed ? TextureChannel.Red : TextureChannel.RGBA;
|
||||
}
|
||||
@@ -142,7 +153,9 @@ namespace OpenRA.Graphics
|
||||
p = int2.Zero;
|
||||
}
|
||||
|
||||
var rect = new Sprite(Current, new Rectangle(p.X + margin, p.Y + margin, imageSize.Width, imageSize.Height), zRamp, spriteOffset, CurrentChannel, BlendMode.Alpha, scale);
|
||||
var rect = new Sprite(
|
||||
Current, new Rectangle(p.X + margin, p.Y + margin, imageSize.Width, imageSize.Height),
|
||||
zRamp, spriteOffset, CurrentChannel, BlendMode.Alpha, scale);
|
||||
p += new int2(imageSize.Width + margin, 0);
|
||||
|
||||
return rect;
|
||||
|
||||
@@ -42,11 +42,11 @@ namespace OpenRA.Graphics
|
||||
// in rendering a line of texels that sample outside the sprite rectangle.
|
||||
// Insetting the texture coordinates by a small fraction of a pixel avoids this
|
||||
// with negligible impact on the 1:1 rendering case.
|
||||
var inset = 1 / 128f;
|
||||
Left = (Math.Min(bounds.Left, bounds.Right) + inset) / sheet.Size.Width;
|
||||
Top = (Math.Min(bounds.Top, bounds.Bottom) + inset) / sheet.Size.Height;
|
||||
Right = (Math.Max(bounds.Left, bounds.Right) - inset) / sheet.Size.Width;
|
||||
Bottom = (Math.Max(bounds.Top, bounds.Bottom) - inset) / sheet.Size.Height;
|
||||
const float Inset = 1 / 128f;
|
||||
Left = (Math.Min(bounds.Left, bounds.Right) + Inset) / sheet.Size.Width;
|
||||
Top = (Math.Min(bounds.Top, bounds.Bottom) + Inset) / sheet.Size.Height;
|
||||
Right = (Math.Max(bounds.Left, bounds.Right) - Inset) / sheet.Size.Width;
|
||||
Bottom = (Math.Max(bounds.Top, bounds.Bottom) - Inset) / sheet.Size.Height;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,31 +11,34 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public delegate ISpriteFrame AdjustFrame(ISpriteFrame input, int index, int total);
|
||||
|
||||
public sealed class SpriteCache : IDisposable
|
||||
{
|
||||
public readonly Dictionary<SheetType, SheetBuilder> SheetBuilders;
|
||||
readonly ISpriteLoader[] loaders;
|
||||
readonly IReadOnlyFileSystem fileSystem;
|
||||
|
||||
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> spriteReservations = new();
|
||||
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> frameReservations = new();
|
||||
readonly Dictionary<string, List<int>> reservationsByFilename = new();
|
||||
readonly Dictionary<
|
||||
int,
|
||||
(ImmutableArray<int> Frames, MiniYamlNode.SourceLocation Location, AdjustFrame AdjustFrame, bool Premultiplied)> spriteReservations = [];
|
||||
readonly Dictionary<string, List<int>> reservationsByFilename = [];
|
||||
|
||||
readonly Dictionary<int, ISpriteFrame[]> resolvedFrames = new();
|
||||
readonly Dictionary<int, Sprite[]> resolvedSprites = new();
|
||||
readonly Dictionary<int, Sprite[]> resolvedSprites = [];
|
||||
|
||||
readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = new();
|
||||
readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = [];
|
||||
|
||||
int nextReservationToken = 1;
|
||||
|
||||
public SpriteCache(IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders, int bgraSheetSize, int indexedSheetSize, int bgraSheetMargin = 1, int indexedSheetMargin = 1)
|
||||
public SpriteCache(
|
||||
IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders, int bgraSheetSize, int indexedSheetSize, int bgraSheetMargin = 1, int indexedSheetMargin = 1)
|
||||
{
|
||||
SheetBuilders = new Dictionary<SheetType, SheetBuilder>
|
||||
{
|
||||
@@ -47,116 +50,122 @@ namespace OpenRA.Graphics
|
||||
this.loaders = loaders;
|
||||
}
|
||||
|
||||
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
|
||||
public int ReserveSprites(string filename, ImmutableArray<int> frames, MiniYamlNode.SourceLocation location,
|
||||
AdjustFrame adjustFrame = null, bool premultiplied = false)
|
||||
{
|
||||
var token = nextReservationToken++;
|
||||
spriteReservations[token] = (frames?.ToArray(), location);
|
||||
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
|
||||
spriteReservations[token] = (frames, location, adjustFrame, premultiplied);
|
||||
reservationsByFilename.GetOrAdd(filename, _ => []).Add(token);
|
||||
return token;
|
||||
}
|
||||
|
||||
public int ReserveFrames(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
|
||||
static ISpriteFrame[] GetFrames(IReadOnlyFileSystem fileSystem, string filename, ISpriteLoader[] loaders)
|
||||
{
|
||||
var token = nextReservationToken++;
|
||||
frameReservations[token] = (frames?.ToArray(), location);
|
||||
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
|
||||
return token;
|
||||
}
|
||||
|
||||
static ISpriteFrame[] GetFrames(IReadOnlyFileSystem fileSystem, string filename, ISpriteLoader[] loaders, out TypeDictionary metadata)
|
||||
{
|
||||
metadata = null;
|
||||
if (!fileSystem.TryOpen(filename, out var stream))
|
||||
return null;
|
||||
|
||||
using (stream)
|
||||
{
|
||||
foreach (var loader in loaders)
|
||||
if (loader.TryParseSprite(stream, filename, out var frames, out metadata))
|
||||
if (loader.TryParseSprite(stream, filename, out var frames, out _))
|
||||
return frames;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ISpriteFrame[] LoadFramesUncached(string filename)
|
||||
{
|
||||
return GetFrames(fileSystem, filename, loaders);
|
||||
}
|
||||
|
||||
public void LoadReservations(ModData modData)
|
||||
{
|
||||
foreach (var sb in SheetBuilders.Values)
|
||||
sb.Current.CreateBuffer();
|
||||
|
||||
var spriteCache = new Dictionary<int, Sprite>();
|
||||
var pendingResolve = new List<(
|
||||
string Filename,
|
||||
int FrameIndex,
|
||||
bool Premultiplied,
|
||||
AdjustFrame AdjustFrame,
|
||||
ISpriteFrame Frame,
|
||||
Sprite[] SpritesForToken)>();
|
||||
foreach (var (filename, tokens) in reservationsByFilename)
|
||||
{
|
||||
modData.LoadScreen?.Display();
|
||||
var loadedFrames = GetFrames(fileSystem, filename, loaders, out _);
|
||||
var loadedFrames = GetFrames(fileSystem, filename, loaders);
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (frameReservations.TryGetValue(token, out var r))
|
||||
{
|
||||
if (loadedFrames != null)
|
||||
{
|
||||
if (r.Frames != null)
|
||||
{
|
||||
var resolved = new ISpriteFrame[loadedFrames.Length];
|
||||
foreach (var i in r.Frames)
|
||||
resolved[i] = loadedFrames[i];
|
||||
resolvedFrames[token] = resolved;
|
||||
}
|
||||
else
|
||||
resolvedFrames[token] = loadedFrames;
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedFrames[token] = null;
|
||||
missingFiles[token] = (filename, r.Location);
|
||||
}
|
||||
}
|
||||
|
||||
if (spriteReservations.TryGetValue(token, out r))
|
||||
if (spriteReservations.TryGetValue(token, out var rs))
|
||||
{
|
||||
if (loadedFrames != null)
|
||||
{
|
||||
var resolved = new Sprite[loadedFrames.Length];
|
||||
var frames = r.Frames ?? Enumerable.Range(0, loadedFrames.Length);
|
||||
foreach (var i in frames)
|
||||
resolved[i] = spriteCache.GetOrAdd(i,
|
||||
f => SheetBuilders[SheetBuilder.FrameTypeToSheetType(loadedFrames[f].Type)].Add(loadedFrames[f]));
|
||||
|
||||
resolvedSprites[token] = resolved;
|
||||
if (rs.Frames != null && rs.Frames.Any(i => i >= loadedFrames.Length))
|
||||
throw new InvalidOperationException($"{rs.Location}: {filename} does not contain frames: " +
|
||||
string.Join(',', rs.Frames.Where(f => f >= loadedFrames.Length)));
|
||||
|
||||
var frames = rs.Frames != null ? rs.Frames : Enumerable.Range(0, loadedFrames.Length);
|
||||
var total = rs.Frames != null ? rs.Frames.Length : loadedFrames.Length;
|
||||
|
||||
var j = 0;
|
||||
foreach (var i in frames)
|
||||
{
|
||||
var frame = loadedFrames[i];
|
||||
if (rs.AdjustFrame != null)
|
||||
frame = rs.AdjustFrame(frame, j++, total);
|
||||
pendingResolve.Add((filename, i, rs.Premultiplied, rs.AdjustFrame, frame, resolved));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedSprites[token] = null;
|
||||
missingFiles[token] = (filename, r.Location);
|
||||
missingFiles[token] = (filename, rs.Location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spriteCache.Clear();
|
||||
}
|
||||
|
||||
spriteReservations.Clear();
|
||||
frameReservations.Clear();
|
||||
spriteReservations.TrimExcess();
|
||||
reservationsByFilename.Clear();
|
||||
reservationsByFilename.TrimExcess();
|
||||
|
||||
// When the sheet builder is adding sprites, it reserves height for the tallest sprite seen along the row.
|
||||
// We can achieve better sheet packing by keeping sprites with similar heights together.
|
||||
var orderedPendingResolve = pendingResolve.OrderBy(x => x.Frame.Size.Height);
|
||||
|
||||
var spriteCache = new Dictionary<(
|
||||
string Filename,
|
||||
int FrameIndex,
|
||||
bool Premultiplied,
|
||||
AdjustFrame AdjustFrame),
|
||||
Sprite>(pendingResolve.Count);
|
||||
foreach (var (filename, frameIndex, premultiplied, adjustFrame, frame, spritesForToken) in orderedPendingResolve)
|
||||
{
|
||||
// Premultiplied and non-premultiplied sprites must be cached separately
|
||||
// to cover the case where the same image is requested in both versions.
|
||||
spritesForToken[frameIndex] = spriteCache.GetOrAdd(
|
||||
(filename, frameIndex, premultiplied, adjustFrame),
|
||||
_ =>
|
||||
{
|
||||
var sheetBuilder = SheetBuilders[SheetBuilder.FrameTypeToSheetType(frame.Type)];
|
||||
return sheetBuilder.Add(frame, premultiplied);
|
||||
});
|
||||
|
||||
modData.LoadScreen?.Display();
|
||||
}
|
||||
|
||||
foreach (var sb in SheetBuilders.Values)
|
||||
sb.Current.ReleaseBuffer();
|
||||
sb.Current?.ReleaseBuffer();
|
||||
}
|
||||
|
||||
public Sprite[] ResolveSprites(int token)
|
||||
{
|
||||
var resolved = resolvedSprites[token];
|
||||
resolvedSprites.Remove(token);
|
||||
if (missingFiles.TryGetValue(token, out var r))
|
||||
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);
|
||||
if (!resolvedSprites.Remove(token, out var resolved))
|
||||
throw new InvalidOperationException($"{nameof(token)} {token} has either already been resolved, or was never reserved via {nameof(ReserveSprites)}");
|
||||
|
||||
return resolved;
|
||||
}
|
||||
resolvedSprites.TrimExcess();
|
||||
|
||||
public ISpriteFrame[] ResolveFrames(int token)
|
||||
{
|
||||
var resolved = resolvedFrames[token];
|
||||
resolvedFrames.Remove(token);
|
||||
if (missingFiles.TryGetValue(token, out var r))
|
||||
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace OpenRA.Graphics
|
||||
|
||||
float deviceScale;
|
||||
|
||||
public SpriteFont(string name, byte[] data, int size, int ascender, float scale, SheetBuilder builder)
|
||||
public SpriteFont(IPlatform platform, string name, byte[] data, int size, int ascender, float scale, SheetBuilder builder)
|
||||
{
|
||||
if (builder.Type != SheetType.BGRA)
|
||||
throw new ArgumentException("The sheet builder must create BGRA sheets.", nameof(builder));
|
||||
@@ -36,7 +36,7 @@ namespace OpenRA.Graphics
|
||||
this.size = size;
|
||||
this.builder = builder;
|
||||
|
||||
font = Game.Renderer.CreateFont(data);
|
||||
font = platform.CreateFont(data);
|
||||
glyphs = new Cache<char, GlyphInfo>(CreateGlyph);
|
||||
contrastGlyphs = new Cache<(char, int), Sprite>(CreateContrastGlyph);
|
||||
dilationElements = new Cache<int, float[]>(CreateCircularWeightMap);
|
||||
|
||||
@@ -22,20 +22,30 @@ namespace OpenRA.Graphics
|
||||
/// </summary>
|
||||
public enum SpriteFrameType
|
||||
{
|
||||
// 8 bit index into an external palette
|
||||
/// <summary>
|
||||
/// 8 bit index into an external palette.
|
||||
/// </summary>
|
||||
Indexed8,
|
||||
|
||||
// 32 bit color such as returned by Color.ToArgb() or the bmp file format
|
||||
// (remember that little-endian systems place the little bits in the first byte!)
|
||||
/// <summary>
|
||||
/// 32 bit color such as returned by Color.ToArgb() or the bmp file format
|
||||
/// (remember that little-endian systems place the little bits in the first byte).
|
||||
/// </summary>
|
||||
Bgra32,
|
||||
|
||||
// Like BGRA, but without an alpha channel
|
||||
/// <summary>
|
||||
/// Like BGRA, but without an alpha channel.
|
||||
/// </summary>
|
||||
Bgr24,
|
||||
|
||||
// 32 bit color in big-endian format, like png
|
||||
/// <summary>
|
||||
/// 32 bit color in big-endian format, like png.
|
||||
/// </summary>
|
||||
Rgba32,
|
||||
|
||||
// Like RGBA, but without an alpha channel
|
||||
/// <summary>
|
||||
/// Like RGBA, but without an alpha channel.
|
||||
/// </summary>
|
||||
Rgb24
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
@@ -17,7 +16,7 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
public class SpriteRenderable : IPalettedRenderable, IModifyableRenderable, IFinalizedRenderable
|
||||
{
|
||||
public static readonly IEnumerable<IRenderable> None = Array.Empty<IRenderable>();
|
||||
public static readonly IEnumerable<IRenderable> None = [];
|
||||
|
||||
readonly Sprite sprite;
|
||||
readonly WPos pos;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
@@ -19,6 +20,7 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
public const int SheetCount = 8;
|
||||
static readonly string[] SheetIndexToTextureName = Exts.MakeArray(SheetCount, i => $"Texture{i}");
|
||||
static readonly int UintSize = Marshal.SizeOf<uint>();
|
||||
|
||||
readonly Renderer renderer;
|
||||
readonly IShader shader;
|
||||
@@ -27,21 +29,21 @@ namespace OpenRA.Graphics
|
||||
readonly Sheet[] sheets = new Sheet[SheetCount];
|
||||
|
||||
BlendMode currentBlend = BlendMode.Alpha;
|
||||
int nv = 0;
|
||||
int ns = 0;
|
||||
int vertexCount = 0;
|
||||
int sheetCount = 0;
|
||||
|
||||
public SpriteRenderer(Renderer renderer, IShader shader)
|
||||
{
|
||||
this.renderer = renderer;
|
||||
this.shader = shader;
|
||||
vertices = renderer.Context.CreateVertices(renderer.TempBufferSize);
|
||||
vertices = renderer.Context.CreateVertices<Vertex>(renderer.TempVertexBufferSize);
|
||||
}
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
if (nv > 0)
|
||||
if (vertexCount > 0)
|
||||
{
|
||||
for (var i = 0; i < ns; i++)
|
||||
for (var i = 0; i < sheetCount; i++)
|
||||
{
|
||||
shader.SetTexture(SheetIndexToTextureName[i], sheets[i].GetTexture());
|
||||
sheets[i] = null;
|
||||
@@ -50,12 +52,11 @@ namespace OpenRA.Graphics
|
||||
renderer.Context.SetBlendMode(currentBlend);
|
||||
shader.PrepareRender();
|
||||
|
||||
// PERF: The renderer may choose to replace vertices with a different temporary buffer.
|
||||
renderer.DrawBatch(ref vertices, nv, PrimitiveType.TriangleList);
|
||||
renderer.DrawQuadBatch(ref vertices, shader, vertexCount);
|
||||
renderer.Context.SetBlendMode(BlendMode.None);
|
||||
|
||||
nv = 0;
|
||||
ns = 0;
|
||||
vertexCount = 0;
|
||||
sheetCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
renderer.CurrentBatchRenderer = this;
|
||||
|
||||
if (s.BlendMode != currentBlend || nv + 6 > renderer.TempBufferSize)
|
||||
if (s.BlendMode != currentBlend || vertexCount + 4 > renderer.TempVertexBufferSize)
|
||||
Flush();
|
||||
|
||||
currentBlend = s.BlendMode;
|
||||
@@ -71,7 +72,7 @@ namespace OpenRA.Graphics
|
||||
// Check if the sheet (or secondary data sheet) have already been mapped
|
||||
var sheet = s.Sheet;
|
||||
var sheetIndex = 0;
|
||||
for (; sheetIndex < ns; sheetIndex++)
|
||||
for (; sheetIndex < sheetCount; sheetIndex++)
|
||||
if (sheets[sheetIndex] == sheet)
|
||||
break;
|
||||
|
||||
@@ -80,7 +81,7 @@ namespace OpenRA.Graphics
|
||||
if (ss != null)
|
||||
{
|
||||
var secondarySheet = ss.SecondarySheet;
|
||||
for (; secondarySheetIndex < ns; secondarySheetIndex++)
|
||||
for (; secondarySheetIndex < sheetCount; secondarySheetIndex++)
|
||||
if (sheets[secondarySheetIndex] == secondarySheet)
|
||||
break;
|
||||
|
||||
@@ -99,22 +100,22 @@ namespace OpenRA.Graphics
|
||||
secondarySheetIndex = ss != null && ss.SecondarySheet != sheet ? 1 : 0;
|
||||
}
|
||||
|
||||
if (sheetIndex >= ns)
|
||||
if (sheetIndex >= sheetCount)
|
||||
{
|
||||
sheets[sheetIndex] = sheet;
|
||||
ns++;
|
||||
sheetCount++;
|
||||
}
|
||||
|
||||
if (secondarySheetIndex >= ns && ss != null)
|
||||
if (secondarySheetIndex >= sheetCount && ss != null)
|
||||
{
|
||||
sheets[secondarySheetIndex] = ss.SecondarySheet;
|
||||
ns++;
|
||||
sheetCount++;
|
||||
}
|
||||
|
||||
return new int2(sheetIndex, secondarySheetIndex);
|
||||
}
|
||||
|
||||
static float ResolveTextureIndex(Sprite s, PaletteReference pal)
|
||||
static int ResolveTextureIndex(Sprite s, PaletteReference pal)
|
||||
{
|
||||
if (pal == null)
|
||||
return 0;
|
||||
@@ -128,20 +129,20 @@ namespace OpenRA.Graphics
|
||||
return pal.TextureIndex;
|
||||
}
|
||||
|
||||
internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, in float3 scale, float rotation = 0f)
|
||||
internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 location, in float3 scale, float rotation = 0f)
|
||||
{
|
||||
var samplers = SetRenderStateForSprite(s);
|
||||
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, float3.Ones,
|
||||
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, vertexCount, scale * s.Size, float3.Ones,
|
||||
1f, rotation);
|
||||
nv += 6;
|
||||
vertexCount += 4;
|
||||
}
|
||||
|
||||
internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, float scale, float rotation = 0f)
|
||||
internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 location, float scale, float rotation = 0f)
|
||||
{
|
||||
var samplers = SetRenderStateForSprite(s);
|
||||
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, float3.Ones,
|
||||
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, vertexCount, scale * s.Size, float3.Ones,
|
||||
1f, rotation);
|
||||
nv += 6;
|
||||
vertexCount += 4;
|
||||
}
|
||||
|
||||
public void DrawSprite(Sprite s, PaletteReference pal, in float3 location, float scale = 1f, float rotation = 0f)
|
||||
@@ -149,13 +150,13 @@ namespace OpenRA.Graphics
|
||||
DrawSprite(s, ResolveTextureIndex(s, pal), location, scale, rotation);
|
||||
}
|
||||
|
||||
internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, float scale, in float3 tint, float alpha,
|
||||
internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 location, float scale, in float3 tint, float alpha,
|
||||
float rotation = 0f)
|
||||
{
|
||||
var samplers = SetRenderStateForSprite(s);
|
||||
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, tint, alpha,
|
||||
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, vertexCount, scale * s.Size, tint, alpha,
|
||||
rotation);
|
||||
nv += 6;
|
||||
vertexCount += 4;
|
||||
}
|
||||
|
||||
public void DrawSprite(Sprite s, PaletteReference pal, in float3 location, float scale, in float3 tint, float alpha,
|
||||
@@ -164,14 +165,14 @@ namespace OpenRA.Graphics
|
||||
DrawSprite(s, ResolveTextureIndex(s, pal), location, scale, tint, alpha, rotation);
|
||||
}
|
||||
|
||||
internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 a, in float3 b, in float3 c, in float3 d, in float3 tint, float alpha)
|
||||
internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 a, in float3 b, in float3 c, in float3 d, in float3 tint, float alpha)
|
||||
{
|
||||
var samplers = SetRenderStateForSprite(s);
|
||||
Util.FastCreateQuad(vertices, a, b, c, d, s, samplers, paletteTextureIndex, tint, alpha, nv);
|
||||
nv += 6;
|
||||
Util.FastCreateQuad(vertices, a, b, c, d, s, samplers, paletteTextureIndex, tint, alpha, vertexCount);
|
||||
vertexCount += 4;
|
||||
}
|
||||
|
||||
public void DrawVertexBuffer(IVertexBuffer<Vertex> buffer, int start, int length, PrimitiveType type, IEnumerable<Sheet> sheets, BlendMode blendMode)
|
||||
public void DrawVertexBuffer(IVertexBuffer<Vertex> buffer, IIndexBuffer indices, int start, int length, IEnumerable<Sheet> sheets, BlendMode blendMode)
|
||||
{
|
||||
var i = 0;
|
||||
foreach (var s in sheets)
|
||||
@@ -185,7 +186,7 @@ namespace OpenRA.Graphics
|
||||
|
||||
renderer.Context.SetBlendMode(blendMode);
|
||||
shader.PrepareRender();
|
||||
renderer.DrawBatch(buffer, start, length, type);
|
||||
renderer.DrawQuadBatch(buffer, indices, shader, length, UintSize * start);
|
||||
renderer.Context.SetBlendMode(BlendMode.None);
|
||||
}
|
||||
|
||||
@@ -196,29 +197,32 @@ namespace OpenRA.Graphics
|
||||
}
|
||||
|
||||
// For RGBAColorRenderer
|
||||
internal void DrawRGBAVertices(Vertex[] v, BlendMode blendMode)
|
||||
internal void DrawRGBAQuad(Vertex[] v, BlendMode blendMode)
|
||||
{
|
||||
renderer.CurrentBatchRenderer = this;
|
||||
|
||||
if (currentBlend != blendMode || nv + v.Length > renderer.TempBufferSize)
|
||||
if (currentBlend != blendMode || vertexCount + 4 > renderer.TempVertexBufferSize)
|
||||
Flush();
|
||||
|
||||
currentBlend = blendMode;
|
||||
Array.Copy(v, 0, vertices, nv, v.Length);
|
||||
nv += v.Length;
|
||||
|
||||
Array.Copy(v, 0, vertices, vertexCount, v.Length);
|
||||
vertexCount += 4;
|
||||
}
|
||||
|
||||
public void SetPalette(ITexture palette, ITexture colorShifts)
|
||||
public void SetPalette(HardwarePalette palette)
|
||||
{
|
||||
shader.SetTexture("Palette", palette);
|
||||
shader.SetTexture("ColorShifts", colorShifts);
|
||||
shader.SetTexture("Palette", palette.Texture);
|
||||
shader.SetTexture("ColorShifts", palette.ColorShifts);
|
||||
shader.SetVec("PaletteRows", palette.Height);
|
||||
}
|
||||
|
||||
public void SetViewportParams(Size sheetSize, int downscale, float depthMargin, int2 scroll)
|
||||
{
|
||||
// Calculate the scale (r1) and offset (r2) that convert from OpenRA viewport pixels
|
||||
// to OpenGL normalized device coordinates (NDC). OpenGL expects coordinates to vary from [-1, 1],
|
||||
// so we rescale viewport pixels to the range [0, 2] using r1 then subtract 1 using r2.
|
||||
// OpenGL only renders x and y coordinates inside [-1, 1] range. We project world coordinates
|
||||
// using p1 to values [0, 2] and then subtract by 1 using p2, where p stands for projection. It's
|
||||
// standard practice for shaders to use a projection matrix, but as we project orthographically
|
||||
// we are able to send less data to the GPU.
|
||||
var width = 2f / (downscale * sheetSize.Width);
|
||||
var height = 2f / (downscale * sheetSize.Height);
|
||||
|
||||
@@ -239,8 +243,8 @@ namespace OpenRA.Graphics
|
||||
var depth = depthMargin != 0f ? 2f / (downscale * (sheetSize.Height + depthMargin)) : 0;
|
||||
shader.SetVec("DepthTextureScale", 128 * depth);
|
||||
shader.SetVec("Scroll", scroll.X, scroll.Y, depthMargin != 0f ? scroll.Y : 0);
|
||||
shader.SetVec("r1", width, height, -depth);
|
||||
shader.SetVec("r2", -1, -1, depthMargin != 0f ? 1 : 0);
|
||||
shader.SetVec("p1", width, height, -depth);
|
||||
shader.SetVec("p2", -1, -1, depthMargin != 0f ? 1 : 0);
|
||||
}
|
||||
|
||||
public void SetDepthPreview(bool enabled, float contrast, float offset)
|
||||
@@ -249,9 +253,9 @@ namespace OpenRA.Graphics
|
||||
shader.SetVec("DepthPreviewParams", contrast, offset);
|
||||
}
|
||||
|
||||
public void SetAntialiasingPixelsPerTexel(float pxPerTx)
|
||||
public void EnablePixelArtScaling(bool enabled)
|
||||
{
|
||||
shader.SetVec("AntialiasPixelsPerTexel", pxPerTx);
|
||||
shader.SetBool("EnablePixelArtScaling", enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public sealed class TerrainSpriteLayer : IDisposable
|
||||
{
|
||||
static readonly int[] CornerVertexMap = { 0, 1, 2, 2, 3, 0 };
|
||||
// PERF: we can reuse the IndexBuffer as all layers have the same size.
|
||||
static readonly ConditionalWeakTable<World, IndexBufferRc> IndexBuffers = [];
|
||||
readonly IndexBufferRc indexBufferWrapper;
|
||||
|
||||
public readonly BlendMode BlendMode;
|
||||
|
||||
@@ -27,8 +30,9 @@ namespace OpenRA.Graphics
|
||||
readonly IVertexBuffer<Vertex> vertexBuffer;
|
||||
readonly Vertex[] vertices;
|
||||
readonly bool[] ignoreTint;
|
||||
readonly HashSet<int> dirtyRows = new();
|
||||
readonly int rowStride;
|
||||
readonly HashSet<int> dirtyRows = [];
|
||||
readonly int indexRowStride;
|
||||
readonly int vertexRowStride;
|
||||
readonly bool restrictToBounds;
|
||||
|
||||
readonly WorldRenderer worldRenderer;
|
||||
@@ -43,19 +47,25 @@ namespace OpenRA.Graphics
|
||||
this.emptySprite = emptySprite;
|
||||
sheets = new Sheet[SpriteRenderer.SheetCount];
|
||||
BlendMode = blendMode;
|
||||
|
||||
map = world.Map;
|
||||
rowStride = 6 * map.MapSize.X;
|
||||
|
||||
vertices = new Vertex[rowStride * map.MapSize.Y];
|
||||
palettes = new PaletteReference[map.MapSize.X * map.MapSize.Y];
|
||||
vertexBuffer = Game.Renderer.Context.CreateVertexBuffer(vertices.Length);
|
||||
vertexRowStride = 4 * map.MapSize.Width;
|
||||
vertices = new Vertex[vertexRowStride * map.MapSize.Height];
|
||||
vertexBuffer = Game.Renderer.Context.CreateEmptyVertexBuffer<Vertex>(vertices.Length);
|
||||
|
||||
indexRowStride = 6 * map.MapSize.Width;
|
||||
lock (IndexBuffers)
|
||||
{
|
||||
indexBufferWrapper = IndexBuffers.GetValue(world, world => new IndexBufferRc(world));
|
||||
indexBufferWrapper.AddRef();
|
||||
}
|
||||
|
||||
palettes = new PaletteReference[map.MapSize.Width * map.MapSize.Height];
|
||||
wr.PaletteInvalidated += UpdatePaletteIndices;
|
||||
|
||||
if (wr.TerrainLighting != null)
|
||||
{
|
||||
ignoreTint = new bool[rowStride * map.MapSize.Y];
|
||||
ignoreTint = new bool[vertexRowStride * map.MapSize.Height];
|
||||
wr.TerrainLighting.CellChanged += UpdateTint;
|
||||
}
|
||||
}
|
||||
@@ -65,11 +75,12 @@ namespace OpenRA.Graphics
|
||||
for (var i = 0; i < vertices.Length; i++)
|
||||
{
|
||||
var v = vertices[i];
|
||||
var p = palettes[i / 6]?.TextureIndex ?? 0;
|
||||
vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, p, v.C, v.R, v.G, v.B, v.A);
|
||||
var p = palettes[i / 4]?.TextureIndex ?? 0;
|
||||
var c = (uint)((p & 0xFFFF) << 16) | (v.C & 0xFFFF);
|
||||
vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, c, v.R, v.G, v.B, v.A);
|
||||
}
|
||||
|
||||
for (var row = 0; row < map.MapSize.Y; row++)
|
||||
for (var row = 0; row < map.MapSize.Height; row++)
|
||||
dirtyRows.Add(row);
|
||||
}
|
||||
|
||||
@@ -97,13 +108,13 @@ namespace OpenRA.Graphics
|
||||
|
||||
void UpdateTint(MPos uv)
|
||||
{
|
||||
var offset = rowStride * uv.V + 6 * uv.U;
|
||||
var offset = vertexRowStride * uv.V + 4 * uv.U;
|
||||
if (ignoreTint[offset])
|
||||
{
|
||||
for (var i = 0; i < 6; i++)
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
var v = vertices[offset + i];
|
||||
vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.P, v.C, v.A * float3.Ones, v.A);
|
||||
vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.C, v.A * float3.Ones, v.A);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -125,10 +136,10 @@ namespace OpenRA.Graphics
|
||||
|
||||
// Apply tint directly to the underlying vertices
|
||||
// This saves us from having to re-query the sprite information, which has not changed
|
||||
for (var i = 0; i < 6; i++)
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
var v = vertices[offset + i];
|
||||
vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.P, v.C, v.A * weights[CornerVertexMap[i]], v.A);
|
||||
vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.C, v.A * weights[i], v.A);
|
||||
}
|
||||
|
||||
dirtyRows.Add(uv.V);
|
||||
@@ -180,9 +191,9 @@ namespace OpenRA.Graphics
|
||||
if (!map.Tiles.Contains(uv))
|
||||
return;
|
||||
|
||||
var offset = rowStride * uv.V + 6 * uv.U;
|
||||
var offset = vertexRowStride * uv.V + 4 * uv.U;
|
||||
Util.FastCreateQuad(vertices, pos, sprite, samplers, palette?.TextureIndex ?? 0, offset, scale * sprite.Size, alpha * float3.Ones, alpha);
|
||||
palettes[uv.V * map.MapSize.X + uv.U] = palette;
|
||||
palettes[uv.V * map.MapSize.Width + uv.U] = palette;
|
||||
|
||||
if (worldRenderer.TerrainLighting != null)
|
||||
{
|
||||
@@ -198,8 +209,8 @@ namespace OpenRA.Graphics
|
||||
var cells = restrictToBounds ? viewport.VisibleCellsInsideBounds : viewport.AllVisibleCells;
|
||||
|
||||
// Only draw the rows that are visible.
|
||||
var firstRow = cells.CandidateMapCoords.TopLeft.V.Clamp(0, map.MapSize.Y);
|
||||
var lastRow = (cells.CandidateMapCoords.BottomRight.V + 1).Clamp(firstRow, map.MapSize.Y);
|
||||
var firstRow = cells.CandidateMapCoords.TopLeft.V.Clamp(0, map.MapSize.Height);
|
||||
var lastRow = (cells.CandidateMapCoords.BottomRight.V + 1).Clamp(firstRow, map.MapSize.Height);
|
||||
|
||||
Game.Renderer.Flush();
|
||||
|
||||
@@ -209,13 +220,13 @@ namespace OpenRA.Graphics
|
||||
if (!dirtyRows.Remove(row))
|
||||
continue;
|
||||
|
||||
var rowOffset = rowStride * row;
|
||||
vertexBuffer.SetData(vertices, rowOffset, rowOffset, rowStride);
|
||||
var rowOffset = vertexRowStride * row;
|
||||
vertexBuffer.SetData(vertices, rowOffset, rowOffset, vertexRowStride);
|
||||
}
|
||||
|
||||
Game.Renderer.WorldSpriteRenderer.DrawVertexBuffer(
|
||||
vertexBuffer, rowStride * firstRow, rowStride * (lastRow - firstRow),
|
||||
PrimitiveType.TriangleList, sheets, BlendMode);
|
||||
vertexBuffer, indexBufferWrapper.Buffer, indexRowStride * firstRow,
|
||||
indexRowStride * (lastRow - firstRow), sheets, BlendMode);
|
||||
|
||||
Game.Renderer.Flush();
|
||||
}
|
||||
@@ -227,6 +238,30 @@ namespace OpenRA.Graphics
|
||||
worldRenderer.TerrainLighting.CellChanged -= UpdateTint;
|
||||
|
||||
vertexBuffer.Dispose();
|
||||
|
||||
lock (IndexBuffers)
|
||||
indexBufferWrapper.Dispose();
|
||||
}
|
||||
|
||||
sealed class IndexBufferRc : IDisposable
|
||||
{
|
||||
public IIndexBuffer Buffer;
|
||||
int count;
|
||||
|
||||
public IndexBufferRc(World world)
|
||||
{
|
||||
Buffer = Game.Renderer.Context.CreateIndexBuffer(
|
||||
Util.CreateQuadIndices(world.Map.MapSize.Width * world.Map.MapSize.Height));
|
||||
}
|
||||
|
||||
public void AddRef() { count++; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
count--;
|
||||
if (count == 0)
|
||||
Buffer.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ namespace OpenRA.Graphics
|
||||
readonly float alpha;
|
||||
readonly float rotation = 0f;
|
||||
|
||||
public UISpriteRenderable(Sprite sprite, WPos effectiveWorldPos, int2 screenPos, int zOffset, PaletteReference palette, float scale = 1f, float alpha = 1f, float rotation = 0f)
|
||||
public UISpriteRenderable(Sprite sprite, WPos effectiveWorldPos, int2 screenPos, int zOffset, PaletteReference palette,
|
||||
float scale = 1f, float alpha = 1f, float rotation = 0f)
|
||||
{
|
||||
this.sprite = sprite;
|
||||
Pos = effectiveWorldPos;
|
||||
@@ -47,7 +48,8 @@ namespace OpenRA.Graphics
|
||||
public PaletteReference Palette { get; }
|
||||
public int ZOffset { get; }
|
||||
|
||||
public IPalettedRenderable WithPalette(PaletteReference newPalette) { return new UISpriteRenderable(sprite, Pos, screenPos, ZOffset, newPalette, scale, alpha, rotation); }
|
||||
public IPalettedRenderable WithPalette(PaletteReference newPalette) =>
|
||||
new UISpriteRenderable(sprite, Pos, screenPos, ZOffset, newPalette, scale, alpha, rotation);
|
||||
public IRenderable WithZOffset(int newOffset) { return this; }
|
||||
public IRenderable OffsetBy(in WVec vec) { return this; }
|
||||
public IRenderable AsDecoration() { return this; }
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using OpenRA.FileFormats;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
@@ -18,9 +19,19 @@ namespace OpenRA.Graphics
|
||||
public static class Util
|
||||
{
|
||||
// yes, our channel order is nuts.
|
||||
static readonly int[] ChannelMasks = { 2, 1, 0, 3 };
|
||||
static readonly int[] ChannelMasks = [2, 1, 0, 3];
|
||||
|
||||
public static void FastCreateQuad(Vertex[] vertices, in float3 o, Sprite r, int2 samplers, float paletteTextureIndex, int nv,
|
||||
public static uint[] CreateQuadIndices(int quads)
|
||||
{
|
||||
var indices = new uint[quads * 6];
|
||||
ReadOnlySpan<uint> cornerVertexMap = [0, 1, 2, 2, 3, 0];
|
||||
for (var i = 0; i < indices.Length; i++)
|
||||
indices[i] = cornerVertexMap[i % 6] + (uint)(4 * (i / 6));
|
||||
|
||||
return indices;
|
||||
}
|
||||
|
||||
public static void FastCreateQuad(Vertex[] vertices, in float3 o, Sprite r, int2 samplers, int paletteTextureIndex, int nv,
|
||||
in float3 size, in float3 tint, float alpha, float rotation = 0f)
|
||||
{
|
||||
float3 a, b, c, d;
|
||||
@@ -62,7 +73,7 @@ namespace OpenRA.Graphics
|
||||
|
||||
public static void FastCreateQuad(Vertex[] vertices,
|
||||
in float3 a, in float3 b, in float3 c, in float3 d,
|
||||
Sprite r, int2 samplers, float paletteTextureIndex,
|
||||
Sprite r, int2 samplers, int paletteTextureIndex,
|
||||
in float3 tint, float alpha, int nv)
|
||||
{
|
||||
float sl = 0;
|
||||
@@ -84,76 +95,33 @@ namespace OpenRA.Graphics
|
||||
attribC |= samplers.Y << 9;
|
||||
}
|
||||
|
||||
var fAttribC = (float)attribC;
|
||||
vertices[nv] = new Vertex(a, r.Left, r.Top, sl, st, paletteTextureIndex, fAttribC, tint, alpha);
|
||||
vertices[nv + 1] = new Vertex(b, r.Right, r.Top, sr, st, paletteTextureIndex, fAttribC, tint, alpha);
|
||||
vertices[nv + 2] = new Vertex(c, r.Right, r.Bottom, sr, sb, paletteTextureIndex, fAttribC, tint, alpha);
|
||||
vertices[nv + 3] = new Vertex(c, r.Right, r.Bottom, sr, sb, paletteTextureIndex, fAttribC, tint, alpha);
|
||||
vertices[nv + 4] = new Vertex(d, r.Left, r.Bottom, sl, sb, paletteTextureIndex, fAttribC, tint, alpha);
|
||||
vertices[nv + 5] = new Vertex(a, r.Left, r.Top, sl, st, paletteTextureIndex, fAttribC, tint, alpha);
|
||||
attribC |= (paletteTextureIndex & 0xFFFF) << 16;
|
||||
|
||||
var uAttribC = (uint)attribC;
|
||||
vertices[nv] = new Vertex(a, r.Left, r.Top, sl, st, uAttribC, tint, alpha);
|
||||
vertices[nv + 1] = new Vertex(b, r.Right, r.Top, sr, st, uAttribC, tint, alpha);
|
||||
vertices[nv + 2] = new Vertex(c, r.Right, r.Bottom, sr, sb, uAttribC, tint, alpha);
|
||||
vertices[nv + 3] = new Vertex(d, r.Left, r.Bottom, sl, sb, uAttribC, tint, alpha);
|
||||
}
|
||||
|
||||
public static void FastCopyIntoChannel(Sprite dest, byte[] src, SpriteFrameType srcType)
|
||||
public static void FastCopyIntoChannel(Sprite dest, byte[] src, SpriteFrameType srcType, bool premultiplied = false)
|
||||
{
|
||||
var destData = dest.Sheet.GetData();
|
||||
var stride = dest.Sheet.Size.Width;
|
||||
var x = dest.Bounds.Left;
|
||||
var y = dest.Bounds.Top;
|
||||
var width = dest.Bounds.Width;
|
||||
var height = dest.Bounds.Height;
|
||||
|
||||
if (dest.Channel == TextureChannel.RGBA)
|
||||
{
|
||||
var destStride = dest.Sheet.Size.Width;
|
||||
unsafe
|
||||
{
|
||||
// Cast the data to an int array so we can copy the src data directly
|
||||
fixed (byte* bd = &destData[0])
|
||||
{
|
||||
var data = (int*)bd;
|
||||
var x = dest.Bounds.Left;
|
||||
var y = dest.Bounds.Top;
|
||||
|
||||
var k = 0;
|
||||
for (var j = 0; j < height; j++)
|
||||
{
|
||||
for (var i = 0; i < width; i++)
|
||||
{
|
||||
byte r, g, b, a;
|
||||
switch (srcType)
|
||||
{
|
||||
case SpriteFrameType.Bgra32:
|
||||
case SpriteFrameType.Bgr24:
|
||||
{
|
||||
b = src[k++];
|
||||
g = src[k++];
|
||||
r = src[k++];
|
||||
a = srcType == SpriteFrameType.Bgra32 ? src[k++] : (byte)255;
|
||||
break;
|
||||
}
|
||||
|
||||
case SpriteFrameType.Rgba32:
|
||||
case SpriteFrameType.Rgb24:
|
||||
{
|
||||
r = src[k++];
|
||||
g = src[k++];
|
||||
b = src[k++];
|
||||
a = srcType == SpriteFrameType.Rgba32 ? src[k++] : (byte)255;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown SpriteFrameType {srcType}");
|
||||
}
|
||||
|
||||
var cc = Color.FromArgb(a, r, g, b);
|
||||
data[(y + j) * destStride + x + i] = PremultiplyAlpha(cc).ToArgb();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CopyIntoRgba(src, srcType, premultiplied, destData, x, y, width, height, stride);
|
||||
}
|
||||
else
|
||||
{
|
||||
var destStride = dest.Sheet.Size.Width * 4;
|
||||
var destOffset = destStride * dest.Bounds.Top + dest.Bounds.Left * 4 + ChannelMasks[(int)dest.Channel];
|
||||
// Copy into single channel of destination.
|
||||
var destStride = stride * 4;
|
||||
var destOffset = destStride * y + x * 4 + ChannelMasks[(int)dest.Channel];
|
||||
var destSkip = destStride - 4 * width;
|
||||
|
||||
var srcOffset = 0;
|
||||
@@ -170,56 +138,119 @@ namespace OpenRA.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
static void CopyIntoRgba(
|
||||
byte[] src, SpriteFrameType srcType, bool premultiplied, byte[] dest, int x, int y, int width, int height, int stride)
|
||||
{
|
||||
var si = 0;
|
||||
var di = y * stride + x;
|
||||
var d = MemoryMarshal.Cast<byte, uint>(dest);
|
||||
|
||||
// SpriteFrameType.Brga32 is a common source format, and it matches the destination format.
|
||||
// Provide a fast past that just performs memory copies.
|
||||
if (srcType == SpriteFrameType.Bgra32)
|
||||
{
|
||||
var s = MemoryMarshal.Cast<byte, uint>(src);
|
||||
for (var h = 0; h < height; h++)
|
||||
{
|
||||
s[si..(si + width)].CopyTo(d[di..(di + width)]);
|
||||
|
||||
if (!premultiplied)
|
||||
{
|
||||
for (var w = 0; w < width; w++)
|
||||
{
|
||||
d[di] = PremultiplyAlpha(Color.FromArgb(d[di])).ToArgb();
|
||||
di++;
|
||||
}
|
||||
|
||||
di -= width;
|
||||
}
|
||||
|
||||
si += width;
|
||||
di += stride;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (var h = 0; h < height; h++)
|
||||
{
|
||||
for (var w = 0; w < width; w++)
|
||||
{
|
||||
byte r, g, b, a;
|
||||
switch (srcType)
|
||||
{
|
||||
case SpriteFrameType.Bgra32:
|
||||
case SpriteFrameType.Bgr24:
|
||||
b = src[si++];
|
||||
g = src[si++];
|
||||
r = src[si++];
|
||||
a = srcType == SpriteFrameType.Bgra32 ? src[si++] : byte.MaxValue;
|
||||
break;
|
||||
|
||||
case SpriteFrameType.Rgba32:
|
||||
case SpriteFrameType.Rgb24:
|
||||
r = src[si++];
|
||||
g = src[si++];
|
||||
b = src[si++];
|
||||
a = srcType == SpriteFrameType.Rgba32 ? src[si++] : byte.MaxValue;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown SpriteFrameType {srcType}");
|
||||
}
|
||||
|
||||
var c = Color.FromArgb(a, r, g, b);
|
||||
if (!premultiplied)
|
||||
c = PremultiplyAlpha(c);
|
||||
d[di++] = c.ToArgb();
|
||||
}
|
||||
|
||||
di += stride - width;
|
||||
}
|
||||
}
|
||||
|
||||
public static void FastCopyIntoSprite(Sprite dest, Png src)
|
||||
{
|
||||
var destData = dest.Sheet.GetData();
|
||||
var destStride = dest.Sheet.Size.Width;
|
||||
var stride = dest.Sheet.Size.Width;
|
||||
var x = dest.Bounds.Left;
|
||||
var y = dest.Bounds.Top;
|
||||
var width = dest.Bounds.Width;
|
||||
var height = dest.Bounds.Height;
|
||||
|
||||
unsafe
|
||||
var si = 0;
|
||||
var di = y * stride + x;
|
||||
var d = MemoryMarshal.Cast<byte, uint>(destData);
|
||||
|
||||
for (var h = 0; h < height; h++)
|
||||
{
|
||||
// Cast the data to an int array so we can copy the src data directly
|
||||
fixed (byte* bd = &destData[0])
|
||||
for (var w = 0; w < width; w++)
|
||||
{
|
||||
var data = (int*)bd;
|
||||
var x = dest.Bounds.Left;
|
||||
var y = dest.Bounds.Top;
|
||||
|
||||
var k = 0;
|
||||
for (var j = 0; j < height; j++)
|
||||
Color c;
|
||||
switch (src.Type)
|
||||
{
|
||||
for (var i = 0; i < width; i++)
|
||||
{
|
||||
Color cc;
|
||||
switch (src.Type)
|
||||
{
|
||||
case SpriteFrameType.Indexed8:
|
||||
{
|
||||
cc = src.Palette[src.Data[k++]];
|
||||
break;
|
||||
}
|
||||
case SpriteFrameType.Indexed8:
|
||||
c = src.Palette[src.Data[si++]];
|
||||
break;
|
||||
|
||||
case SpriteFrameType.Rgba32:
|
||||
case SpriteFrameType.Rgb24:
|
||||
{
|
||||
var r = src.Data[k++];
|
||||
var g = src.Data[k++];
|
||||
var b = src.Data[k++];
|
||||
var a = src.Type == SpriteFrameType.Rgba32 ? src.Data[k++] : (byte)255;
|
||||
cc = Color.FromArgb(a, r, g, b);
|
||||
break;
|
||||
}
|
||||
case SpriteFrameType.Rgba32:
|
||||
case SpriteFrameType.Rgb24:
|
||||
var r = src.Data[si++];
|
||||
var g = src.Data[si++];
|
||||
var b = src.Data[si++];
|
||||
var a = src.Type == SpriteFrameType.Rgba32 ? src.Data[si++] : byte.MaxValue;
|
||||
c = Color.FromArgb(a, r, g, b);
|
||||
break;
|
||||
|
||||
// Pngs don't support BGR[A], so no need to include them here
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown SpriteFrameType {src.Type}");
|
||||
}
|
||||
|
||||
data[(y + j) * destStride + x + i] = PremultiplyAlpha(cc).ToArgb();
|
||||
}
|
||||
// PNGs don't support BGR[A], so no need to include them here
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown SpriteFrameType {src.Type}");
|
||||
}
|
||||
|
||||
d[di++] = PremultiplyAlpha(c).ToArgb();
|
||||
}
|
||||
|
||||
di += stride - width;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,13 +277,13 @@ namespace OpenRA.Graphics
|
||||
size.X * angleSin - size.Y * angleCos,
|
||||
(size.X * angleSin - size.Y * angleCos) * size.Z / size.Y);
|
||||
|
||||
return new float3[]
|
||||
{
|
||||
return
|
||||
[
|
||||
center - ra,
|
||||
center + rb,
|
||||
center + ra,
|
||||
center - rb
|
||||
};
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -305,239 +336,5 @@ namespace OpenRA.Graphics
|
||||
(int)((byte)(t * a2 * c2.G + 0.5f) + (1 - t) * (byte)(a1 * c1.G + 0.5f)),
|
||||
(int)((byte)(t * a2 * c2.B + 0.5f) + (1 - t) * (byte)(a1 * c1.B + 0.5f))));
|
||||
}
|
||||
|
||||
public static float[] IdentityMatrix()
|
||||
{
|
||||
return Exts.MakeArray(16, j => (j % 5 == 0) ? 1.0f : 0);
|
||||
}
|
||||
|
||||
public static float[] ScaleMatrix(float sx, float sy, float sz)
|
||||
{
|
||||
var mtx = IdentityMatrix();
|
||||
mtx[0] = sx;
|
||||
mtx[5] = sy;
|
||||
mtx[10] = sz;
|
||||
return mtx;
|
||||
}
|
||||
|
||||
public static float[] TranslationMatrix(float x, float y, float z)
|
||||
{
|
||||
var mtx = IdentityMatrix();
|
||||
mtx[12] = x;
|
||||
mtx[13] = y;
|
||||
mtx[14] = z;
|
||||
return mtx;
|
||||
}
|
||||
|
||||
public static float[] MatrixMultiply(float[] lhs, float[] rhs)
|
||||
{
|
||||
var mtx = new float[16];
|
||||
for (var i = 0; i < 4; i++)
|
||||
for (var j = 0; j < 4; j++)
|
||||
{
|
||||
mtx[4 * i + j] = 0;
|
||||
for (var k = 0; k < 4; k++)
|
||||
mtx[4 * i + j] += lhs[4 * k + j] * rhs[4 * i + k];
|
||||
}
|
||||
|
||||
return mtx;
|
||||
}
|
||||
|
||||
public static float[] MatrixVectorMultiply(float[] mtx, float[] vec)
|
||||
{
|
||||
var ret = new float[4];
|
||||
for (var j = 0; j < 4; j++)
|
||||
{
|
||||
ret[j] = 0;
|
||||
for (var k = 0; k < 4; k++)
|
||||
ret[j] += mtx[4 * k + j] * vec[k];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static float[] MatrixInverse(float[] m)
|
||||
{
|
||||
var mtx = new float[16];
|
||||
|
||||
mtx[0] = m[5] * m[10] * m[15] -
|
||||
m[5] * m[11] * m[14] -
|
||||
m[9] * m[6] * m[15] +
|
||||
m[9] * m[7] * m[14] +
|
||||
m[13] * m[6] * m[11] -
|
||||
m[13] * m[7] * m[10];
|
||||
|
||||
mtx[4] = -m[4] * m[10] * m[15] +
|
||||
m[4] * m[11] * m[14] +
|
||||
m[8] * m[6] * m[15] -
|
||||
m[8] * m[7] * m[14] -
|
||||
m[12] * m[6] * m[11] +
|
||||
m[12] * m[7] * m[10];
|
||||
|
||||
mtx[8] = m[4] * m[9] * m[15] -
|
||||
m[4] * m[11] * m[13] -
|
||||
m[8] * m[5] * m[15] +
|
||||
m[8] * m[7] * m[13] +
|
||||
m[12] * m[5] * m[11] -
|
||||
m[12] * m[7] * m[9];
|
||||
|
||||
mtx[12] = -m[4] * m[9] * m[14] +
|
||||
m[4] * m[10] * m[13] +
|
||||
m[8] * m[5] * m[14] -
|
||||
m[8] * m[6] * m[13] -
|
||||
m[12] * m[5] * m[10] +
|
||||
m[12] * m[6] * m[9];
|
||||
|
||||
mtx[1] = -m[1] * m[10] * m[15] +
|
||||
m[1] * m[11] * m[14] +
|
||||
m[9] * m[2] * m[15] -
|
||||
m[9] * m[3] * m[14] -
|
||||
m[13] * m[2] * m[11] +
|
||||
m[13] * m[3] * m[10];
|
||||
|
||||
mtx[5] = m[0] * m[10] * m[15] -
|
||||
m[0] * m[11] * m[14] -
|
||||
m[8] * m[2] * m[15] +
|
||||
m[8] * m[3] * m[14] +
|
||||
m[12] * m[2] * m[11] -
|
||||
m[12] * m[3] * m[10];
|
||||
|
||||
mtx[9] = -m[0] * m[9] * m[15] +
|
||||
m[0] * m[11] * m[13] +
|
||||
m[8] * m[1] * m[15] -
|
||||
m[8] * m[3] * m[13] -
|
||||
m[12] * m[1] * m[11] +
|
||||
m[12] * m[3] * m[9];
|
||||
|
||||
mtx[13] = m[0] * m[9] * m[14] -
|
||||
m[0] * m[10] * m[13] -
|
||||
m[8] * m[1] * m[14] +
|
||||
m[8] * m[2] * m[13] +
|
||||
m[12] * m[1] * m[10] -
|
||||
m[12] * m[2] * m[9];
|
||||
|
||||
mtx[2] = m[1] * m[6] * m[15] -
|
||||
m[1] * m[7] * m[14] -
|
||||
m[5] * m[2] * m[15] +
|
||||
m[5] * m[3] * m[14] +
|
||||
m[13] * m[2] * m[7] -
|
||||
m[13] * m[3] * m[6];
|
||||
|
||||
mtx[6] = -m[0] * m[6] * m[15] +
|
||||
m[0] * m[7] * m[14] +
|
||||
m[4] * m[2] * m[15] -
|
||||
m[4] * m[3] * m[14] -
|
||||
m[12] * m[2] * m[7] +
|
||||
m[12] * m[3] * m[6];
|
||||
|
||||
mtx[10] = m[0] * m[5] * m[15] -
|
||||
m[0] * m[7] * m[13] -
|
||||
m[4] * m[1] * m[15] +
|
||||
m[4] * m[3] * m[13] +
|
||||
m[12] * m[1] * m[7] -
|
||||
m[12] * m[3] * m[5];
|
||||
|
||||
mtx[14] = -m[0] * m[5] * m[14] +
|
||||
m[0] * m[6] * m[13] +
|
||||
m[4] * m[1] * m[14] -
|
||||
m[4] * m[2] * m[13] -
|
||||
m[12] * m[1] * m[6] +
|
||||
m[12] * m[2] * m[5];
|
||||
|
||||
mtx[3] = -m[1] * m[6] * m[11] +
|
||||
m[1] * m[7] * m[10] +
|
||||
m[5] * m[2] * m[11] -
|
||||
m[5] * m[3] * m[10] -
|
||||
m[9] * m[2] * m[7] +
|
||||
m[9] * m[3] * m[6];
|
||||
|
||||
mtx[7] = m[0] * m[6] * m[11] -
|
||||
m[0] * m[7] * m[10] -
|
||||
m[4] * m[2] * m[11] +
|
||||
m[4] * m[3] * m[10] +
|
||||
m[8] * m[2] * m[7] -
|
||||
m[8] * m[3] * m[6];
|
||||
|
||||
mtx[11] = -m[0] * m[5] * m[11] +
|
||||
m[0] * m[7] * m[9] +
|
||||
m[4] * m[1] * m[11] -
|
||||
m[4] * m[3] * m[9] -
|
||||
m[8] * m[1] * m[7] +
|
||||
m[8] * m[3] * m[5];
|
||||
|
||||
mtx[15] = m[0] * m[5] * m[10] -
|
||||
m[0] * m[6] * m[9] -
|
||||
m[4] * m[1] * m[10] +
|
||||
m[4] * m[2] * m[9] +
|
||||
m[8] * m[1] * m[6] -
|
||||
m[8] * m[2] * m[5];
|
||||
|
||||
var det = m[0] * mtx[0] + m[1] * mtx[4] + m[2] * mtx[8] + m[3] * mtx[12];
|
||||
if (det == 0)
|
||||
return null;
|
||||
|
||||
for (var i = 0; i < 16; i++)
|
||||
mtx[i] *= 1 / det;
|
||||
|
||||
return mtx;
|
||||
}
|
||||
|
||||
public static float[] MakeFloatMatrix(Int32Matrix4x4 imtx)
|
||||
{
|
||||
var multipler = 1f / imtx.M44;
|
||||
return new[]
|
||||
{
|
||||
imtx.M11 * multipler,
|
||||
imtx.M12 * multipler,
|
||||
imtx.M13 * multipler,
|
||||
imtx.M14 * multipler,
|
||||
|
||||
imtx.M21 * multipler,
|
||||
imtx.M22 * multipler,
|
||||
imtx.M23 * multipler,
|
||||
imtx.M24 * multipler,
|
||||
|
||||
imtx.M31 * multipler,
|
||||
imtx.M32 * multipler,
|
||||
imtx.M33 * multipler,
|
||||
imtx.M34 * multipler,
|
||||
|
||||
imtx.M41 * multipler,
|
||||
imtx.M42 * multipler,
|
||||
imtx.M43 * multipler,
|
||||
imtx.M44 * multipler,
|
||||
};
|
||||
}
|
||||
|
||||
public static float[] MatrixAABBMultiply(float[] mtx, float[] bounds)
|
||||
{
|
||||
// Corner offsets
|
||||
var ix = new uint[] { 0, 0, 0, 0, 3, 3, 3, 3 };
|
||||
var iy = new uint[] { 1, 1, 4, 4, 1, 1, 4, 4 };
|
||||
var iz = new uint[] { 2, 5, 2, 5, 2, 5, 2, 5 };
|
||||
|
||||
// Vectors to opposing corner
|
||||
var ret = new[]
|
||||
{
|
||||
float.MaxValue, float.MaxValue, float.MaxValue,
|
||||
float.MinValue, float.MinValue, float.MinValue
|
||||
};
|
||||
|
||||
// Transform vectors and find new bounding box
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
var vec = new[] { bounds[ix[i]], bounds[iy[i]], bounds[iz[i]], 1 };
|
||||
var tvec = MatrixVectorMultiply(mtx, vec);
|
||||
|
||||
ret[0] = Math.Min(ret[0], tvec[0] / tvec[3]);
|
||||
ret[1] = Math.Min(ret[1], tvec[1] / tvec[3]);
|
||||
ret[2] = Math.Min(ret[2], tvec[2] / tvec[3]);
|
||||
ret[3] = Math.Max(ret[3], tvec[0] / tvec[3]);
|
||||
ret[4] = Math.Max(ret[4], tvec[1] / tvec[3]);
|
||||
ret[5] = Math.Max(ret[5], tvec[2] / tvec[3]);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,27 +23,42 @@ namespace OpenRA.Graphics
|
||||
public readonly float S, T, U, V;
|
||||
|
||||
// Palette and channel flags
|
||||
public readonly float P, C;
|
||||
public readonly uint C;
|
||||
|
||||
// Color tint
|
||||
public readonly float R, G, B, A;
|
||||
|
||||
public Vertex(in float3 xyz, float s, float t, float u, float v, float p, float c)
|
||||
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, p, c, float3.Ones, 1f) { }
|
||||
public Vertex(in float3 xyz, float s, float t, float u, float v, uint c)
|
||||
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, c, float3.Ones, 1f) { }
|
||||
|
||||
public Vertex(in float3 xyz, float s, float t, float u, float v, float p, float c, in float3 tint, float a)
|
||||
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, p, c, tint.X, tint.Y, tint.Z, a) { }
|
||||
public Vertex(in float3 xyz, float s, float t, float u, float v, uint c, in float3 tint, float a)
|
||||
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, c, tint.X, tint.Y, tint.Z, a) { }
|
||||
|
||||
public Vertex(float x, float y, float z, float s, float t, float u, float v, float p, float c, in float3 tint, float a)
|
||||
: this(x, y, z, s, t, u, v, p, c, tint.X, tint.Y, tint.Z, a) { }
|
||||
public Vertex(float x, float y, float z, float s, float t, float u, float v, uint c, in float3 tint, float a)
|
||||
: this(x, y, z, s, t, u, v, c, tint.X, tint.Y, tint.Z, a) { }
|
||||
|
||||
public Vertex(float x, float y, float z, float s, float t, float u, float v, float p, float c, float r, float g, float b, float a)
|
||||
public Vertex(float x, float y, float z, float s, float t, float u, float v, uint c, float r, float g, float b, float a)
|
||||
{
|
||||
X = x; Y = y; Z = z;
|
||||
S = s; T = t;
|
||||
U = u; V = v;
|
||||
P = p; C = c;
|
||||
C = c;
|
||||
R = r; G = g; B = b; A = a;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CombinedShaderBindings : ShaderBindings
|
||||
{
|
||||
public CombinedShaderBindings()
|
||||
: base("combined")
|
||||
{ }
|
||||
|
||||
public override ShaderVertexAttribute[] Attributes { get; } =
|
||||
[
|
||||
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
|
||||
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
|
||||
new ShaderVertexAttribute("aVertexAttributes", ShaderVertexAttributeType.UInt, 1, 28),
|
||||
new ShaderVertexAttribute("aVertexTint", ShaderVertexAttributeType.Float, 4, 32)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
@@ -49,14 +50,13 @@ namespace OpenRA.Graphics
|
||||
readonly Size tileSize;
|
||||
|
||||
// Viewport geometry (world-px)
|
||||
public int2 CenterLocation { get; private set; }
|
||||
public float2 CenterLocation { get; private set; }
|
||||
|
||||
public WPos CenterPosition => worldRenderer.ProjectedPosition(CenterLocation);
|
||||
public WPos CenterPosition => worldRenderer.ProjectedPosition(CenterLocation.ToInt2());
|
||||
|
||||
public Rectangle Rectangle => new(TopLeft, new Size(viewportSize.X, viewportSize.Y));
|
||||
public int2 TopLeft => CenterLocation - viewportSize / 2;
|
||||
public int2 BottomRight => CenterLocation + viewportSize / 2;
|
||||
int2 viewportSize;
|
||||
public int2 TopLeft => CenterLocation.ToInt2() - ViewportSize.ToInt2() / 2;
|
||||
public int2 BottomRight => CenterLocation.ToInt2() + ViewportSize.ToInt2() / 2;
|
||||
public Size ViewportSize { get; private set; }
|
||||
ProjectedCellRegion cells;
|
||||
bool cellsDirty = true;
|
||||
|
||||
@@ -69,6 +69,10 @@ namespace OpenRA.Graphics
|
||||
bool unlockMinZoom;
|
||||
float unlockedMinZoomScale;
|
||||
float unlockedMinZoom = 1f;
|
||||
float defaultScale;
|
||||
bool overrideUserScale;
|
||||
|
||||
public Func<float2> ViewportCenterProvider;
|
||||
|
||||
public float Zoom
|
||||
{
|
||||
@@ -77,7 +81,7 @@ namespace OpenRA.Graphics
|
||||
private set
|
||||
{
|
||||
zoom = value;
|
||||
viewportSize = (1f / zoom * new float2(Game.Renderer.NativeResolution)).ToInt2();
|
||||
ViewportSize = Size.FromInt2((1f / zoom * new float2(Game.Renderer.NativeResolution)).ToInt2());
|
||||
cellsDirty = true;
|
||||
allCellsDirty = true;
|
||||
}
|
||||
@@ -86,6 +90,13 @@ namespace OpenRA.Graphics
|
||||
public float MinZoom { get; private set; } = 1f;
|
||||
public float MaxZoom { get; private set; } = 2f;
|
||||
|
||||
public void OverrideDefaultHeight(float height)
|
||||
{
|
||||
defaultScale = viewportSizes.DefaultScale * Game.Renderer.NativeResolution.Height / height;
|
||||
overrideUserScale = true;
|
||||
UpdateViewportZooms(false);
|
||||
}
|
||||
|
||||
public void AdjustZoom(float dz)
|
||||
{
|
||||
// Exponential ensures that equal positive and negative steps have the same effect
|
||||
@@ -97,7 +108,9 @@ namespace OpenRA.Graphics
|
||||
var oldCenter = worldRenderer.Viewport.ViewToWorldPx(center);
|
||||
AdjustZoom(dz);
|
||||
var newCenter = worldRenderer.Viewport.ViewToWorldPx(center);
|
||||
CenterLocation += oldCenter - newCenter;
|
||||
|
||||
var candidateCenterLocation = CenterLocation + oldCenter - newCenter;
|
||||
CenterLocation = candidateCenterLocation.Clamp(mapBounds);
|
||||
}
|
||||
|
||||
public void ToggleZoom()
|
||||
@@ -137,16 +150,17 @@ namespace OpenRA.Graphics
|
||||
public Viewport(WorldRenderer wr, Map map)
|
||||
{
|
||||
worldRenderer = wr;
|
||||
var grid = Game.ModData.Manifest.Get<MapGrid>();
|
||||
viewportSizes = Game.ModData.Manifest.Get<WorldViewportSizes>();
|
||||
tileSize = map.Rules.TerrainInfo.TileSize;
|
||||
viewportSizes = Game.ModData.GetOrCreate<WorldViewportSizes>();
|
||||
graphicSettings = Game.Settings.Graphics;
|
||||
defaultScale = viewportSizes.DefaultScale;
|
||||
|
||||
// Calculate map bounds in world-px
|
||||
if (wr.World.Type == WorldType.Editor)
|
||||
{
|
||||
// The full map is visible in the editor
|
||||
var width = map.MapSize.X * grid.TileSize.Width;
|
||||
var height = map.MapSize.Y * grid.TileSize.Height;
|
||||
var width = map.MapSize.Width * tileSize.Width;
|
||||
var height = map.MapSize.Height * tileSize.Height;
|
||||
if (wr.World.Map.Grid.Type == MapGridType.RectangularIsometric)
|
||||
height /= 2;
|
||||
|
||||
@@ -161,8 +175,6 @@ namespace OpenRA.Graphics
|
||||
CenterLocation = (tl + br) / 2;
|
||||
}
|
||||
|
||||
tileSize = grid.TileSize;
|
||||
|
||||
UpdateViewportZooms();
|
||||
}
|
||||
|
||||
@@ -170,6 +182,9 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
if (lastViewportDistance != graphicSettings.ViewportDistance)
|
||||
UpdateViewportZooms();
|
||||
|
||||
if (ViewportCenterProvider != null)
|
||||
Center(ViewportCenterProvider());
|
||||
}
|
||||
|
||||
static float CalculateMinimumZoom(float minHeight, float maxHeight)
|
||||
@@ -207,15 +222,17 @@ namespace OpenRA.Graphics
|
||||
lastViewportDistance = graphicSettings.ViewportDistance;
|
||||
|
||||
var vd = graphicSettings.ViewportDistance;
|
||||
if (viewportSizes.AllowNativeZoom && vd == WorldViewport.Native)
|
||||
MinZoom = viewportSizes.DefaultScale;
|
||||
if (overrideUserScale || (viewportSizes.AllowNativeZoom && vd == WorldViewport.Native))
|
||||
MinZoom = defaultScale;
|
||||
else
|
||||
{
|
||||
var range = viewportSizes.GetSizeRange(vd);
|
||||
MinZoom = CalculateMinimumZoom(range.X, range.Y) * viewportSizes.DefaultScale;
|
||||
MinZoom = CalculateMinimumZoom(range.X, range.Y) * defaultScale;
|
||||
}
|
||||
|
||||
MaxZoom = Math.Min(MinZoom * viewportSizes.MaxZoomScale, Game.Renderer.NativeResolution.Height * viewportSizes.DefaultScale / viewportSizes.MaxZoomWindowHeight);
|
||||
MaxZoom = Math.Min(
|
||||
MinZoom * viewportSizes.MaxZoomScale,
|
||||
Game.Renderer.NativeResolution.Height * defaultScale / viewportSizes.MaxZoomWindowHeight);
|
||||
|
||||
if (unlockMinZoom)
|
||||
{
|
||||
@@ -231,16 +248,17 @@ namespace OpenRA.Graphics
|
||||
else
|
||||
Zoom = Zoom.Clamp(MinZoom, MaxZoom);
|
||||
|
||||
var maxSize = 1f / (unlockMinZoom ? unlockedMinZoom : MinZoom) * new float2(Game.Renderer.NativeResolution);
|
||||
var minZoom = unlockMinZoom ? unlockedMinZoom : MinZoom;
|
||||
var maxSize = 1f / minZoom * new float2(Game.Renderer.NativeResolution);
|
||||
Game.Renderer.SetMaximumViewportSize(new Size((int)maxSize.X, (int)maxSize.Y));
|
||||
|
||||
foreach (var t in worldRenderer.World.WorldActor.TraitsImplementing<INotifyViewportZoomExtentsChanged>())
|
||||
t.ViewportZoomExtentsChanged(MinZoom, MaxZoom);
|
||||
t.ViewportZoomExtentsChanged(minZoom, MaxZoom);
|
||||
}
|
||||
|
||||
public CPos ViewToWorld(int2 view)
|
||||
{
|
||||
var world = worldRenderer.Viewport.ViewToWorldPx(view);
|
||||
var world = ViewToWorldPx(view);
|
||||
var map = worldRenderer.World.Map;
|
||||
var candidates = CandidateMouseoverCells(world).ToList();
|
||||
|
||||
@@ -253,7 +271,7 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
var ramp = map.Grid.Ramps[map.Ramp.Contains(uv) ? map.Ramp[uv] : 0];
|
||||
var pos = map.CenterOfCell(uv.ToCPos(map)) - new WVec(0, 0, ramp.CenterHeightOffset);
|
||||
var screen = ramp.Corners.Select(c => worldRenderer.ScreenPxPosition(pos + c)).ToArray();
|
||||
var screen = ramp.Corners.Select(c => worldRenderer.ScreenPxPosition(pos + c)).ToImmutableArray();
|
||||
if (screen.PolygonContains(world))
|
||||
return uv.ToCPos(map);
|
||||
}
|
||||
@@ -282,27 +300,42 @@ namespace OpenRA.Graphics
|
||||
IEnumerable<MPos> CandidateMouseoverCells(int2 world)
|
||||
{
|
||||
var map = worldRenderer.World.Map;
|
||||
var tileScale = map.Grid.TileScale / 2;
|
||||
var minPos = worldRenderer.ProjectedPosition(world);
|
||||
|
||||
// Find all the cells that could potentially have been clicked
|
||||
var a = map.CellContaining(minPos - new WVec(1024, 0, 0)).ToMPos(map.Grid.Type);
|
||||
var b = map.CellContaining(minPos + new WVec(512, 512 * map.Grid.MaximumTerrainHeight, 0)).ToMPos(map.Grid.Type);
|
||||
// Find all the cells that could potentially have been clicked.
|
||||
MPos a;
|
||||
MPos b;
|
||||
if (map.Grid.Type == MapGridType.RectangularIsometric)
|
||||
{
|
||||
// TODO: this generates too many cells.
|
||||
a = map.CellContaining(minPos - new WVec(tileScale, 0, 0)).ToMPos(map.Grid.Type);
|
||||
b = map.CellContaining(minPos + new WVec(tileScale, tileScale * map.Grid.MaximumTerrainHeight, 0)).ToMPos(map.Grid.Type);
|
||||
}
|
||||
else
|
||||
{
|
||||
a = map.CellContaining(minPos).ToMPos(map.Grid.Type);
|
||||
b = map.CellContaining(minPos + new WVec(0, tileScale * map.Grid.MaximumTerrainHeight, 0)).ToMPos(map.Grid.Type);
|
||||
}
|
||||
|
||||
for (var v = b.V; v >= a.V; v--)
|
||||
for (var u = b.U; u >= a.U; u--)
|
||||
yield return new MPos(u, v);
|
||||
}
|
||||
|
||||
public int2 ViewToWorldPx(int2 view) { return (graphicSettings.UIScale / Zoom * view.ToFloat2()).ToInt2() + TopLeft; }
|
||||
public int2 WorldToViewPx(int2 world) { return (Zoom / graphicSettings.UIScale * (world - TopLeft).ToFloat2()).ToInt2(); }
|
||||
public int2 WorldToViewPx(in float3 world) { return (Zoom / graphicSettings.UIScale * (world - TopLeft).XY).ToInt2(); }
|
||||
public int2 ViewToWorldPx(int2 view) => (graphicSettings.UIScale / Zoom * view.ToFloat2() + CenterLocation - ViewportSize.ToInt2() / 2).ToInt2();
|
||||
public int2 WorldToViewPx(int2 world) => (Zoom / graphicSettings.UIScale * (world - CenterLocation + ViewportSize.ToInt2() / 2)).ToInt2();
|
||||
public int2 WorldToViewPx(in float3 world) => (Zoom / graphicSettings.UIScale * (world - CenterLocation + ViewportSize.ToInt2() / 2).XY).ToInt2();
|
||||
|
||||
public void Center(IEnumerable<Actor> actors)
|
||||
{
|
||||
if (!actors.Any())
|
||||
var actorsCollection = actors as IReadOnlyCollection<Actor>;
|
||||
actorsCollection ??= actors.ToList();
|
||||
|
||||
if (actorsCollection.Count == 0)
|
||||
return;
|
||||
|
||||
Center(actors.Select(a => a.CenterPosition).Average());
|
||||
Center(actorsCollection.Select(a => a.CenterPosition).Average());
|
||||
}
|
||||
|
||||
public void Center(WPos pos)
|
||||
@@ -312,10 +345,17 @@ namespace OpenRA.Graphics
|
||||
allCellsDirty = true;
|
||||
}
|
||||
|
||||
public void Center(float2 pos)
|
||||
{
|
||||
CenterLocation = worldRenderer.ScreenPosition(pos).Clamp(mapBounds);
|
||||
cellsDirty = true;
|
||||
allCellsDirty = true;
|
||||
}
|
||||
|
||||
public void Scroll(float2 delta, bool ignoreBorders)
|
||||
{
|
||||
// Convert scroll delta from world-px to viewport-px
|
||||
CenterLocation += (1f / Zoom * delta).ToInt2();
|
||||
CenterLocation += 1f / Zoom * delta;
|
||||
cellsDirty = true;
|
||||
allCellsDirty = true;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using OpenRA.Effects;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
@@ -31,44 +32,61 @@ namespace OpenRA.Graphics
|
||||
|
||||
public event Action PaletteInvalidated = null;
|
||||
|
||||
readonly HashSet<Actor> onScreenActors = new();
|
||||
readonly HashSet<Actor> onScreenActors = [];
|
||||
readonly HardwarePalette palette = new();
|
||||
readonly Dictionary<string, PaletteReference> palettes = new();
|
||||
readonly Dictionary<string, PaletteReference> palettes = [];
|
||||
readonly IRenderTerrain terrainRenderer;
|
||||
readonly Lazy<DebugVisualizations> debugVis;
|
||||
readonly Func<string, PaletteReference> createPaletteReference;
|
||||
readonly bool enableDepthBuffer;
|
||||
|
||||
readonly List<IFinalizedRenderable> preparedRenderables = new();
|
||||
readonly List<IFinalizedRenderable> preparedOverlayRenderables = new();
|
||||
readonly List<IFinalizedRenderable> preparedAnnotationRenderables = new();
|
||||
readonly List<IFinalizedRenderable> preparedRenderables = [];
|
||||
readonly List<IFinalizedRenderable> preparedOverlayRenderables = [];
|
||||
readonly List<IFinalizedRenderable> preparedAnnotationRenderables = [];
|
||||
|
||||
readonly List<IRenderable> renderablesBuffer = new();
|
||||
readonly List<IRenderable> renderablesBuffer = [];
|
||||
readonly IRenderer[] renderers;
|
||||
readonly IRenderPostProcessPass[] postProcessPasses;
|
||||
long[] renderablesKeysBuffer = [];
|
||||
|
||||
internal WorldRenderer(ModData modData, World world)
|
||||
{
|
||||
World = world;
|
||||
TileSize = World.Map.Grid.TileSize;
|
||||
TileSize = World.Map.Rules.TerrainInfo.TileSize;
|
||||
TileScale = World.Map.Grid.TileScale;
|
||||
Viewport = new Viewport(this, world.Map);
|
||||
|
||||
createPaletteReference = CreatePaletteReference;
|
||||
|
||||
var mapGrid = modData.Manifest.Get<MapGrid>();
|
||||
var mapGrid = modData.GetOrCreate<MapGrid>();
|
||||
enableDepthBuffer = mapGrid.EnableDepthBuffer;
|
||||
|
||||
foreach (var pal in world.TraitDict.ActorsWithTrait<ILoadsPalettes>())
|
||||
pal.Trait.LoadPalettes(this);
|
||||
|
||||
foreach (var p in world.Players)
|
||||
UpdatePalettesForPlayer(p.InternalName, p.Color, false);
|
||||
Player.SetupRelationshipColors(world.Players, world.LocalPlayer, this, true);
|
||||
|
||||
palette.Initialize();
|
||||
|
||||
TerrainLighting = world.WorldActor.TraitOrDefault<ITerrainLighting>();
|
||||
renderers = world.WorldActor.TraitsImplementing<IRenderer>().ToArray();
|
||||
terrainRenderer = world.WorldActor.TraitOrDefault<IRenderTerrain>();
|
||||
|
||||
debugVis = Exts.Lazy(() => world.WorldActor.TraitOrDefault<DebugVisualizations>());
|
||||
debugVis = Exts.Lazy(world.WorldActor.TraitOrDefault<DebugVisualizations>);
|
||||
|
||||
postProcessPasses = world.WorldActor.TraitsImplementing<IRenderPostProcessPass>().ToArray();
|
||||
}
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
foreach (var r in renderers)
|
||||
r.BeginFrame();
|
||||
}
|
||||
|
||||
public void EndFrame()
|
||||
{
|
||||
foreach (var r in renderers)
|
||||
r.EndFrame();
|
||||
}
|
||||
|
||||
public void UpdatePalettesForPlayer(string internalName, Color color, bool replaceExisting)
|
||||
@@ -141,7 +159,14 @@ namespace OpenRA.Graphics
|
||||
renderablesBuffer.AddRange(e.Render(this));
|
||||
|
||||
// Renderables must be ordered using a stable sorting algorithm to avoid flickering artefacts
|
||||
foreach (var renderable in renderablesBuffer.OrderBy(RenderableZPositionComparisonKey))
|
||||
if (renderablesKeysBuffer.Length < renderablesBuffer.Count)
|
||||
renderablesKeysBuffer = new long[Exts.NextPowerOf2(renderablesBuffer.Count)];
|
||||
for (var i = 0; i < renderablesBuffer.Count; i++)
|
||||
renderablesKeysBuffer[i] = ((long)RenderableZPositionComparisonKey(renderablesBuffer[i]) << 32) + i;
|
||||
var keys = renderablesKeysBuffer.AsSpan(0, renderablesBuffer.Count);
|
||||
keys.Sort(CollectionsMarshal.AsSpan(renderablesBuffer));
|
||||
|
||||
foreach (var renderable in renderablesBuffer)
|
||||
preparedRenderables.Add(renderable.PrepareRender(this));
|
||||
|
||||
// PERF: Reuse collection to avoid allocations.
|
||||
@@ -270,6 +295,8 @@ namespace OpenRA.Graphics
|
||||
if (enableDepthBuffer)
|
||||
Game.Renderer.ClearDepthBuffer();
|
||||
|
||||
ApplyPostProcessing(PostProcessPassType.AfterActors);
|
||||
|
||||
World.ApplyToActorsWithTrait<IRenderAboveWorld>((actor, trait) =>
|
||||
{
|
||||
if (actor.IsInWorld && !actor.Disposed)
|
||||
@@ -279,6 +306,8 @@ namespace OpenRA.Graphics
|
||||
if (enableDepthBuffer)
|
||||
Game.Renderer.ClearDepthBuffer();
|
||||
|
||||
ApplyPostProcessing(PostProcessPassType.AfterWorld);
|
||||
|
||||
World.ApplyToActorsWithTrait<IRenderShroud>((actor, trait) => trait.RenderShroud(this));
|
||||
|
||||
if (enableDepthBuffer)
|
||||
@@ -292,9 +321,23 @@ namespace OpenRA.Graphics
|
||||
foreach (var r in g)
|
||||
r.Render(this);
|
||||
|
||||
ApplyPostProcessing(PostProcessPassType.AfterShroud);
|
||||
|
||||
Game.Renderer.Flush();
|
||||
}
|
||||
|
||||
void ApplyPostProcessing(PostProcessPassType type)
|
||||
{
|
||||
foreach (var pass in postProcessPasses)
|
||||
{
|
||||
if (pass.Type != type || !pass.Enabled)
|
||||
continue;
|
||||
|
||||
Game.Renderer.Flush();
|
||||
pass.Draw(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void DrawAnnotations()
|
||||
{
|
||||
Game.Renderer.EnableAntialiasingFilter();
|
||||
@@ -337,6 +380,8 @@ namespace OpenRA.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
ApplyPostProcessing(PostProcessPassType.AfterAnnotations);
|
||||
|
||||
Game.Renderer.Flush();
|
||||
|
||||
preparedRenderables.Clear();
|
||||
@@ -350,12 +395,22 @@ namespace OpenRA.Graphics
|
||||
Game.Renderer.SetPalette(palette);
|
||||
}
|
||||
|
||||
// Conversion between world and screen coordinates
|
||||
/// <summary>
|
||||
/// Converts a world position to a screen position.
|
||||
/// </summary>
|
||||
public float2 ScreenPosition(WPos pos)
|
||||
{
|
||||
return new float2((float)TileSize.Width * pos.X / TileScale, (float)TileSize.Height * (pos.Y - pos.Z) / TileScale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a world position to a screen position.
|
||||
/// </summary>
|
||||
public float2 ScreenPosition(float2 pos)
|
||||
{
|
||||
return new float2(TileSize.Width * pos.X / TileScale, TileSize.Height * pos.Y / TileScale);
|
||||
}
|
||||
|
||||
public float3 Screen3DPosition(WPos pos)
|
||||
{
|
||||
// The projection from world coordinates to screen coordinates has
|
||||
@@ -395,7 +450,7 @@ namespace OpenRA.Graphics
|
||||
public float[] ScreenVector(in WVec vec)
|
||||
{
|
||||
var xyz = ScreenVectorComponents(vec);
|
||||
return new[] { xyz.X, xyz.Y, xyz.Z, 1f };
|
||||
return [xyz.X, xyz.Y, xyz.Z, 1f];
|
||||
}
|
||||
|
||||
public int2 ScreenPxOffset(in WVec vec)
|
||||
|
||||
@@ -9,18 +9,27 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public sealed class HotkeyDefinition
|
||||
{
|
||||
public const string ContextFluentPrefix = "hotkey-context";
|
||||
|
||||
public readonly string Name;
|
||||
public readonly Hotkey Default = Hotkey.Invalid;
|
||||
|
||||
[FluentReference]
|
||||
public readonly string Description = "";
|
||||
public readonly HashSet<string> Types = new();
|
||||
public readonly HashSet<string> Contexts = new();
|
||||
|
||||
public readonly FrozenSet<string> Types = FrozenSet<string>.Empty;
|
||||
|
||||
[FluentReference]
|
||||
public readonly FrozenSet<string> Contexts = FrozenSet<string>.Empty;
|
||||
|
||||
public readonly bool Readonly = false;
|
||||
public bool HasDuplicates { get; internal set; }
|
||||
|
||||
@@ -31,29 +40,27 @@ namespace OpenRA
|
||||
if (!string.IsNullOrEmpty(node.Value))
|
||||
Default = FieldLoader.GetValue<Hotkey>("value", node.Value);
|
||||
|
||||
var descriptionNode = node.Nodes.FirstOrDefault(n => n.Key == "Description");
|
||||
if (descriptionNode != null)
|
||||
Description = descriptionNode.Value.Value;
|
||||
var nodeDict = node.ToDictionary();
|
||||
|
||||
var typesNode = node.Nodes.FirstOrDefault(n => n.Key == "Types");
|
||||
if (typesNode != null)
|
||||
Types = FieldLoader.GetValue<HashSet<string>>("Types", typesNode.Value.Value);
|
||||
if (nodeDict.TryGetValue("Description", out var descriptionYaml))
|
||||
Description = descriptionYaml.Value;
|
||||
|
||||
var contextsNode = node.Nodes.FirstOrDefault(n => n.Key == "Contexts");
|
||||
if (contextsNode != null)
|
||||
Contexts = FieldLoader.GetValue<HashSet<string>>("Contexts", contextsNode.Value.Value);
|
||||
if (nodeDict.TryGetValue("Types", out var typesYaml))
|
||||
Types = FieldLoader.GetValue<FrozenSet<string>>("Types", typesYaml.Value);
|
||||
|
||||
var platformNode = node.Nodes.FirstOrDefault(n => n.Key == "Platform");
|
||||
if (platformNode != null)
|
||||
if (nodeDict.TryGetValue("Contexts", out var contextYaml))
|
||||
Contexts = FieldLoader.GetValue<ImmutableArray<string>>("Contexts", contextYaml.Value)
|
||||
.Select(c => ContextFluentPrefix + "." + c).ToFrozenSet();
|
||||
|
||||
if (nodeDict.TryGetValue("Platform", out var platformYaml))
|
||||
{
|
||||
var platformOverride = platformNode.Value.Nodes.FirstOrDefault(n => n.Key == Platform.CurrentPlatform.ToString());
|
||||
var platformOverride = platformYaml.NodeWithKeyOrDefault(Platform.CurrentPlatform.ToString());
|
||||
if (platformOverride != null)
|
||||
Default = FieldLoader.GetValue<Hotkey>("value", platformOverride.Value.Value);
|
||||
}
|
||||
|
||||
var readonlyNode = node.Nodes.FirstOrDefault(n => n.Key == "Readonly");
|
||||
if (readonlyNode != null)
|
||||
Readonly = FieldLoader.GetValue<bool>("Readonly", readonlyNode.Value.Value);
|
||||
if (nodeDict.TryGetValue("Readonly", out var readonlyYaml))
|
||||
Readonly = FieldLoader.GetValue<bool>("Readonly", readonlyYaml.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,16 @@ namespace OpenRA
|
||||
{
|
||||
public sealed class HotkeyManager
|
||||
{
|
||||
readonly Dictionary<string, Hotkey> settings;
|
||||
readonly Dictionary<string, HotkeyDefinition> definitions = new();
|
||||
readonly Dictionary<string, Hotkey> keys = new();
|
||||
[YamlNode("Keys", shared: true)]
|
||||
sealed class HotkeySettings : SettingsModule { }
|
||||
|
||||
public HotkeyManager(IReadOnlyFileSystem fileSystem, Dictionary<string, Hotkey> settings, Manifest manifest)
|
||||
readonly Dictionary<string, HotkeyDefinition> definitions = [];
|
||||
readonly Dictionary<string, Hotkey> keys = [];
|
||||
readonly HotkeySettings hotkeySettings;
|
||||
|
||||
public HotkeyManager(IReadOnlyFileSystem fileSystem, ObjectCreator objectCreator, Manifest manifest)
|
||||
{
|
||||
this.settings = settings;
|
||||
hotkeySettings = Game.Settings.GetOrCreate<HotkeySettings>(objectCreator, manifest.Id);
|
||||
|
||||
var keyDefinitions = MiniYaml.Load(fileSystem, manifest.Hotkeys, null);
|
||||
foreach (var kd in keyDefinitions)
|
||||
@@ -33,11 +36,9 @@ namespace OpenRA
|
||||
keys[kd.Key] = definition.Default;
|
||||
}
|
||||
|
||||
foreach (var kv in settings)
|
||||
{
|
||||
if (definitions.TryGetValue(kv.Key, out var definition) && !definition.Readonly)
|
||||
keys[kv.Key] = kv.Value;
|
||||
}
|
||||
foreach (var node in hotkeySettings.Yaml.Nodes)
|
||||
if (definitions.TryGetValue(node.Key, out var definition) && !definition.Readonly)
|
||||
keys[node.Key] = FieldLoader.GetValue<Hotkey>(node.Key, node.Value.Value);
|
||||
|
||||
foreach (var hd in definitions)
|
||||
hd.Value.HasDuplicates = GetFirstDuplicate(hd.Value, this[hd.Value.Name].GetValue()) != null;
|
||||
@@ -68,10 +69,9 @@ namespace OpenRA
|
||||
return;
|
||||
|
||||
keys[name] = value;
|
||||
hotkeySettings.Yaml.Nodes.RemoveAll(n => n.Key == name);
|
||||
if (value != definition.Default)
|
||||
settings[name] = value;
|
||||
else
|
||||
settings.Remove(name);
|
||||
hotkeySettings.Yaml.Nodes.Add(new MiniYamlNodeBuilder(name, FieldSaver.FormatValue(value)));
|
||||
|
||||
var hadDuplicates = definition.HasDuplicates;
|
||||
definition.HasDuplicates = GetFirstDuplicate(definition, this[definition.Name].GetValue()) != null;
|
||||
@@ -108,5 +108,10 @@ namespace OpenRA
|
||||
public HotkeyReference this[string name] => new(GetHotkeyReference(name));
|
||||
|
||||
public IEnumerable<HotkeyDefinition> Definitions => definitions.Values;
|
||||
|
||||
public void Save()
|
||||
{
|
||||
hotkeySettings.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,7 @@ namespace OpenRA
|
||||
var total = response.Content.Headers.ContentLength ?? -1;
|
||||
var canReportProgress = total > 0;
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
using (var contentStream = await response.Content.ReadAsStreamAsync(token))
|
||||
#else
|
||||
using (var contentStream = await response.Content.ReadAsStreamAsync())
|
||||
#endif
|
||||
await using (var contentStream = await response.Content.ReadAsStreamAsync(token))
|
||||
{
|
||||
var totalRead = 0L;
|
||||
var buffer = new byte[8192];
|
||||
|
||||
@@ -27,25 +27,21 @@ namespace OpenRA
|
||||
public static bool TryParse(string s, out Hotkey result)
|
||||
{
|
||||
result = Invalid;
|
||||
if (string.IsNullOrWhiteSpace(s))
|
||||
if (s == null)
|
||||
return false;
|
||||
|
||||
var parts = s.Split(' ');
|
||||
Span<Range> ranges = stackalloc Range[2];
|
||||
var span = s.AsSpan();
|
||||
var count = span.Split(ranges, ' ');
|
||||
if (count == 0)
|
||||
return false;
|
||||
|
||||
if (!Enum<Keycode>.TryParse(parts[0], true, out var key))
|
||||
{
|
||||
if (!int.TryParse(parts[0], out var c))
|
||||
return false;
|
||||
key = (Keycode)c;
|
||||
}
|
||||
if (!Enum.TryParse(span[ranges[0]], true, out Keycode key))
|
||||
return false;
|
||||
|
||||
var mods = Modifiers.None;
|
||||
if (parts.Length >= 2)
|
||||
{
|
||||
var modString = s[s.IndexOf(' ')..];
|
||||
if (!Enum<Modifiers>.TryParse(modString, true, out mods))
|
||||
return false;
|
||||
}
|
||||
if (count == 2 && !Enum.TryParse(span[ranges[1]], true, out mods))
|
||||
return false;
|
||||
|
||||
result = new Hotkey(key, mods);
|
||||
return true;
|
||||
@@ -91,16 +87,16 @@ namespace OpenRA
|
||||
var ret = KeycodeExts.DisplayString(Key);
|
||||
|
||||
if (Modifiers.HasModifier(Modifiers.Shift))
|
||||
ret = "Shift + " + ret;
|
||||
ret = $"{ModifiersExts.DisplayString(Modifiers.Shift)} + {ret}";
|
||||
|
||||
if (Modifiers.HasModifier(Modifiers.Alt))
|
||||
ret = "Alt + " + ret;
|
||||
ret = $"{ModifiersExts.DisplayString(Modifiers.Alt)} + {ret}";
|
||||
|
||||
if (Modifiers.HasModifier(Modifiers.Ctrl))
|
||||
ret = "Ctrl + " + ret;
|
||||
ret = $"{ModifiersExts.DisplayString(Modifiers.Ctrl)} + {ret}";
|
||||
|
||||
if (Modifiers.HasModifier(Modifiers.Meta))
|
||||
ret = (Platform.CurrentPlatform == PlatformType.OSX ? "Cmd + " : "Meta + ") + ret;
|
||||
ret = $"{ModifiersExts.DisplayString(Modifiers.Meta)} + {ret}";
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
@@ -22,25 +23,7 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public enum MouseInputEvent { Down, Move, Up, Scroll }
|
||||
public struct MouseInput
|
||||
{
|
||||
public MouseInputEvent Event;
|
||||
public MouseButton Button;
|
||||
public int2 Location;
|
||||
public int2 Delta;
|
||||
public Modifiers Modifiers;
|
||||
public int MultiTapCount;
|
||||
|
||||
public MouseInput(MouseInputEvent ev, MouseButton button, int2 location, int2 delta, Modifiers mods, int multiTapCount)
|
||||
{
|
||||
Event = ev;
|
||||
Button = button;
|
||||
Location = location;
|
||||
Delta = delta;
|
||||
Modifiers = mods;
|
||||
MultiTapCount = multiTapCount;
|
||||
}
|
||||
}
|
||||
public record struct MouseInput(MouseInputEvent Event, MouseButton Button, int2 Location, int2 Delta, Modifiers Modifiers, int MultiTapCount);
|
||||
|
||||
[Flags]
|
||||
public enum MouseButton
|
||||
@@ -61,6 +44,33 @@ namespace OpenRA
|
||||
Meta = 8,
|
||||
}
|
||||
|
||||
public static class ModifiersExts
|
||||
{
|
||||
[FluentReference]
|
||||
const string Cmd = "keycode-modifier.cmd";
|
||||
|
||||
[FluentReference(Traits.LintDictionaryReference.Values)]
|
||||
public static readonly IReadOnlyDictionary<Modifiers, string> ModifierFluentKeys = new Dictionary<Modifiers, string>()
|
||||
{
|
||||
{ Modifiers.None, "keycode-modifier.none" },
|
||||
{ Modifiers.Shift, "keycode-modifier.shift" },
|
||||
{ Modifiers.Alt, "keycode-modifier.alt" },
|
||||
{ Modifiers.Ctrl, "keycode-modifier.ctrl" },
|
||||
{ Modifiers.Meta, "keycode-modifier.meta" },
|
||||
};
|
||||
|
||||
public static string DisplayString(Modifiers m)
|
||||
{
|
||||
if (m == Modifiers.Meta && Platform.CurrentPlatform == PlatformType.OSX)
|
||||
return FluentProvider.GetMessage(Cmd);
|
||||
|
||||
if (!ModifierFluentKeys.TryGetValue(m, out var fluentKey))
|
||||
return m.ToString();
|
||||
|
||||
return FluentProvider.GetMessage(fluentKey);
|
||||
}
|
||||
}
|
||||
|
||||
public enum KeyInputEvent { Down, Up }
|
||||
public struct KeyInput
|
||||
{
|
||||
|
||||
@@ -50,11 +50,4 @@ namespace OpenRA
|
||||
Sync.RunUnsynced(world, () => Ui.HandleInput(input));
|
||||
}
|
||||
}
|
||||
|
||||
public class MouseButtonPreference
|
||||
{
|
||||
public MouseButton Action => Game.Settings.Game.UseClassicMouseStyle ? MouseButton.Left : MouseButton.Right;
|
||||
|
||||
public MouseButton Cancel => Game.Settings.Game.UseClassicMouseStyle ? MouseButton.Right : MouseButton.Left;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,254 +259,255 @@ namespace OpenRA
|
||||
|
||||
public static class KeycodeExts
|
||||
{
|
||||
static readonly Dictionary<Keycode, string> KeyNames = new()
|
||||
[FluentReference(Traits.LintDictionaryReference.Values)]
|
||||
public static readonly IReadOnlyDictionary<Keycode, string> KeycodeFluentKeys = new Dictionary<Keycode, string>()
|
||||
{
|
||||
{ Keycode.UNKNOWN, "Undefined" },
|
||||
{ Keycode.RETURN, "Return" },
|
||||
{ Keycode.ESCAPE, "Escape" },
|
||||
{ Keycode.BACKSPACE, "Backspace" },
|
||||
{ Keycode.TAB, "Tab" },
|
||||
{ Keycode.SPACE, "Space" },
|
||||
{ Keycode.EXCLAIM, "!" },
|
||||
{ Keycode.QUOTEDBL, "\"" },
|
||||
{ Keycode.HASH, "#" },
|
||||
{ Keycode.PERCENT, "%" },
|
||||
{ Keycode.DOLLAR, "$" },
|
||||
{ Keycode.AMPERSAND, "&" },
|
||||
{ Keycode.QUOTE, "'" },
|
||||
{ Keycode.LEFTPAREN, "(" },
|
||||
{ Keycode.RIGHTPAREN, ")" },
|
||||
{ Keycode.ASTERISK, "*" },
|
||||
{ Keycode.PLUS, "+" },
|
||||
{ Keycode.COMMA, "," },
|
||||
{ Keycode.MINUS, "-" },
|
||||
{ Keycode.PERIOD, "." },
|
||||
{ Keycode.SLASH, "/" },
|
||||
{ Keycode.NUMBER_0, "0" },
|
||||
{ Keycode.NUMBER_1, "1" },
|
||||
{ Keycode.NUMBER_2, "2" },
|
||||
{ Keycode.NUMBER_3, "3" },
|
||||
{ Keycode.NUMBER_4, "4" },
|
||||
{ Keycode.NUMBER_5, "5" },
|
||||
{ Keycode.NUMBER_6, "6" },
|
||||
{ Keycode.NUMBER_7, "7" },
|
||||
{ Keycode.NUMBER_8, "8" },
|
||||
{ Keycode.NUMBER_9, "9" },
|
||||
{ Keycode.COLON, ":" },
|
||||
{ Keycode.SEMICOLON, ";" },
|
||||
{ Keycode.LESS, "<" },
|
||||
{ Keycode.EQUALS, "=" },
|
||||
{ Keycode.GREATER, ">" },
|
||||
{ Keycode.QUESTION, "?" },
|
||||
{ Keycode.AT, "@" },
|
||||
{ Keycode.LEFTBRACKET, "[" },
|
||||
{ Keycode.BACKSLASH, "\\" },
|
||||
{ Keycode.RIGHTBRACKET, "]" },
|
||||
{ Keycode.CARET, "^" },
|
||||
{ Keycode.UNDERSCORE, "_" },
|
||||
{ Keycode.BACKQUOTE, "`" },
|
||||
{ Keycode.A, "A" },
|
||||
{ Keycode.B, "B" },
|
||||
{ Keycode.C, "C" },
|
||||
{ Keycode.D, "D" },
|
||||
{ Keycode.E, "E" },
|
||||
{ Keycode.F, "F" },
|
||||
{ Keycode.G, "G" },
|
||||
{ Keycode.H, "H" },
|
||||
{ Keycode.I, "I" },
|
||||
{ Keycode.J, "J" },
|
||||
{ Keycode.K, "K" },
|
||||
{ Keycode.L, "L" },
|
||||
{ Keycode.M, "M" },
|
||||
{ Keycode.N, "N" },
|
||||
{ Keycode.O, "O" },
|
||||
{ Keycode.P, "P" },
|
||||
{ Keycode.Q, "Q" },
|
||||
{ Keycode.R, "R" },
|
||||
{ Keycode.S, "S" },
|
||||
{ Keycode.T, "T" },
|
||||
{ Keycode.U, "U" },
|
||||
{ Keycode.V, "V" },
|
||||
{ Keycode.W, "W" },
|
||||
{ Keycode.X, "X" },
|
||||
{ Keycode.Y, "Y" },
|
||||
{ Keycode.Z, "Z" },
|
||||
{ Keycode.CAPSLOCK, "CapsLock" },
|
||||
{ Keycode.F1, "F1" },
|
||||
{ Keycode.F2, "F2" },
|
||||
{ Keycode.F3, "F3" },
|
||||
{ Keycode.F4, "F4" },
|
||||
{ Keycode.F5, "F5" },
|
||||
{ Keycode.F6, "F6" },
|
||||
{ Keycode.F7, "F7" },
|
||||
{ Keycode.F8, "F8" },
|
||||
{ Keycode.F9, "F9" },
|
||||
{ Keycode.F10, "F10" },
|
||||
{ Keycode.F11, "F11" },
|
||||
{ Keycode.F12, "F12" },
|
||||
{ Keycode.PRINTSCREEN, "PrintScreen" },
|
||||
{ Keycode.SCROLLLOCK, "ScrollLock" },
|
||||
{ Keycode.PAUSE, "Pause" },
|
||||
{ Keycode.INSERT, "Insert" },
|
||||
{ Keycode.HOME, "Home" },
|
||||
{ Keycode.PAGEUP, "PageUp" },
|
||||
{ Keycode.DELETE, "Delete" },
|
||||
{ Keycode.END, "End" },
|
||||
{ Keycode.PAGEDOWN, "PageDown" },
|
||||
{ Keycode.RIGHT, "Right" },
|
||||
{ Keycode.LEFT, "Left" },
|
||||
{ Keycode.DOWN, "Down" },
|
||||
{ Keycode.UP, "Up" },
|
||||
{ Keycode.NUMLOCKCLEAR, "Numlock" },
|
||||
{ Keycode.KP_DIVIDE, "Keypad /" },
|
||||
{ Keycode.KP_MULTIPLY, "Keypad *" },
|
||||
{ Keycode.KP_MINUS, "Keypad -" },
|
||||
{ Keycode.KP_PLUS, "Keypad +" },
|
||||
{ Keycode.KP_ENTER, "Keypad Enter" },
|
||||
{ Keycode.KP_1, "Keypad 1" },
|
||||
{ Keycode.KP_2, "Keypad 2" },
|
||||
{ Keycode.KP_3, "Keypad 3" },
|
||||
{ Keycode.KP_4, "Keypad 4" },
|
||||
{ Keycode.KP_5, "Keypad 5" },
|
||||
{ Keycode.KP_6, "Keypad 6" },
|
||||
{ Keycode.KP_7, "Keypad 7" },
|
||||
{ Keycode.KP_8, "Keypad 8" },
|
||||
{ Keycode.KP_9, "Keypad 9" },
|
||||
{ Keycode.KP_0, "Keypad 0" },
|
||||
{ Keycode.KP_PERIOD, "Keypad ." },
|
||||
{ Keycode.APPLICATION, "Application" },
|
||||
{ Keycode.POWER, "Power" },
|
||||
{ Keycode.KP_EQUALS, "Keypad =" },
|
||||
{ Keycode.F13, "F13" },
|
||||
{ Keycode.F14, "F14" },
|
||||
{ Keycode.F15, "F15" },
|
||||
{ Keycode.F16, "F16" },
|
||||
{ Keycode.F17, "F17" },
|
||||
{ Keycode.F18, "F18" },
|
||||
{ Keycode.F19, "F19" },
|
||||
{ Keycode.F20, "F20" },
|
||||
{ Keycode.F21, "F21" },
|
||||
{ Keycode.F22, "F22" },
|
||||
{ Keycode.F23, "F23" },
|
||||
{ Keycode.F24, "F24" },
|
||||
{ Keycode.EXECUTE, "Execute" },
|
||||
{ Keycode.HELP, "Help" },
|
||||
{ Keycode.MENU, "Menu" },
|
||||
{ Keycode.SELECT, "Select" },
|
||||
{ Keycode.STOP, "Stop" },
|
||||
{ Keycode.AGAIN, "Again" },
|
||||
{ Keycode.UNDO, "Undo" },
|
||||
{ Keycode.CUT, "Cut" },
|
||||
{ Keycode.COPY, "Copy" },
|
||||
{ Keycode.PASTE, "Paste" },
|
||||
{ Keycode.FIND, "Find" },
|
||||
{ Keycode.MUTE, "Mute" },
|
||||
{ Keycode.VOLUMEUP, "VolumeUp" },
|
||||
{ Keycode.VOLUMEDOWN, "VolumeDown" },
|
||||
{ Keycode.KP_COMMA, "Keypad }," },
|
||||
{ Keycode.KP_EQUALSAS400, "Keypad, (AS400)" },
|
||||
{ Keycode.ALTERASE, "AltErase" },
|
||||
{ Keycode.SYSREQ, "SysReq" },
|
||||
{ Keycode.CANCEL, "Cancel" },
|
||||
{ Keycode.CLEAR, "Clear" },
|
||||
{ Keycode.PRIOR, "Prior" },
|
||||
{ Keycode.RETURN2, "Return" },
|
||||
{ Keycode.SEPARATOR, "Separator" },
|
||||
{ Keycode.OUT, "Out" },
|
||||
{ Keycode.OPER, "Oper" },
|
||||
{ Keycode.CLEARAGAIN, "Clear / Again" },
|
||||
{ Keycode.CRSEL, "CrSel" },
|
||||
{ Keycode.EXSEL, "ExSel" },
|
||||
{ Keycode.KP_00, "Keypad 00" },
|
||||
{ Keycode.KP_000, "Keypad 000" },
|
||||
{ Keycode.THOUSANDSSEPARATOR, "ThousandsSeparator" },
|
||||
{ Keycode.DECIMALSEPARATOR, "DecimalSeparator" },
|
||||
{ Keycode.CURRENCYUNIT, "CurrencyUnit" },
|
||||
{ Keycode.CURRENCYSUBUNIT, "CurrencySubUnit" },
|
||||
{ Keycode.KP_LEFTPAREN, "Keypad (" },
|
||||
{ Keycode.KP_RIGHTPAREN, "Keypad )" },
|
||||
{ Keycode.KP_LEFTBRACE, "Keypad {" },
|
||||
{ Keycode.KP_RIGHTBRACE, "Keypad }" },
|
||||
{ Keycode.KP_TAB, "Keypad Tab" },
|
||||
{ Keycode.KP_BACKSPACE, "Keypad Backspace" },
|
||||
{ Keycode.KP_A, "Keypad A" },
|
||||
{ Keycode.KP_B, "Keypad B" },
|
||||
{ Keycode.KP_C, "Keypad C" },
|
||||
{ Keycode.KP_D, "Keypad D" },
|
||||
{ Keycode.KP_E, "Keypad E" },
|
||||
{ Keycode.KP_F, "Keypad F" },
|
||||
{ Keycode.KP_XOR, "Keypad XOR" },
|
||||
{ Keycode.KP_POWER, "Keypad ^" },
|
||||
{ Keycode.KP_PERCENT, "Keypad %" },
|
||||
{ Keycode.KP_LESS, "Keypad <" },
|
||||
{ Keycode.KP_GREATER, "Keypad >" },
|
||||
{ Keycode.KP_AMPERSAND, "Keypad &" },
|
||||
{ Keycode.KP_DBLAMPERSAND, "Keypad &&" },
|
||||
{ Keycode.KP_VERTICALBAR, "Keypad |" },
|
||||
{ Keycode.KP_DBLVERTICALBAR, "Keypad ||" },
|
||||
{ Keycode.KP_COLON, "Keypad :" },
|
||||
{ Keycode.KP_HASH, "Keypad #" },
|
||||
{ Keycode.KP_SPACE, "Keypad Space" },
|
||||
{ Keycode.KP_AT, "Keypad @" },
|
||||
{ Keycode.KP_EXCLAM, "Keypad !" },
|
||||
{ Keycode.KP_MEMSTORE, "Keypad MemStore" },
|
||||
{ Keycode.KP_MEMRECALL, "Keypad MemRecall" },
|
||||
{ Keycode.KP_MEMCLEAR, "Keypad MemClear" },
|
||||
{ Keycode.KP_MEMADD, "Keypad MemAdd" },
|
||||
{ Keycode.KP_MEMSUBTRACT, "Keypad MemSubtract" },
|
||||
{ Keycode.KP_MEMMULTIPLY, "Keypad MemMultiply" },
|
||||
{ Keycode.KP_MEMDIVIDE, "Keypad MemDivide" },
|
||||
{ Keycode.KP_PLUSMINUS, "Keypad +/-" },
|
||||
{ Keycode.KP_CLEAR, "Keypad Clear" },
|
||||
{ Keycode.KP_CLEARENTRY, "Keypad ClearEntry" },
|
||||
{ Keycode.KP_BINARY, "Keypad Binary" },
|
||||
{ Keycode.KP_OCTAL, "Keypad Octal" },
|
||||
{ Keycode.KP_DECIMAL, "Keypad Decimal" },
|
||||
{ Keycode.KP_HEXADECIMAL, "Keypad Hexadecimal" },
|
||||
{ Keycode.LCTRL, "Left Ctrl" },
|
||||
{ Keycode.LSHIFT, "Left Shift" },
|
||||
{ Keycode.LALT, "Left Alt" },
|
||||
{ Keycode.LGUI, "Left GUI" },
|
||||
{ Keycode.RCTRL, "Right Ctrl" },
|
||||
{ Keycode.RSHIFT, "Right Shift" },
|
||||
{ Keycode.RALT, "Right Alt" },
|
||||
{ Keycode.RGUI, "Right GUI" },
|
||||
{ Keycode.MODE, "ModeSwitch" },
|
||||
{ Keycode.AUDIONEXT, "AudioNext" },
|
||||
{ Keycode.AUDIOPREV, "AudioPrev" },
|
||||
{ Keycode.AUDIOSTOP, "AudioStop" },
|
||||
{ Keycode.AUDIOPLAY, "AudioPlay" },
|
||||
{ Keycode.AUDIOMUTE, "AudioMute" },
|
||||
{ Keycode.MEDIASELECT, "MediaSelect" },
|
||||
{ Keycode.WWW, "WWW" },
|
||||
{ Keycode.MAIL, "Mail" },
|
||||
{ Keycode.CALCULATOR, "Calculator" },
|
||||
{ Keycode.COMPUTER, "Computer" },
|
||||
{ Keycode.AC_SEARCH, "AC Search" },
|
||||
{ Keycode.AC_HOME, "AC Home" },
|
||||
{ Keycode.AC_BACK, "AC Back" },
|
||||
{ Keycode.AC_FORWARD, "AC Forward" },
|
||||
{ Keycode.AC_STOP, "AC Stop" },
|
||||
{ Keycode.AC_REFRESH, "AC Refresh" },
|
||||
{ Keycode.AC_BOOKMARKS, "AC Bookmarks" },
|
||||
{ Keycode.BRIGHTNESSDOWN, "BrightnessDown" },
|
||||
{ Keycode.BRIGHTNESSUP, "BrightnessUp" },
|
||||
{ Keycode.DISPLAYSWITCH, "DisplaySwitch" },
|
||||
{ Keycode.KBDILLUMTOGGLE, "KBDIllumToggle" },
|
||||
{ Keycode.KBDILLUMDOWN, "KBDIllumDown" },
|
||||
{ Keycode.KBDILLUMUP, "KBDIllumUp" },
|
||||
{ Keycode.EJECT, "Eject" },
|
||||
{ Keycode.SLEEP, "Sleep" },
|
||||
{ Keycode.MOUSE4, "Mouse 4" },
|
||||
{ Keycode.MOUSE5, "Mouse 5" },
|
||||
{ Keycode.UNKNOWN, "keycode.unknown" },
|
||||
{ Keycode.RETURN, "keycode.return" },
|
||||
{ Keycode.ESCAPE, "keycode.escape" },
|
||||
{ Keycode.BACKSPACE, "keycode.backspace" },
|
||||
{ Keycode.TAB, "keycode.tab" },
|
||||
{ Keycode.SPACE, "keycode.space" },
|
||||
{ Keycode.EXCLAIM, "keycode.exclaim" },
|
||||
{ Keycode.QUOTEDBL, "keycode.quotedbl" },
|
||||
{ Keycode.HASH, "keycode.hash" },
|
||||
{ Keycode.PERCENT, "keycode.percent" },
|
||||
{ Keycode.DOLLAR, "keycode.dollar" },
|
||||
{ Keycode.AMPERSAND, "keycode.ampersand" },
|
||||
{ Keycode.QUOTE, "keycode.quote" },
|
||||
{ Keycode.LEFTPAREN, "keycode.leftparen" },
|
||||
{ Keycode.RIGHTPAREN, "keycode.rightparen" },
|
||||
{ Keycode.ASTERISK, "keycode.asterisk" },
|
||||
{ Keycode.PLUS, "keycode.plus" },
|
||||
{ Keycode.COMMA, "keycode.comma" },
|
||||
{ Keycode.MINUS, "keycode.minus" },
|
||||
{ Keycode.PERIOD, "keycode.period" },
|
||||
{ Keycode.SLASH, "keycode.slash" },
|
||||
{ Keycode.NUMBER_0, "keycode.number_0" },
|
||||
{ Keycode.NUMBER_1, "keycode.number_1" },
|
||||
{ Keycode.NUMBER_2, "keycode.number_2" },
|
||||
{ Keycode.NUMBER_3, "keycode.number_3" },
|
||||
{ Keycode.NUMBER_4, "keycode.number_4" },
|
||||
{ Keycode.NUMBER_5, "keycode.number_5" },
|
||||
{ Keycode.NUMBER_6, "keycode.number_6" },
|
||||
{ Keycode.NUMBER_7, "keycode.number_7" },
|
||||
{ Keycode.NUMBER_8, "keycode.number_8" },
|
||||
{ Keycode.NUMBER_9, "keycode.number_9" },
|
||||
{ Keycode.COLON, "keycode.colon" },
|
||||
{ Keycode.SEMICOLON, "keycode.semicolon" },
|
||||
{ Keycode.LESS, "keycode.less" },
|
||||
{ Keycode.EQUALS, "keycode.equals" },
|
||||
{ Keycode.GREATER, "keycode.greater" },
|
||||
{ Keycode.QUESTION, "keycode.question" },
|
||||
{ Keycode.AT, "keycode.at" },
|
||||
{ Keycode.LEFTBRACKET, "keycode.leftbracket" },
|
||||
{ Keycode.BACKSLASH, "keycode.backslash" },
|
||||
{ Keycode.RIGHTBRACKET, "keycode.rightbracket" },
|
||||
{ Keycode.CARET, "keycode.caret" },
|
||||
{ Keycode.UNDERSCORE, "keycode.underscore" },
|
||||
{ Keycode.BACKQUOTE, "keycode.backquote" },
|
||||
{ Keycode.A, "keycode.a" },
|
||||
{ Keycode.B, "keycode.b" },
|
||||
{ Keycode.C, "keycode.c" },
|
||||
{ Keycode.D, "keycode.d" },
|
||||
{ Keycode.E, "keycode.e" },
|
||||
{ Keycode.F, "keycode.f" },
|
||||
{ Keycode.G, "keycode.g" },
|
||||
{ Keycode.H, "keycode.h" },
|
||||
{ Keycode.I, "keycode.i" },
|
||||
{ Keycode.J, "keycode.j" },
|
||||
{ Keycode.K, "keycode.k" },
|
||||
{ Keycode.L, "keycode.l" },
|
||||
{ Keycode.M, "keycode.m" },
|
||||
{ Keycode.N, "keycode.n" },
|
||||
{ Keycode.O, "keycode.o" },
|
||||
{ Keycode.P, "keycode.p" },
|
||||
{ Keycode.Q, "keycode.q" },
|
||||
{ Keycode.R, "keycode.r" },
|
||||
{ Keycode.S, "keycode.s" },
|
||||
{ Keycode.T, "keycode.t" },
|
||||
{ Keycode.U, "keycode.u" },
|
||||
{ Keycode.V, "keycode.v" },
|
||||
{ Keycode.W, "keycode.w" },
|
||||
{ Keycode.X, "keycode.x" },
|
||||
{ Keycode.Y, "keycode.y" },
|
||||
{ Keycode.Z, "keycode.z" },
|
||||
{ Keycode.CAPSLOCK, "keycode.capslock" },
|
||||
{ Keycode.F1, "keycode.f1" },
|
||||
{ Keycode.F2, "keycode.f2" },
|
||||
{ Keycode.F3, "keycode.f3" },
|
||||
{ Keycode.F4, "keycode.f4" },
|
||||
{ Keycode.F5, "keycode.f5" },
|
||||
{ Keycode.F6, "keycode.f6" },
|
||||
{ Keycode.F7, "keycode.f7" },
|
||||
{ Keycode.F8, "keycode.f8" },
|
||||
{ Keycode.F9, "keycode.f9" },
|
||||
{ Keycode.F10, "keycode.f10" },
|
||||
{ Keycode.F11, "keycode.f11" },
|
||||
{ Keycode.F12, "keycode.f12" },
|
||||
{ Keycode.PRINTSCREEN, "keycode.printscreen" },
|
||||
{ Keycode.SCROLLLOCK, "keycode.scrolllock" },
|
||||
{ Keycode.PAUSE, "keycode.pause" },
|
||||
{ Keycode.INSERT, "keycode.insert" },
|
||||
{ Keycode.HOME, "keycode.home" },
|
||||
{ Keycode.PAGEUP, "keycode.pageup" },
|
||||
{ Keycode.DELETE, "keycode.delete" },
|
||||
{ Keycode.END, "keycode.end" },
|
||||
{ Keycode.PAGEDOWN, "keycode.pagedown" },
|
||||
{ Keycode.RIGHT, "keycode.right" },
|
||||
{ Keycode.LEFT, "keycode.left" },
|
||||
{ Keycode.DOWN, "keycode.down" },
|
||||
{ Keycode.UP, "keycode.up" },
|
||||
{ Keycode.NUMLOCKCLEAR, "keycode.numlockclear" },
|
||||
{ Keycode.KP_DIVIDE, "keycode.kp_divide" },
|
||||
{ Keycode.KP_MULTIPLY, "keycode.kp_multiply" },
|
||||
{ Keycode.KP_MINUS, "keycode.kp_minus" },
|
||||
{ Keycode.KP_PLUS, "keycode.kp_plus" },
|
||||
{ Keycode.KP_ENTER, "keycode.kp_enter" },
|
||||
{ Keycode.KP_1, "keycode.kp_1" },
|
||||
{ Keycode.KP_2, "keycode.kp_2" },
|
||||
{ Keycode.KP_3, "keycode.kp_3" },
|
||||
{ Keycode.KP_4, "keycode.kp_4" },
|
||||
{ Keycode.KP_5, "keycode.kp_5" },
|
||||
{ Keycode.KP_6, "keycode.kp_6" },
|
||||
{ Keycode.KP_7, "keycode.kp_7" },
|
||||
{ Keycode.KP_8, "keycode.kp_8" },
|
||||
{ Keycode.KP_9, "keycode.kp_9" },
|
||||
{ Keycode.KP_0, "keycode.kp_0" },
|
||||
{ Keycode.KP_PERIOD, "keycode.kp_period" },
|
||||
{ Keycode.APPLICATION, "keycode.application" },
|
||||
{ Keycode.POWER, "keycode.power" },
|
||||
{ Keycode.KP_EQUALS, "keycode.kp_equals" },
|
||||
{ Keycode.F13, "keycode.f13" },
|
||||
{ Keycode.F14, "keycode.f14" },
|
||||
{ Keycode.F15, "keycode.f15" },
|
||||
{ Keycode.F16, "keycode.f16" },
|
||||
{ Keycode.F17, "keycode.f17" },
|
||||
{ Keycode.F18, "keycode.f18" },
|
||||
{ Keycode.F19, "keycode.f19" },
|
||||
{ Keycode.F20, "keycode.f20" },
|
||||
{ Keycode.F21, "keycode.f21" },
|
||||
{ Keycode.F22, "keycode.f22" },
|
||||
{ Keycode.F23, "keycode.f23" },
|
||||
{ Keycode.F24, "keycode.f24" },
|
||||
{ Keycode.EXECUTE, "keycode.execute" },
|
||||
{ Keycode.HELP, "keycode.help" },
|
||||
{ Keycode.MENU, "keycode.menu" },
|
||||
{ Keycode.SELECT, "keycode.select" },
|
||||
{ Keycode.STOP, "keycode.stop" },
|
||||
{ Keycode.AGAIN, "keycode.again" },
|
||||
{ Keycode.UNDO, "keycode.undo" },
|
||||
{ Keycode.CUT, "keycode.cut" },
|
||||
{ Keycode.COPY, "keycode.copy" },
|
||||
{ Keycode.PASTE, "keycode.paste" },
|
||||
{ Keycode.FIND, "keycode.find" },
|
||||
{ Keycode.MUTE, "keycode.mute" },
|
||||
{ Keycode.VOLUMEUP, "keycode.volumeup" },
|
||||
{ Keycode.VOLUMEDOWN, "keycode.volumedown" },
|
||||
{ Keycode.KP_COMMA, "keycode.kp_comma" },
|
||||
{ Keycode.KP_EQUALSAS400, "keycode.kp_equalsas400" },
|
||||
{ Keycode.ALTERASE, "keycode.alterase" },
|
||||
{ Keycode.SYSREQ, "keycode.sysreq" },
|
||||
{ Keycode.CANCEL, "keycode.cancel" },
|
||||
{ Keycode.CLEAR, "keycode.clear" },
|
||||
{ Keycode.PRIOR, "keycode.prior" },
|
||||
{ Keycode.RETURN2, "keycode.return2" },
|
||||
{ Keycode.SEPARATOR, "keycode.separator" },
|
||||
{ Keycode.OUT, "keycode.out" },
|
||||
{ Keycode.OPER, "keycode.oper" },
|
||||
{ Keycode.CLEARAGAIN, "keycode.clearagain" },
|
||||
{ Keycode.CRSEL, "keycode.crsel" },
|
||||
{ Keycode.EXSEL, "keycode.exsel" },
|
||||
{ Keycode.KP_00, "keycode.kp_00" },
|
||||
{ Keycode.KP_000, "keycode.kp_000" },
|
||||
{ Keycode.THOUSANDSSEPARATOR, "keycode.thousandsseparator" },
|
||||
{ Keycode.DECIMALSEPARATOR, "keycode.decimalseparator" },
|
||||
{ Keycode.CURRENCYUNIT, "keycode.currencyunit" },
|
||||
{ Keycode.CURRENCYSUBUNIT, "keycode.currencysubunit" },
|
||||
{ Keycode.KP_LEFTPAREN, "keycode.kp_leftparen" },
|
||||
{ Keycode.KP_RIGHTPAREN, "keycode.kp_rightparen" },
|
||||
{ Keycode.KP_LEFTBRACE, "keycode.kp_leftbrace" },
|
||||
{ Keycode.KP_RIGHTBRACE, "keycode.kp_rightbrace" },
|
||||
{ Keycode.KP_TAB, "keycode.kp_tab" },
|
||||
{ Keycode.KP_BACKSPACE, "keycode.kp_backspace" },
|
||||
{ Keycode.KP_A, "keycode.kp_a" },
|
||||
{ Keycode.KP_B, "keycode.kp_b" },
|
||||
{ Keycode.KP_C, "keycode.kp_c" },
|
||||
{ Keycode.KP_D, "keycode.kp_d" },
|
||||
{ Keycode.KP_E, "keycode.kp_e" },
|
||||
{ Keycode.KP_F, "keycode.kp_f" },
|
||||
{ Keycode.KP_XOR, "keycode.kp_xor" },
|
||||
{ Keycode.KP_POWER, "keycode.kp_power" },
|
||||
{ Keycode.KP_PERCENT, "keycode.kp_percent" },
|
||||
{ Keycode.KP_LESS, "keycode.kp_less" },
|
||||
{ Keycode.KP_GREATER, "keycode.kp_greater" },
|
||||
{ Keycode.KP_AMPERSAND, "keycode.kp_ampersand" },
|
||||
{ Keycode.KP_DBLAMPERSAND, "keycode.kp_dblampersand" },
|
||||
{ Keycode.KP_VERTICALBAR, "keycode.kp_verticalbar" },
|
||||
{ Keycode.KP_DBLVERTICALBAR, "keycode.kp_dblverticalbar" },
|
||||
{ Keycode.KP_COLON, "keycode.kp_colon" },
|
||||
{ Keycode.KP_HASH, "keycode.kp_hash" },
|
||||
{ Keycode.KP_SPACE, "keycode.kp_space" },
|
||||
{ Keycode.KP_AT, "keycode.kp_at" },
|
||||
{ Keycode.KP_EXCLAM, "keycode.kp_exclam" },
|
||||
{ Keycode.KP_MEMSTORE, "keycode.kp_memstore" },
|
||||
{ Keycode.KP_MEMRECALL, "keycode.kp_memrecall" },
|
||||
{ Keycode.KP_MEMCLEAR, "keycode.kp_memclear" },
|
||||
{ Keycode.KP_MEMADD, "keycode.kp_memadd" },
|
||||
{ Keycode.KP_MEMSUBTRACT, "keycode.kp_memsubtract" },
|
||||
{ Keycode.KP_MEMMULTIPLY, "keycode.kp_memmultiply" },
|
||||
{ Keycode.KP_MEMDIVIDE, "keycode.kp_memdivide" },
|
||||
{ Keycode.KP_PLUSMINUS, "keycode.kp_plusminus" },
|
||||
{ Keycode.KP_CLEAR, "keycode.kp_clear" },
|
||||
{ Keycode.KP_CLEARENTRY, "keycode.kp_clearentry" },
|
||||
{ Keycode.KP_BINARY, "keycode.kp_binary" },
|
||||
{ Keycode.KP_OCTAL, "keycode.kp_octal" },
|
||||
{ Keycode.KP_DECIMAL, "keycode.kp_decimal" },
|
||||
{ Keycode.KP_HEXADECIMAL, "keycode.kp_hexadecimal" },
|
||||
{ Keycode.LCTRL, "keycode.lctrl" },
|
||||
{ Keycode.LSHIFT, "keycode.lshift" },
|
||||
{ Keycode.LALT, "keycode.lalt" },
|
||||
{ Keycode.LGUI, "keycode.lgui" },
|
||||
{ Keycode.RCTRL, "keycode.rctrl" },
|
||||
{ Keycode.RSHIFT, "keycode.rshift" },
|
||||
{ Keycode.RALT, "keycode.ralt" },
|
||||
{ Keycode.RGUI, "keycode.rgui" },
|
||||
{ Keycode.MODE, "keycode.mode" },
|
||||
{ Keycode.AUDIONEXT, "keycode.audionext" },
|
||||
{ Keycode.AUDIOPREV, "keycode.audioprev" },
|
||||
{ Keycode.AUDIOSTOP, "keycode.audiostop" },
|
||||
{ Keycode.AUDIOPLAY, "keycode.audioplay" },
|
||||
{ Keycode.AUDIOMUTE, "keycode.audiomute" },
|
||||
{ Keycode.MEDIASELECT, "keycode.mediaselect" },
|
||||
{ Keycode.WWW, "keycode.www" },
|
||||
{ Keycode.MAIL, "keycode.mail" },
|
||||
{ Keycode.CALCULATOR, "keycode.calculator" },
|
||||
{ Keycode.COMPUTER, "keycode.computer" },
|
||||
{ Keycode.AC_SEARCH, "keycode.ac_search" },
|
||||
{ Keycode.AC_HOME, "keycode.ac_home" },
|
||||
{ Keycode.AC_BACK, "keycode.ac_back" },
|
||||
{ Keycode.AC_FORWARD, "keycode.ac_forward" },
|
||||
{ Keycode.AC_STOP, "keycode.ac_stop" },
|
||||
{ Keycode.AC_REFRESH, "keycode.ac_refresh" },
|
||||
{ Keycode.AC_BOOKMARKS, "keycode.ac_bookmarks" },
|
||||
{ Keycode.BRIGHTNESSDOWN, "keycode.brightnessdown" },
|
||||
{ Keycode.BRIGHTNESSUP, "keycode.brightnessup" },
|
||||
{ Keycode.DISPLAYSWITCH, "keycode.displayswitch" },
|
||||
{ Keycode.KBDILLUMTOGGLE, "keycode.kbdillumtoggle" },
|
||||
{ Keycode.KBDILLUMDOWN, "keycode.kbdillumdown" },
|
||||
{ Keycode.KBDILLUMUP, "keycode.kbdillumup" },
|
||||
{ Keycode.EJECT, "keycode.eject" },
|
||||
{ Keycode.SLEEP, "keycode.sleep" },
|
||||
{ Keycode.MOUSE4, "keycode.mouse4" },
|
||||
{ Keycode.MOUSE5, "keycode.mouse5" },
|
||||
};
|
||||
|
||||
public static string DisplayString(Keycode k)
|
||||
{
|
||||
if (!KeyNames.TryGetValue(k, out var ret))
|
||||
if (!KeycodeFluentKeys.TryGetValue(k, out var fluentKey))
|
||||
return k.ToString();
|
||||
|
||||
return ret;
|
||||
return FluentProvider.GetMessage(fluentKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,10 +84,11 @@ namespace OpenRA
|
||||
{
|
||||
var client = HttpClientFactory.Create();
|
||||
|
||||
var httpResponseMessage = await client.GetAsync(playerDatabase.Profile + Fingerprint);
|
||||
var url = playerDatabase.Profile + Fingerprint;
|
||||
var httpResponseMessage = await client.GetAsync(url);
|
||||
var result = await httpResponseMessage.Content.ReadAsStreamAsync();
|
||||
|
||||
var yaml = MiniYaml.FromStream(result).First();
|
||||
var yaml = MiniYaml.FromStream(result, url).First();
|
||||
if (yaml.Key == "Player")
|
||||
{
|
||||
innerData = FieldLoader.Load<PlayerProfile>(yaml.Value);
|
||||
|
||||
@@ -56,8 +56,7 @@ namespace OpenRA
|
||||
// (b) Therefore:
|
||||
// - au + 2bv adds (a + b) to (x, y)
|
||||
// - a correction factor is added if v is odd
|
||||
var offset = (V & 1) == 1 ? 1 : 0;
|
||||
var y = (V - offset) / 2 - U;
|
||||
var y = (V - (V & 1)) / 2 - U;
|
||||
var x = V - y;
|
||||
return new CPos(x, y);
|
||||
}
|
||||
|
||||
@@ -9,103 +9,92 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public interface IGlobalModData { }
|
||||
|
||||
public sealed class TerrainFormat : IGlobalModData
|
||||
{
|
||||
public readonly string Type;
|
||||
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
|
||||
public TerrainFormat(MiniYaml yaml)
|
||||
{
|
||||
Type = yaml.Value;
|
||||
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SpriteSequenceFormat : IGlobalModData
|
||||
{
|
||||
public readonly string Type;
|
||||
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
|
||||
public SpriteSequenceFormat(MiniYaml yaml)
|
||||
{
|
||||
Type = yaml.Value;
|
||||
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ModelSequenceFormat : IGlobalModData
|
||||
{
|
||||
public readonly string Type;
|
||||
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
|
||||
public ModelSequenceFormat(MiniYaml yaml)
|
||||
{
|
||||
Type = yaml.Value;
|
||||
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
|
||||
}
|
||||
}
|
||||
|
||||
public class ModMetadata
|
||||
{
|
||||
public string Title;
|
||||
public string Version;
|
||||
public string Website;
|
||||
public string WebIcon32;
|
||||
public string WindowTitle;
|
||||
public bool Hidden;
|
||||
// FieldLoader used here, must matching naming in YAML.
|
||||
#pragma warning disable IDE1006 // Naming Styles
|
||||
[FluentReference]
|
||||
public readonly string Title;
|
||||
public readonly string Version;
|
||||
public readonly string Website;
|
||||
public readonly string WebIcon32;
|
||||
|
||||
[FluentReference(optional: true)]
|
||||
public readonly string WindowTitle;
|
||||
public readonly bool Hidden;
|
||||
#pragma warning restore IDE1006 // Naming Styles
|
||||
|
||||
public string TitleTranslated => FluentProvider.GetMessage(Title);
|
||||
public string WindowTitleTranslated => WindowTitle != null ? FluentProvider.GetMessage(WindowTitle) : null;
|
||||
}
|
||||
|
||||
public class RendererConstants
|
||||
{
|
||||
public readonly int FontSheetSize = 512;
|
||||
public readonly int CursorSheetSize = 512;
|
||||
public readonly int MapPreviewSheetSize = 2048;
|
||||
public readonly int SequenceBgraSheetSize = 2048;
|
||||
public readonly int SequenceIndexedSheetSize = 2048;
|
||||
public readonly int VertexBatchSize = 8192;
|
||||
}
|
||||
|
||||
/// <summary>Describes what is to be loaded in order to run a mod.</summary>
|
||||
public sealed class Manifest : IDisposable
|
||||
public sealed class Manifest
|
||||
{
|
||||
public readonly string Id;
|
||||
public readonly IReadOnlyPackage Package;
|
||||
public readonly ModMetadata Metadata;
|
||||
public readonly string[]
|
||||
public readonly ImmutableArray<string>
|
||||
Rules, ServerTraits,
|
||||
Sequences, ModelSequences, Cursors, Chrome, Assemblies, ChromeLayout,
|
||||
Weapons, Voices, Notifications, Music, Translations, TileSets,
|
||||
Sequences, ModelSequences, Cursors, Chrome, ChromeLayout,
|
||||
Weapons, Voices, Notifications, Music, FluentMessages, TileSets,
|
||||
ChromeMetrics, MapCompatibility, Missions, Hotkeys;
|
||||
|
||||
public readonly IReadOnlyDictionary<string, string> Packages;
|
||||
public readonly IReadOnlyDictionary<string, string> MapFolders;
|
||||
public readonly FrozenDictionary<string, string> MapFolders;
|
||||
public readonly MiniYaml FileSystem;
|
||||
public readonly MiniYaml LoadScreen;
|
||||
public readonly string DefaultOrderGenerator;
|
||||
public readonly RendererConstants RendererConstants;
|
||||
|
||||
public readonly string[] SoundFormats = Array.Empty<string>();
|
||||
public readonly string[] SpriteFormats = Array.Empty<string>();
|
||||
public readonly string[] PackageFormats = Array.Empty<string>();
|
||||
public readonly string[] VideoFormats = Array.Empty<string>();
|
||||
public readonly ImmutableArray<string> Assemblies = [];
|
||||
public readonly ImmutableArray<string> SoundFormats = [];
|
||||
public readonly ImmutableArray<string> SpriteFormats = [];
|
||||
public readonly ImmutableArray<string> PackageFormats = [];
|
||||
public readonly ImmutableArray<string> VideoFormats = [];
|
||||
public readonly string SpriteSequenceFormat;
|
||||
public readonly string TerrainFormat;
|
||||
|
||||
readonly string[] reservedModuleNames =
|
||||
// TODO: This should be controlled by a user-selected translation bundle!
|
||||
public readonly string FluentCulture = "en";
|
||||
public readonly bool AllowUnusedFluentMessagesInExternalPackages = true;
|
||||
|
||||
static readonly FrozenSet<string> ReservedModuleNames = new HashSet<string>
|
||||
{
|
||||
"Include", "Metadata", "Folders", "MapFolders", "Packages", "Rules",
|
||||
"Include", "Metadata", "FileSystem", "MapFolders", "Rules",
|
||||
"Sequences", "ModelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons",
|
||||
"Voices", "Notifications", "Music", "Translations", "TileSets", "ChromeMetrics", "Missions", "Hotkeys",
|
||||
"Voices", "Notifications", "Music", "FluentMessages", "TileSets", "ChromeMetrics", "Missions", "Hotkeys",
|
||||
"ServerTraits", "LoadScreen", "DefaultOrderGenerator", "SupportsMapsFrom", "SoundFormats", "SpriteFormats", "VideoFormats",
|
||||
"RequiresMods", "PackageFormats"
|
||||
};
|
||||
"SpriteSequenceFormat", "TerrainFormat", "RequiresMods", "PackageFormats", "AllowUnusedFluentMessagesInExternalPackages", "RendererConstants"
|
||||
}.ToFrozenSet();
|
||||
|
||||
readonly TypeDictionary modules = new();
|
||||
readonly Dictionary<string, MiniYaml> yaml;
|
||||
|
||||
bool customDataLoaded;
|
||||
public readonly FrozenDictionary<string, MiniYaml> GlobalModData;
|
||||
|
||||
public Manifest(string modId, IReadOnlyPackage package)
|
||||
{
|
||||
Id = modId;
|
||||
Package = package;
|
||||
|
||||
var nodes = MiniYaml.FromStream(package.GetStream("mod.yaml"), "mod.yaml");
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
var nodes = MiniYaml.FromStream(package.GetStream("mod.yaml"), $"{package.Name}:mod.yaml", stringPool: stringPool).ToList();
|
||||
for (var i = nodes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (nodes[i].Key != "Include")
|
||||
@@ -118,32 +107,31 @@ namespace OpenRA
|
||||
throw new YamlException($"{nodes[i].Location}: File `{filename}` not found.");
|
||||
|
||||
nodes.RemoveAt(i);
|
||||
nodes.InsertRange(i, MiniYaml.FromStream(contents, filename));
|
||||
nodes.InsertRange(i, MiniYaml.FromStream(contents, $"{package.Name}:{filename}", stringPool: stringPool));
|
||||
}
|
||||
|
||||
// Merge inherited overrides
|
||||
yaml = new MiniYaml(null, MiniYaml.Merge(new[] { nodes })).ToDictionary();
|
||||
var yaml = new MiniYaml(null, MiniYaml.Merge([nodes])).ToDictionary();
|
||||
|
||||
Metadata = FieldLoader.Load<ModMetadata>(yaml["Metadata"]);
|
||||
|
||||
// TODO: Use fieldloader
|
||||
MapFolders = YamlDictionary(yaml, "MapFolders");
|
||||
|
||||
if (yaml.TryGetValue("Packages", out var packages))
|
||||
Packages = packages.ToDictionary(x => x.Value);
|
||||
if (!yaml.TryGetValue("FileSystem", out FileSystem))
|
||||
throw new InvalidDataException("`FileSystem` section is not defined.");
|
||||
|
||||
Rules = YamlList(yaml, "Rules");
|
||||
Sequences = YamlList(yaml, "Sequences");
|
||||
ModelSequences = YamlList(yaml, "ModelSequences");
|
||||
Cursors = YamlList(yaml, "Cursors");
|
||||
Chrome = YamlList(yaml, "Chrome");
|
||||
Assemblies = YamlList(yaml, "Assemblies");
|
||||
ChromeLayout = YamlList(yaml, "ChromeLayout");
|
||||
Weapons = YamlList(yaml, "Weapons");
|
||||
Voices = YamlList(yaml, "Voices");
|
||||
Notifications = YamlList(yaml, "Notifications");
|
||||
Music = YamlList(yaml, "Music");
|
||||
Translations = YamlList(yaml, "Translations");
|
||||
FluentMessages = YamlList(yaml, "FluentMessages");
|
||||
TileSets = YamlList(yaml, "TileSets");
|
||||
ChromeMetrics = YamlList(yaml, "ChromeMetrics");
|
||||
Missions = YamlList(yaml, "Missions");
|
||||
@@ -160,131 +148,59 @@ namespace OpenRA
|
||||
if (yaml.TryGetValue("SupportsMapsFrom", out var entry))
|
||||
compat.AddRange(entry.Value.Split(',').Select(c => c.Trim()));
|
||||
|
||||
MapCompatibility = compat.ToArray();
|
||||
MapCompatibility = compat.ToImmutableArray();
|
||||
|
||||
if (yaml.TryGetValue("DefaultOrderGenerator", out entry))
|
||||
DefaultOrderGenerator = entry.Value;
|
||||
|
||||
if (yaml.TryGetValue("Assemblies", out entry))
|
||||
Assemblies = FieldLoader.GetValue<ImmutableArray<string>>("Assemblies", entry.Value);
|
||||
|
||||
if (yaml.TryGetValue("PackageFormats", out entry))
|
||||
PackageFormats = FieldLoader.GetValue<string[]>("PackageFormats", entry.Value);
|
||||
PackageFormats = FieldLoader.GetValue<ImmutableArray<string>>("PackageFormats", entry.Value);
|
||||
|
||||
if (yaml.TryGetValue("SoundFormats", out entry))
|
||||
SoundFormats = FieldLoader.GetValue<string[]>("SoundFormats", entry.Value);
|
||||
SoundFormats = FieldLoader.GetValue<ImmutableArray<string>>("SoundFormats", entry.Value);
|
||||
|
||||
if (yaml.TryGetValue("SpriteFormats", out entry))
|
||||
SpriteFormats = FieldLoader.GetValue<string[]>("SpriteFormats", entry.Value);
|
||||
SpriteFormats = FieldLoader.GetValue<ImmutableArray<string>>("SpriteFormats", entry.Value);
|
||||
|
||||
if (yaml.TryGetValue("VideoFormats", out entry))
|
||||
VideoFormats = FieldLoader.GetValue<string[]>("VideoFormats", entry.Value);
|
||||
}
|
||||
VideoFormats = FieldLoader.GetValue<ImmutableArray<string>>("VideoFormats", entry.Value);
|
||||
|
||||
public void LoadCustomData(ObjectCreator oc)
|
||||
{
|
||||
foreach (var kv in yaml)
|
||||
{
|
||||
if (reservedModuleNames.Contains(kv.Key))
|
||||
continue;
|
||||
if (yaml.TryGetValue("SpriteSequenceFormat", out entry))
|
||||
SpriteSequenceFormat = entry.Value;
|
||||
|
||||
var t = oc.FindType(kv.Key);
|
||||
if (t == null || !typeof(IGlobalModData).IsAssignableFrom(t))
|
||||
throw new InvalidDataException($"`{kv.Key}` is not a valid mod manifest entry.");
|
||||
if (yaml.TryGetValue("TerrainFormat", out entry))
|
||||
TerrainFormat = entry.Value;
|
||||
|
||||
IGlobalModData module;
|
||||
var ctor = t.GetConstructor(new[] { typeof(MiniYaml) });
|
||||
if (ctor != null)
|
||||
{
|
||||
// Class has opted-in to DIY initialization
|
||||
module = (IGlobalModData)ctor.Invoke(new object[] { kv.Value });
|
||||
}
|
||||
else
|
||||
{
|
||||
// Automatically load the child nodes using FieldLoader
|
||||
module = oc.CreateObject<IGlobalModData>(kv.Key);
|
||||
FieldLoader.Load(module, kv.Value);
|
||||
}
|
||||
if (yaml.TryGetValue("AllowUnusedFluentMessagesInExternalPackages", out entry))
|
||||
AllowUnusedFluentMessagesInExternalPackages =
|
||||
FieldLoader.GetValue<bool>("AllowUnusedFluentMessagesInExternalPackages", entry.Value);
|
||||
|
||||
modules.Add(module);
|
||||
}
|
||||
|
||||
customDataLoaded = true;
|
||||
}
|
||||
|
||||
static string[] YamlList(Dictionary<string, MiniYaml> yaml, string key)
|
||||
{
|
||||
if (!yaml.ContainsKey(key))
|
||||
return Array.Empty<string>();
|
||||
|
||||
return yaml[key].ToDictionary().Keys.ToArray();
|
||||
}
|
||||
|
||||
static IReadOnlyDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
|
||||
{
|
||||
if (!yaml.ContainsKey(key))
|
||||
return new Dictionary<string, string>();
|
||||
|
||||
return yaml[key].ToDictionary(my => my.Value);
|
||||
}
|
||||
|
||||
public bool Contains<T>() where T : IGlobalModData
|
||||
{
|
||||
return modules.Contains<T>();
|
||||
}
|
||||
|
||||
/// <summary>Load a cached IGlobalModData instance.</summary>
|
||||
public T Get<T>() where T : IGlobalModData
|
||||
{
|
||||
if (!customDataLoaded)
|
||||
throw new InvalidOperationException("Attempted to call Manifest.Get() before loading custom data!");
|
||||
|
||||
var module = modules.GetOrDefault<T>();
|
||||
|
||||
// Lazily create the default values if not explicitly defined.
|
||||
if (module == null)
|
||||
{
|
||||
module = (T)Game.ModData.ObjectCreator.CreateBasic(typeof(T));
|
||||
modules.Add(module);
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load an uncached IGlobalModData instance directly from the manifest yaml.
|
||||
/// This should only be used by external mods that want to query data from this mod.
|
||||
/// </summary>
|
||||
public T Get<T>(ObjectCreator oc) where T : IGlobalModData
|
||||
{
|
||||
var t = typeof(T);
|
||||
if (!yaml.TryGetValue(t.Name, out var data))
|
||||
{
|
||||
// Lazily create the default values if not explicitly defined.
|
||||
return (T)oc.CreateBasic(t);
|
||||
}
|
||||
|
||||
IGlobalModData module;
|
||||
var ctor = t.GetConstructor(new[] { typeof(MiniYaml) });
|
||||
if (ctor != null)
|
||||
{
|
||||
// Class has opted-in to DIY initialization
|
||||
module = (IGlobalModData)ctor.Invoke(new object[] { data.Value });
|
||||
}
|
||||
if (yaml.TryGetValue("RendererConstants", out entry))
|
||||
RendererConstants = FieldLoader.Load<RendererConstants>(entry);
|
||||
else
|
||||
{
|
||||
// Automatically load the child nodes using FieldLoader
|
||||
module = oc.CreateObject<IGlobalModData>(t.Name);
|
||||
FieldLoader.Load(module, data);
|
||||
}
|
||||
RendererConstants = new RendererConstants();
|
||||
|
||||
return (T)module;
|
||||
GlobalModData = yaml.Where(n => !ReservedModuleNames.Contains(n.Key))
|
||||
.ToFrozenDictionary(n => n.Key, n => n.Value);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
static ImmutableArray<string> YamlList(Dictionary<string, MiniYaml> yaml, string key)
|
||||
{
|
||||
foreach (var module in modules)
|
||||
{
|
||||
var disposableModule = module as IDisposable;
|
||||
disposableModule?.Dispose();
|
||||
}
|
||||
if (!yaml.TryGetValue(key, out var value))
|
||||
return [];
|
||||
|
||||
return value.Nodes.Select(n => n.Key).ToImmutableArray();
|
||||
}
|
||||
|
||||
static FrozenDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
|
||||
{
|
||||
if (!yaml.TryGetValue(key, out var value))
|
||||
return FrozenDictionary<string, string>.Empty;
|
||||
|
||||
return value.ToDictionary(my => my.Value).ToFrozenDictionary();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ namespace OpenRA
|
||||
|
||||
public virtual void Initialize(MiniYaml yaml)
|
||||
{
|
||||
Initialize((T)FieldLoader.GetValue(nameof(value), typeof(T), yaml.Value));
|
||||
Initialize(FieldLoader.GetValue<T>(nameof(value), yaml.Value));
|
||||
}
|
||||
|
||||
public virtual void Initialize(T value)
|
||||
@@ -175,8 +175,7 @@ namespace OpenRA
|
||||
protected CompositeActorInit(TraitInfo info)
|
||||
: base(info.InstanceName) { }
|
||||
|
||||
protected CompositeActorInit()
|
||||
: base() { }
|
||||
protected CompositeActorInit() { }
|
||||
|
||||
public virtual void Initialize(MiniYaml yaml)
|
||||
{
|
||||
@@ -217,10 +216,8 @@ namespace OpenRA
|
||||
}
|
||||
}
|
||||
|
||||
public class LocationInit : ValueActorInit<CPos>, ISingleInstanceInit
|
||||
public class LocationInit(CPos value) : ValueActorInit<CPos>(value), ISingleInstanceInit
|
||||
{
|
||||
public LocationInit(CPos value)
|
||||
: base(value) { }
|
||||
}
|
||||
|
||||
public class OwnerInit : ActorInit, ISingleInstanceInit
|
||||
|
||||
@@ -14,7 +14,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
@@ -30,15 +30,15 @@ namespace OpenRA
|
||||
internal TypeDictionary InitDict => initDict.Value;
|
||||
|
||||
public ActorReference(string type)
|
||||
: this(type, new Dictionary<string, MiniYaml>()) { }
|
||||
: this(type, new MiniYaml("")) { }
|
||||
|
||||
public ActorReference(string type, Dictionary<string, MiniYaml> inits)
|
||||
public ActorReference(string type, MiniYaml inits)
|
||||
{
|
||||
Type = type;
|
||||
initDict = Exts.Lazy(() =>
|
||||
{
|
||||
var dict = new TypeDictionary();
|
||||
foreach (var i in inits)
|
||||
foreach (var i in inits.Nodes)
|
||||
{
|
||||
var init = LoadInit(i.Key, i.Value);
|
||||
if (init is ISingleInstanceInit && dict.Contains(init.GetType()))
|
||||
@@ -54,13 +54,8 @@ namespace OpenRA
|
||||
public ActorReference(string type, TypeDictionary inits)
|
||||
{
|
||||
Type = type;
|
||||
initDict = new Lazy<TypeDictionary>(() =>
|
||||
{
|
||||
var dict = new TypeDictionary();
|
||||
foreach (var i in inits)
|
||||
dict.Add(i);
|
||||
return dict;
|
||||
});
|
||||
var initsClone = new TypeDictionary(inits);
|
||||
initDict = Exts.Lazy(() => initsClone);
|
||||
}
|
||||
|
||||
static ActorInit LoadInit(string initName, MiniYaml initYaml)
|
||||
@@ -70,21 +65,21 @@ namespace OpenRA
|
||||
if (type == null)
|
||||
throw new InvalidDataException($"Unknown initializer type '{initInstance[0]}Init'");
|
||||
|
||||
var init = (ActorInit)FormatterServices.GetUninitializedObject(type);
|
||||
var init = (ActorInit)RuntimeHelpers.GetUninitializedObject(type);
|
||||
if (initInstance.Length > 1)
|
||||
type.GetField(nameof(ActorInit.InstanceName)).SetValue(init, initInstance[1]);
|
||||
|
||||
var loader = type.GetMethod("Initialize", new[] { typeof(MiniYaml) });
|
||||
var loader = type.GetMethod("Initialize", [typeof(MiniYaml)]);
|
||||
if (loader == null)
|
||||
throw new InvalidDataException($"{initInstance[0]}Init does not define a yaml-assignable type.");
|
||||
|
||||
loader.Invoke(init, new[] { initYaml });
|
||||
loader.Invoke(init, [initYaml]);
|
||||
return init;
|
||||
}
|
||||
|
||||
public MiniYaml Save(Func<ActorInit, bool> initFilter = null)
|
||||
{
|
||||
var ret = new MiniYaml(Type);
|
||||
var nodes = new List<MiniYamlNode>();
|
||||
foreach (var o in initDict.Value)
|
||||
{
|
||||
if (o is not ActorInit init || o is ISuppressInitExport)
|
||||
@@ -98,10 +93,10 @@ namespace OpenRA
|
||||
if (!string.IsNullOrEmpty(init.InstanceName))
|
||||
initName += ActorInfo.TraitInstanceSeparator + init.InstanceName;
|
||||
|
||||
ret.Nodes.Add(new MiniYamlNode(initName, init.Save()));
|
||||
nodes.Add(new MiniYamlNode(initName, init.Save()));
|
||||
}
|
||||
|
||||
return ret;
|
||||
return new MiniYaml(Type, nodes);
|
||||
}
|
||||
|
||||
public IEnumerator<object> GetEnumerator() { return initDict.Value.GetEnumerator(); }
|
||||
@@ -110,11 +105,7 @@ namespace OpenRA
|
||||
|
||||
public ActorReference Clone()
|
||||
{
|
||||
var clone = new ActorReference(Type);
|
||||
foreach (var init in initDict.Value)
|
||||
clone.initDict.Value.Add(init);
|
||||
|
||||
return clone;
|
||||
return new ActorReference(Type, initDict.Value);
|
||||
}
|
||||
|
||||
public void Add(ActorInit init)
|
||||
@@ -125,6 +116,15 @@ namespace OpenRA
|
||||
InitDict.Add(init);
|
||||
}
|
||||
|
||||
public void Replace<T>(T init) where T : ActorInit, ISingleInstanceInit
|
||||
{
|
||||
var original = GetOrDefault<T>();
|
||||
if (original != null)
|
||||
Remove(original);
|
||||
|
||||
Add(init);
|
||||
}
|
||||
|
||||
public void Remove(ActorInit o) { initDict.Value.Remove(o); }
|
||||
|
||||
public int RemoveAll<T>() where T : ActorInit
|
||||
@@ -139,7 +139,7 @@ namespace OpenRA
|
||||
return removed;
|
||||
}
|
||||
|
||||
public IEnumerable<T> GetAll<T>() where T : ActorInit
|
||||
public IReadOnlyCollection<T> GetAll<T>() where T : ActorInit
|
||||
{
|
||||
return initDict.Value.WithInterface<T>();
|
||||
}
|
||||
@@ -152,8 +152,9 @@ namespace OpenRA
|
||||
// If a more specific init is not available, fall back to an unnamed init.
|
||||
// If duplicate inits are defined, take the last to match standard yaml override expectations
|
||||
if (info != null && !string.IsNullOrEmpty(info.InstanceName))
|
||||
return inits.LastOrDefault(i => i.InstanceName == info.InstanceName) ??
|
||||
inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName));
|
||||
return
|
||||
inits.LastOrDefault(i => i.InstanceName == info.InstanceName) ??
|
||||
inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName));
|
||||
|
||||
// Untagged traits will only use untagged inits
|
||||
return inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName));
|
||||
|
||||
121
OpenRA.Game/Map/CellCoordsRegion.cs
Normal file
121
OpenRA.Game/Map/CellCoordsRegion.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public readonly struct CellCoordsRegion : IEnumerable<CPos>
|
||||
{
|
||||
public struct CellCoordsEnumerator : IEnumerator<CPos>
|
||||
{
|
||||
readonly CellCoordsRegion r;
|
||||
|
||||
public CellCoordsEnumerator(CellCoordsRegion region)
|
||||
: this()
|
||||
{
|
||||
r = region;
|
||||
Reset();
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
var x = Current.X + 1;
|
||||
var y = Current.Y;
|
||||
|
||||
// Check for column overflow.
|
||||
if (x > r.BottomRight.X)
|
||||
{
|
||||
y++;
|
||||
x = r.TopLeft.X;
|
||||
|
||||
// Check for row overflow.
|
||||
if (y > r.BottomRight.Y)
|
||||
return false;
|
||||
}
|
||||
|
||||
Current = new CPos(x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Current = new CPos(r.TopLeft.X - 1, r.TopLeft.Y);
|
||||
}
|
||||
|
||||
public CPos Current { get; private set; }
|
||||
|
||||
readonly object IEnumerator.Current => Current;
|
||||
public readonly void Dispose() { }
|
||||
}
|
||||
|
||||
public CellCoordsRegion(CPos topLeft, CPos bottomRight)
|
||||
{
|
||||
TopLeft = topLeft;
|
||||
BottomRight = bottomRight;
|
||||
}
|
||||
|
||||
public bool Contains(CPos cell)
|
||||
{
|
||||
return cell.X >= TopLeft.X && cell.X <= BottomRight.X && cell.Y >= TopLeft.Y && cell.Y <= BottomRight.Y;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{TopLeft}->{BottomRight}";
|
||||
}
|
||||
|
||||
public CellCoordsEnumerator GetEnumerator()
|
||||
{
|
||||
return new CellCoordsEnumerator(this);
|
||||
}
|
||||
|
||||
IEnumerator<CPos> IEnumerable<CPos>.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
/// <summary>Returns the minimal region that covers at least the specified cells.</summary>
|
||||
public static CellCoordsRegion BoundingRegion(IReadOnlyCollection<CPos> cells)
|
||||
{
|
||||
if (cells == null || cells.Count == 0)
|
||||
throw new ArgumentException("cells must not be null or empty.", nameof(cells));
|
||||
|
||||
var minX = int.MaxValue;
|
||||
var minY = int.MaxValue;
|
||||
var maxX = int.MinValue;
|
||||
var maxY = int.MinValue;
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (minX > cell.X)
|
||||
minX = cell.X;
|
||||
if (maxX < cell.X)
|
||||
maxX = cell.X;
|
||||
if (minY > cell.Y)
|
||||
minY = cell.Y;
|
||||
if (maxY < cell.Y)
|
||||
maxY = cell.Y;
|
||||
}
|
||||
|
||||
return new CellCoordsRegion(new CPos(minX, minY), new CPos(maxX, maxY));
|
||||
}
|
||||
|
||||
public CPos TopLeft { get; }
|
||||
public CPos BottomRight { get; }
|
||||
}
|
||||
}
|
||||
@@ -63,12 +63,16 @@ namespace OpenRA
|
||||
|
||||
var u = (x - y) / 2;
|
||||
var v = x + y;
|
||||
if (!Bounds.Contains(u, v))
|
||||
throw new IndexOutOfRangeException();
|
||||
return v * Size.Width + u;
|
||||
}
|
||||
|
||||
// Resolve an array index from map coordinates
|
||||
int Index(MPos uv)
|
||||
{
|
||||
if (!Bounds.Contains(uv.U, uv.V))
|
||||
throw new IndexOutOfRangeException();
|
||||
return uv.V * Size.Width + uv.U;
|
||||
}
|
||||
|
||||
@@ -145,6 +149,9 @@ namespace OpenRA
|
||||
{
|
||||
return uv.Clamp(new Rectangle(0, 0, Size.Width - 1, Size.Height - 1));
|
||||
}
|
||||
|
||||
public CellRegion CellRegion =>
|
||||
new(GridType, new MPos(0, 0), new MPos(Size.Width - 1, Size.Height - 1));
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace OpenRA
|
||||
protected readonly Rectangle Bounds;
|
||||
|
||||
protected CellLayerBase(Map map)
|
||||
: this(map.Grid.Type, new Size(map.MapSize.X, map.MapSize.Y)) { }
|
||||
: this(map.Grid.Type, map.MapSize) { }
|
||||
|
||||
protected CellLayerBase(MapGridType gridType, Size size)
|
||||
{
|
||||
@@ -40,19 +40,19 @@ namespace OpenRA
|
||||
if (Size != anotherLayer.Size || GridType != anotherLayer.GridType)
|
||||
throw new ArgumentException("Layers must have a matching size and shape (grid type).", nameof(anotherLayer));
|
||||
|
||||
Array.Copy(anotherLayer.Entries, Entries, Entries.Length);
|
||||
anotherLayer.Entries.AsSpan().CopyTo(Entries);
|
||||
}
|
||||
|
||||
/// <summary>Clears the layer contents with their default value.</summary>
|
||||
public virtual void Clear()
|
||||
{
|
||||
Array.Clear(Entries, 0, Entries.Length);
|
||||
Entries.AsSpan().Clear();
|
||||
}
|
||||
|
||||
/// <summary>Clears the layer contents with a known value.</summary>
|
||||
public virtual void Clear(T clearValue)
|
||||
{
|
||||
Array.Fill(Entries, clearValue);
|
||||
Entries.AsSpan().Fill(clearValue);
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
@@ -64,5 +64,15 @@ namespace OpenRA
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public ReadOnlyMemory<T> AsReadOnlyMemory()
|
||||
{
|
||||
return Entries.AsMemory();
|
||||
}
|
||||
|
||||
public Memory<T> AsMemory()
|
||||
{
|
||||
return Entries.AsMemory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
@@ -64,9 +63,9 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
/// <summary>Returns the minimal region that covers at least the specified cells.</summary>
|
||||
public static CellRegion BoundingRegion(MapGridType shape, IEnumerable<CPos> cells)
|
||||
public static CellRegion BoundingRegion(MapGridType shape, IReadOnlyCollection<CPos> cells)
|
||||
{
|
||||
if (cells == null || !cells.Any())
|
||||
if (cells == null || cells.Count == 0)
|
||||
throw new ArgumentException("cells must not be null or empty.", nameof(cells));
|
||||
|
||||
var minU = int.MaxValue;
|
||||
@@ -103,6 +102,7 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public MapCoordsRegion MapCoords => new(mapTopLeft, mapBottomRight);
|
||||
public CellCoordsRegion CellCoords => new(TopLeft, BottomRight);
|
||||
|
||||
public CellRegionEnumerator GetEnumerator()
|
||||
{
|
||||
@@ -136,12 +136,12 @@ namespace OpenRA
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
u += 1;
|
||||
u++;
|
||||
|
||||
// Check for column overflow
|
||||
if (u > r.mapBottomRight.U)
|
||||
{
|
||||
v += 1;
|
||||
v++;
|
||||
u = r.mapTopLeft.U;
|
||||
|
||||
// Check for row overflow
|
||||
@@ -162,8 +162,8 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public CPos Current { get; private set; }
|
||||
object IEnumerator.Current => Current;
|
||||
public void Dispose() { }
|
||||
readonly object IEnumerator.Current => Current;
|
||||
public readonly void Dispose() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
@@ -32,12 +33,12 @@ namespace OpenRA
|
||||
public readonly uint HeightsOffset;
|
||||
public readonly uint ResourcesOffset;
|
||||
|
||||
public BinaryDataHeader(Stream s, int2 expectedSize)
|
||||
public BinaryDataHeader(Stream s, Size expectedSize)
|
||||
{
|
||||
Format = s.ReadUInt8();
|
||||
var width = s.ReadUInt16();
|
||||
var height = s.ReadUInt16();
|
||||
if (width != expectedSize.X || height != expectedSize.Y)
|
||||
if (width != expectedSize.Width || height != expectedSize.Height)
|
||||
throw new InvalidDataException("Invalid tile data");
|
||||
|
||||
if (Format == 1)
|
||||
@@ -90,13 +91,13 @@ namespace OpenRA
|
||||
throw new InvalidOperationException("Map does not have a field/property " + fieldName);
|
||||
|
||||
var t = field != null ? field.FieldType : property.PropertyType;
|
||||
type = t == typeof(List<MiniYamlNode>) ? Type.NodeList :
|
||||
type = t == typeof(IReadOnlyCollection<MiniYamlNode>) ? Type.NodeList :
|
||||
t == typeof(MiniYaml) ? Type.MiniYaml : Type.Normal;
|
||||
}
|
||||
|
||||
public void Deserialize(Map map, List<MiniYamlNode> nodes)
|
||||
public void Deserialize(Map map, MiniYaml yaml)
|
||||
{
|
||||
var node = nodes.FirstOrDefault(n => n.Key == key);
|
||||
var node = yaml.NodeWithKeyOrDefault(key);
|
||||
if (node == null)
|
||||
{
|
||||
if (required)
|
||||
@@ -111,7 +112,7 @@ namespace OpenRA
|
||||
else if (type == Type.MiniYaml)
|
||||
field.SetValue(map, node.Value);
|
||||
else
|
||||
FieldLoader.LoadField(map, fieldName, node.Value.Value);
|
||||
FieldLoader.LoadFieldOrProperty(map, fieldName, node.Value.Value);
|
||||
}
|
||||
|
||||
if (property != null)
|
||||
@@ -121,7 +122,7 @@ namespace OpenRA
|
||||
else if (type == Type.MiniYaml)
|
||||
property.SetValue(map, node.Value, null);
|
||||
else
|
||||
FieldLoader.LoadField(map, fieldName, node.Value.Value);
|
||||
FieldLoader.LoadFieldOrProperty(map, fieldName, node.Value.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,14 +131,14 @@ namespace OpenRA
|
||||
var value = field != null ? field.GetValue(map) : property.GetValue(map, null);
|
||||
if (type == Type.NodeList)
|
||||
{
|
||||
var listValue = (List<MiniYamlNode>)value;
|
||||
var listValue = (IReadOnlyCollection<MiniYamlNode>)value;
|
||||
if (required || listValue.Count > 0)
|
||||
nodes.Add(new MiniYamlNode(key, null, listValue));
|
||||
}
|
||||
else if (type == Type.MiniYaml)
|
||||
{
|
||||
var yamlValue = (MiniYaml)value;
|
||||
if (required || (yamlValue != null && (yamlValue.Value != null || yamlValue.Nodes.Count > 0)))
|
||||
if (required || (yamlValue != null && (yamlValue.Value != null || yamlValue.Nodes.Length > 0)))
|
||||
nodes.Add(new MiniYamlNode(key, yamlValue));
|
||||
}
|
||||
else
|
||||
@@ -157,28 +158,28 @@ namespace OpenRA
|
||||
|
||||
/// <summary>Defines the order of the fields in map.yaml.</summary>
|
||||
static readonly MapField[] YamlFields =
|
||||
{
|
||||
new MapField("MapFormat"),
|
||||
new MapField("RequiresMod"),
|
||||
new MapField("Title"),
|
||||
new MapField("Author"),
|
||||
new MapField("Tileset"),
|
||||
new MapField("MapSize"),
|
||||
new MapField("Bounds"),
|
||||
new MapField("Visibility"),
|
||||
new MapField("Categories"),
|
||||
new MapField("LockPreview", required: false, ignoreIfValue: "False"),
|
||||
new MapField("Players", "PlayerDefinitions"),
|
||||
new MapField("Actors", "ActorDefinitions"),
|
||||
new MapField("Rules", "RuleDefinitions", required: false),
|
||||
new MapField("Translations", "TranslationDefinitions", required: false),
|
||||
new MapField("Sequences", "SequenceDefinitions", required: false),
|
||||
new MapField("ModelSequences", "ModelSequenceDefinitions", required: false),
|
||||
new MapField("Weapons", "WeaponDefinitions", required: false),
|
||||
new MapField("Voices", "VoiceDefinitions", required: false),
|
||||
new MapField("Music", "MusicDefinitions", required: false),
|
||||
new MapField("Notifications", "NotificationDefinitions", required: false),
|
||||
};
|
||||
[
|
||||
new("MapFormat"),
|
||||
new("RequiresMod"),
|
||||
new("Title"),
|
||||
new("Author"),
|
||||
new("Tileset"),
|
||||
new("MapSize"),
|
||||
new("Bounds"),
|
||||
new("Visibility"),
|
||||
new("Categories"),
|
||||
new("LockPreview", required: false, ignoreIfValue: "False"),
|
||||
new("Players", nameof(PlayerDefinitions)),
|
||||
new("Actors", nameof(ActorDefinitions)),
|
||||
new("Rules", nameof(RuleDefinitions), required: false),
|
||||
new("FluentMessages", nameof(FluentMessageDefinitions), required: false),
|
||||
new("Sequences", nameof(SequenceDefinitions), required: false),
|
||||
new("ModelSequences", nameof(ModelSequenceDefinitions), required: false),
|
||||
new("Weapons", nameof(WeaponDefinitions), required: false),
|
||||
new("Voices", nameof(VoiceDefinitions), required: false),
|
||||
new("Music", nameof(MusicDefinitions), required: false),
|
||||
new("Notifications", nameof(NotificationDefinitions), required: false),
|
||||
];
|
||||
|
||||
// Format versions
|
||||
public int MapFormat { get; private set; }
|
||||
@@ -192,25 +193,25 @@ namespace OpenRA
|
||||
public bool LockPreview;
|
||||
public Rectangle Bounds;
|
||||
public MapVisibility Visibility = MapVisibility.Lobby;
|
||||
public string[] Categories = { "Conquest" };
|
||||
public ImmutableArray<string> Categories = ["Conquest"];
|
||||
|
||||
public int2 MapSize { get; private set; }
|
||||
public Size MapSize { get; private set; }
|
||||
|
||||
// Player and actor yaml. Public for access by the map importers and lint checks.
|
||||
public List<MiniYamlNode> PlayerDefinitions = new();
|
||||
public List<MiniYamlNode> ActorDefinitions = new();
|
||||
public IReadOnlyCollection<MiniYamlNode> PlayerDefinitions = [];
|
||||
public IReadOnlyCollection<MiniYamlNode> ActorDefinitions = [];
|
||||
|
||||
// Custom map yaml. Public for access by the map importers and lint checks
|
||||
public readonly MiniYaml RuleDefinitions;
|
||||
public readonly MiniYaml TranslationDefinitions;
|
||||
public readonly MiniYaml SequenceDefinitions;
|
||||
public readonly MiniYaml ModelSequenceDefinitions;
|
||||
public readonly MiniYaml WeaponDefinitions;
|
||||
public readonly MiniYaml VoiceDefinitions;
|
||||
public readonly MiniYaml MusicDefinitions;
|
||||
public readonly MiniYaml NotificationDefinitions;
|
||||
public MiniYaml RuleDefinitions;
|
||||
public MiniYaml FluentMessageDefinitions;
|
||||
public MiniYaml SequenceDefinitions;
|
||||
public MiniYaml ModelSequenceDefinitions;
|
||||
public MiniYaml WeaponDefinitions;
|
||||
public MiniYaml VoiceDefinitions;
|
||||
public MiniYaml MusicDefinitions;
|
||||
public MiniYaml NotificationDefinitions;
|
||||
|
||||
public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = new();
|
||||
public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = [];
|
||||
|
||||
// Generated data
|
||||
public readonly MapGrid Grid;
|
||||
@@ -274,12 +275,15 @@ namespace OpenRA
|
||||
try
|
||||
{
|
||||
foreach (var filename in contents)
|
||||
if (filename.EndsWith(".yaml") || filename.EndsWith(".bin") || filename.EndsWith(".lua") || (format >= 12 && filename == "map.png"))
|
||||
if (filename.EndsWith(".yaml", StringComparison.Ordinal) ||
|
||||
filename.EndsWith(".bin", StringComparison.Ordinal) ||
|
||||
filename.EndsWith(".lua", StringComparison.Ordinal) ||
|
||||
(format >= 12 && filename == "map.png"))
|
||||
streams.Add(package.GetStream(filename));
|
||||
|
||||
// Take the SHA1
|
||||
if (streams.Count == 0)
|
||||
return CryptoUtil.SHA1Hash(Array.Empty<byte>());
|
||||
return CryptoUtil.SHA1Hash([]);
|
||||
|
||||
var merged = streams[0];
|
||||
for (var i = 1; i < streams.Count; i++)
|
||||
@@ -317,26 +321,25 @@ namespace OpenRA
|
||||
/// Initializes a new map created by the editor or importer.
|
||||
/// The map will not receive a valid UID until after it has been saved and reloaded.
|
||||
/// </summary>
|
||||
public Map(ModData modData, ITerrainInfo terrainInfo, int width, int height)
|
||||
public Map(ModData modData, ITerrainInfo terrainInfo, Size size)
|
||||
{
|
||||
this.modData = modData;
|
||||
var size = new Size(width, height);
|
||||
Grid = modData.Manifest.Get<MapGrid>();
|
||||
MapSize = size;
|
||||
Grid = modData.GetOrCreate<MapGrid>();
|
||||
|
||||
Title = "Name your map here";
|
||||
Author = "Your name here";
|
||||
|
||||
MapSize = new int2(size);
|
||||
Tileset = terrainInfo.Id;
|
||||
|
||||
// Empty rules that can be added to by the importers.
|
||||
// Will be dropped on save if nothing is added to it
|
||||
RuleDefinitions = new MiniYaml("");
|
||||
|
||||
Tiles = new CellLayer<TerrainTile>(Grid.Type, size);
|
||||
Resources = new CellLayer<ResourceTile>(Grid.Type, size);
|
||||
Height = new CellLayer<byte>(Grid.Type, size);
|
||||
Ramp = new CellLayer<byte>(Grid.Type, size);
|
||||
Tiles = new CellLayer<TerrainTile>(Grid.Type, MapSize);
|
||||
Resources = new CellLayer<ResourceTile>(Grid.Type, MapSize);
|
||||
Height = new CellLayer<byte>(Grid.Type, MapSize);
|
||||
Ramp = new CellLayer<byte>(Grid.Type, MapSize);
|
||||
Tiles.Clear(terrainInfo.DefaultTerrainTile);
|
||||
|
||||
if (Grid.MaximumTerrainHeight > 0)
|
||||
@@ -357,23 +360,22 @@ namespace OpenRA
|
||||
if (!Package.Contains("map.yaml") || !Package.Contains("map.bin"))
|
||||
throw new InvalidDataException($"Not a valid map\n File: {package.Name}");
|
||||
|
||||
var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), package.Name));
|
||||
var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), $"{package.Name}:map.yaml"));
|
||||
foreach (var field in YamlFields)
|
||||
field.Deserialize(this, yaml.Nodes);
|
||||
field.Deserialize(this, yaml);
|
||||
|
||||
if (MapFormat < SupportedMapFormat)
|
||||
throw new InvalidDataException($"Map format {MapFormat} is not supported.\n File: {package.Name}");
|
||||
|
||||
PlayerDefinitions = MiniYaml.NodesOrEmpty(yaml, "Players");
|
||||
ActorDefinitions = MiniYaml.NodesOrEmpty(yaml, "Actors");
|
||||
PlayerDefinitions = yaml.NodeWithKeyOrDefault("Players")?.Value.Nodes ?? [];
|
||||
ActorDefinitions = yaml.NodeWithKeyOrDefault("Actors")?.Value.Nodes ?? [];
|
||||
|
||||
Grid = modData.Manifest.Get<MapGrid>();
|
||||
Grid = modData.GetOrCreate<MapGrid>();
|
||||
|
||||
var size = new Size(MapSize.X, MapSize.Y);
|
||||
Tiles = new CellLayer<TerrainTile>(Grid.Type, size);
|
||||
Resources = new CellLayer<ResourceTile>(Grid.Type, size);
|
||||
Height = new CellLayer<byte>(Grid.Type, size);
|
||||
Ramp = new CellLayer<byte>(Grid.Type, size);
|
||||
Tiles = new CellLayer<TerrainTile>(Grid.Type, MapSize);
|
||||
Resources = new CellLayer<ResourceTile>(Grid.Type, MapSize);
|
||||
Height = new CellLayer<byte>(Grid.Type, MapSize);
|
||||
Ramp = new CellLayer<byte>(Grid.Type, MapSize);
|
||||
|
||||
using (var s = Package.GetStream("map.bin"))
|
||||
{
|
||||
@@ -381,9 +383,9 @@ namespace OpenRA
|
||||
if (header.TilesOffset > 0)
|
||||
{
|
||||
s.Position = header.TilesOffset;
|
||||
for (var i = 0; i < MapSize.X; i++)
|
||||
for (var i = 0; i < MapSize.Width; i++)
|
||||
{
|
||||
for (var j = 0; j < MapSize.Y; j++)
|
||||
for (var j = 0; j < MapSize.Height; j++)
|
||||
{
|
||||
var tile = s.ReadUInt16();
|
||||
var index = s.ReadUInt8();
|
||||
@@ -400,9 +402,9 @@ namespace OpenRA
|
||||
if (header.ResourcesOffset > 0)
|
||||
{
|
||||
s.Position = header.ResourcesOffset;
|
||||
for (var i = 0; i < MapSize.X; i++)
|
||||
for (var i = 0; i < MapSize.Width; i++)
|
||||
{
|
||||
for (var j = 0; j < MapSize.Y; j++)
|
||||
for (var j = 0; j < MapSize.Height; j++)
|
||||
{
|
||||
var type = s.ReadUInt8();
|
||||
var density = s.ReadUInt8();
|
||||
@@ -414,8 +416,8 @@ namespace OpenRA
|
||||
if (header.HeightsOffset > 0)
|
||||
{
|
||||
s.Position = header.HeightsOffset;
|
||||
for (var i = 0; i < MapSize.X; i++)
|
||||
for (var j = 0; j < MapSize.Y; j++)
|
||||
for (var i = 0; i < MapSize.Width; i++)
|
||||
for (var j = 0; j < MapSize.Height; j++)
|
||||
Height[new MPos(i, j)] = s.ReadUInt8().Clamp((byte)0, Grid.MaximumTerrainHeight);
|
||||
}
|
||||
}
|
||||
@@ -451,7 +453,7 @@ namespace OpenRA
|
||||
Sequences = new SequenceSet(this, modData, Tileset, SequenceDefinitions);
|
||||
|
||||
var tl = new MPos(0, 0).ToCPos(this);
|
||||
var br = new MPos(MapSize.X - 1, MapSize.Y - 1).ToCPos(this);
|
||||
var br = new MPos(MapSize.Width - 1, MapSize.Height - 1).ToCPos(this);
|
||||
AllCells = new CellRegion(Grid.Type, tl, br);
|
||||
|
||||
var btl = new PPos(Bounds.Left, Bounds.Top);
|
||||
@@ -512,7 +514,7 @@ namespace OpenRA
|
||||
foreach (var cell in AllCells)
|
||||
{
|
||||
var uv = cell.ToMPos(Grid.Type);
|
||||
cellProjection[uv] = Array.Empty<PPos>();
|
||||
cellProjection[uv] = [];
|
||||
inverseCellProjection[uv] = new List<MPos>(1);
|
||||
}
|
||||
|
||||
@@ -528,7 +530,7 @@ namespace OpenRA
|
||||
if (Grid.MaximumTerrainHeight == 0)
|
||||
{
|
||||
uv = cell.ToMPos(Grid.Type);
|
||||
cellProjection[cell] = new[] { (PPos)uv };
|
||||
cellProjection[cell] = [(PPos)uv];
|
||||
var inverse = inverseCellProjection[uv];
|
||||
inverse.Clear();
|
||||
inverse.Add(uv);
|
||||
@@ -603,11 +605,11 @@ namespace OpenRA
|
||||
// Any changes to this function should be reflected when setting projectionSafeBounds.
|
||||
var height = mapHeight[uv];
|
||||
if (height == 0)
|
||||
return new[] { (PPos)uv };
|
||||
return [(PPos)uv];
|
||||
|
||||
// Odd-height ramps get bumped up a level to the next even height layer
|
||||
if ((height & 1) == 1 && Ramp[uv] != 0)
|
||||
height += 1;
|
||||
height++;
|
||||
|
||||
var candidates = new List<PPos>();
|
||||
|
||||
@@ -646,21 +648,24 @@ namespace OpenRA
|
||||
foreach (var file in Package.Contents)
|
||||
toPackage.Update(file, Package.GetStream(file).ReadAllBytes());
|
||||
|
||||
if (!LockPreview)
|
||||
void UpdatePackage(string filename, byte[] data)
|
||||
{
|
||||
var previewData = SavePreview();
|
||||
if (Package != toPackage || !Enumerable.SequenceEqual(previewData, Package.GetStream("map.png").ReadAllBytes()))
|
||||
toPackage.Update("map.png", previewData);
|
||||
if (Package != toPackage)
|
||||
toPackage.Update(filename, data);
|
||||
else
|
||||
{
|
||||
var stream = Package.GetStream(filename);
|
||||
if (stream == null || !Enumerable.SequenceEqual(data, stream.ReadAllBytes()))
|
||||
toPackage.Update(filename, data);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the package with the new map data
|
||||
var textData = Encoding.UTF8.GetBytes(root.WriteToString());
|
||||
if (Package != toPackage || !Enumerable.SequenceEqual(textData, Package.GetStream("map.yaml").ReadAllBytes()))
|
||||
toPackage.Update("map.yaml", textData);
|
||||
if (!LockPreview)
|
||||
UpdatePackage("map.png", SavePreview());
|
||||
|
||||
var binaryData = SaveBinaryData();
|
||||
if (Package != toPackage || !Enumerable.SequenceEqual(binaryData, Package.GetStream("map.bin").ReadAllBytes()))
|
||||
toPackage.Update("map.bin", binaryData);
|
||||
// Update the package with the new map data
|
||||
UpdatePackage("map.yaml", Encoding.UTF8.GetBytes(root.WriteToString()));
|
||||
UpdatePackage("map.bin", SaveBinaryData());
|
||||
|
||||
Package = toPackage;
|
||||
|
||||
@@ -677,24 +682,24 @@ namespace OpenRA
|
||||
writer.Write(TileFormat);
|
||||
|
||||
// Size
|
||||
writer.Write((ushort)MapSize.X);
|
||||
writer.Write((ushort)MapSize.Y);
|
||||
writer.Write((ushort)MapSize.Width);
|
||||
writer.Write((ushort)MapSize.Height);
|
||||
|
||||
// Data offsets
|
||||
var tilesOffset = 17;
|
||||
var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.X * MapSize.Y + 17 : 0;
|
||||
var resourcesOffset = (Grid.MaximumTerrainHeight > 0 ? 4 : 3) * MapSize.X * MapSize.Y + 17;
|
||||
const int TilesOffset = 17;
|
||||
var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.Width * MapSize.Height + 17 : 0;
|
||||
var resourcesOffset = (Grid.MaximumTerrainHeight > 0 ? 4 : 3) * MapSize.Width * MapSize.Height + 17;
|
||||
|
||||
writer.Write((uint)tilesOffset);
|
||||
writer.Write((uint)TilesOffset);
|
||||
writer.Write((uint)heightsOffset);
|
||||
writer.Write((uint)resourcesOffset);
|
||||
|
||||
// Tile data
|
||||
if (tilesOffset != 0)
|
||||
if (TilesOffset != 0)
|
||||
{
|
||||
for (var i = 0; i < MapSize.X; i++)
|
||||
for (var i = 0; i < MapSize.Width; i++)
|
||||
{
|
||||
for (var j = 0; j < MapSize.Y; j++)
|
||||
for (var j = 0; j < MapSize.Height; j++)
|
||||
{
|
||||
var tile = Tiles[new MPos(i, j)];
|
||||
writer.Write(tile.Type);
|
||||
@@ -705,16 +710,16 @@ namespace OpenRA
|
||||
|
||||
// Height data
|
||||
if (heightsOffset != 0)
|
||||
for (var i = 0; i < MapSize.X; i++)
|
||||
for (var j = 0; j < MapSize.Y; j++)
|
||||
for (var i = 0; i < MapSize.Width; i++)
|
||||
for (var j = 0; j < MapSize.Height; j++)
|
||||
writer.Write(Height[new MPos(i, j)]);
|
||||
|
||||
// Resource data
|
||||
if (resourcesOffset != 0)
|
||||
{
|
||||
for (var i = 0; i < MapSize.X; i++)
|
||||
for (var i = 0; i < MapSize.Width; i++)
|
||||
{
|
||||
for (var j = 0; j < MapSize.Y; j++)
|
||||
for (var j = 0; j < MapSize.Height; j++)
|
||||
{
|
||||
var tile = Resources[new MPos(i, j)];
|
||||
writer.Write(tile.Type);
|
||||
@@ -751,7 +756,7 @@ namespace OpenRA
|
||||
var positions = new List<(MPos Uv, Color Color)>();
|
||||
foreach (var actor in actors)
|
||||
{
|
||||
var s = new ActorReference(actor.Value.Value, actor.Value.ToDictionary());
|
||||
var s = new ActorReference(actor.Value.Value, actor.Value);
|
||||
|
||||
var ai = Rules.Actors[actor.Value.Value];
|
||||
var impsis = ai.TraitInfos<IMapPreviewSignatureInfo>();
|
||||
@@ -772,19 +777,10 @@ namespace OpenRA
|
||||
|
||||
if (Grid.MaximumTerrainHeight > 0)
|
||||
{
|
||||
// The minimap is drawn in cell space, so we need to
|
||||
// unproject the PPos bounds to find the MPos boundaries.
|
||||
// This matches the calculation in RadarWidget that is used ingame
|
||||
for (var x = Bounds.Left; x < Bounds.Right; x++)
|
||||
{
|
||||
var allTop = Unproject(new PPos(x, Bounds.Top));
|
||||
var allBottom = Unproject(new PPos(x, Bounds.Bottom));
|
||||
if (allTop.Count > 0)
|
||||
top = Math.Min(top, allTop.MinBy(uv => uv.V).V);
|
||||
(top, bottom) = GetCellSpaceBounds();
|
||||
|
||||
if (allBottom.Count > 0)
|
||||
bottom = Math.Max(bottom, allBottom.MaxBy(uv => uv.V).V);
|
||||
}
|
||||
if (top == int.MaxValue || bottom == int.MinValue)
|
||||
throw new InvalidDataException("The map has invalid boundaries");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -801,7 +797,7 @@ namespace OpenRA
|
||||
bitmapWidth = 2 * bitmapWidth - 1;
|
||||
|
||||
var stride = bitmapWidth * 4;
|
||||
var pxStride = 4;
|
||||
const int PxStride = 4;
|
||||
var minimapData = new byte[stride * height];
|
||||
(Color Left, Color Right) terrainColor = default;
|
||||
|
||||
@@ -823,10 +819,10 @@ namespace OpenRA
|
||||
{
|
||||
// Odd rows are shifted right by 1px
|
||||
var dx = uv.V & 1;
|
||||
var xOffset = pxStride * (2 * x + dx);
|
||||
var xOffset = PxStride * (2 * x + dx);
|
||||
if (x + dx > 0)
|
||||
{
|
||||
var z = y * stride + xOffset - pxStride;
|
||||
var z = y * stride + xOffset - PxStride;
|
||||
var c = actorColor.A == 0 ? terrainColor.Left : actorColor;
|
||||
minimapData[z++] = c.R;
|
||||
minimapData[z++] = c.G;
|
||||
@@ -846,7 +842,7 @@ namespace OpenRA
|
||||
}
|
||||
else
|
||||
{
|
||||
var z = y * stride + pxStride * x;
|
||||
var z = y * stride + PxStride * x;
|
||||
var c = actorColor.A == 0 ? terrainColor.Left : actorColor;
|
||||
minimapData[z++] = c.R;
|
||||
minimapData[z++] = c.G;
|
||||
@@ -860,6 +856,28 @@ namespace OpenRA
|
||||
return png.Save();
|
||||
}
|
||||
|
||||
public (int Top, int Bottom) GetCellSpaceBounds()
|
||||
{
|
||||
var top = int.MaxValue;
|
||||
var bottom = int.MinValue;
|
||||
|
||||
// The minimap is drawn in cell space, so we need to
|
||||
// unproject the PPos bounds to find the MPos boundaries.
|
||||
// This matches the calculation in RadarWidget that is used ingame
|
||||
for (var x = Bounds.Left; x < Bounds.Right; x++)
|
||||
{
|
||||
var allTop = Unproject(new PPos(x, Bounds.Top));
|
||||
var allBottom = Unproject(new PPos(x, Bounds.Bottom));
|
||||
if (allTop.Count > 0)
|
||||
top = Math.Min(top, allTop.MinBy(uv => uv.V).V);
|
||||
|
||||
if (allBottom.Count > 0)
|
||||
bottom = Math.Max(bottom, allBottom.MaxBy(uv => uv.V).V);
|
||||
}
|
||||
|
||||
return (top, bottom);
|
||||
}
|
||||
|
||||
public bool Contains(CPos cell)
|
||||
{
|
||||
if (Grid.Type == MapGridType.RectangularIsometric)
|
||||
@@ -1024,7 +1042,7 @@ namespace OpenRA
|
||||
return (PPos)CellContaining(projectedPos).ToMPos(Grid.Type);
|
||||
}
|
||||
|
||||
static readonly PPos[] NoProjectedCells = Array.Empty<PPos>();
|
||||
static readonly PPos[] NoProjectedCells = [];
|
||||
public PPos[] ProjectedCellsCovering(MPos uv)
|
||||
{
|
||||
if (!initializedCellProjection)
|
||||
@@ -1044,7 +1062,7 @@ namespace OpenRA
|
||||
InitializeCellProjection();
|
||||
|
||||
if (!inverseCellProjection.Contains(uv))
|
||||
return new List<MPos>();
|
||||
return [];
|
||||
|
||||
return inverseCellProjection[uv];
|
||||
}
|
||||
@@ -1069,16 +1087,15 @@ namespace OpenRA
|
||||
var oldMapResources = Resources;
|
||||
var oldMapHeight = Height;
|
||||
var oldMapRamp = Ramp;
|
||||
var newSize = new Size(width, height);
|
||||
|
||||
Tiles = CellLayer.Resize(oldMapTiles, newSize, oldMapTiles[MPos.Zero]);
|
||||
Resources = CellLayer.Resize(oldMapResources, newSize, oldMapResources[MPos.Zero]);
|
||||
Height = CellLayer.Resize(oldMapHeight, newSize, oldMapHeight[MPos.Zero]);
|
||||
Ramp = CellLayer.Resize(oldMapRamp, newSize, oldMapRamp[MPos.Zero]);
|
||||
MapSize = new int2(newSize);
|
||||
MapSize = new Size(width, height);
|
||||
Tiles = CellLayer.Resize(oldMapTiles, MapSize, oldMapTiles[MPos.Zero]);
|
||||
Resources = CellLayer.Resize(oldMapResources, MapSize, oldMapResources[MPos.Zero]);
|
||||
Height = CellLayer.Resize(oldMapHeight, MapSize, oldMapHeight[MPos.Zero]);
|
||||
Ramp = CellLayer.Resize(oldMapRamp, MapSize, oldMapRamp[MPos.Zero]);
|
||||
|
||||
var tl = new MPos(0, 0);
|
||||
var br = new MPos(MapSize.X - 1, MapSize.Y - 1);
|
||||
var br = new MPos(MapSize.Width - 1, MapSize.Height - 1);
|
||||
AllCells = new CellRegion(Grid.Type, tl.ToCPos(this), br.ToCPos(this));
|
||||
SetBounds(new PPos(tl.U + 1, tl.V + 1), new PPos(br.U - 1, br.V - 1));
|
||||
}
|
||||
@@ -1125,6 +1142,11 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public byte GetTerrainIndex(CPos cell)
|
||||
{
|
||||
return GetTerrainIndex(cell.ToMPos(this));
|
||||
}
|
||||
|
||||
public byte GetTerrainIndex(MPos uv)
|
||||
{
|
||||
// Lazily initialize a cache for terrain indexes.
|
||||
if (cachedTerrainIndexes == null)
|
||||
@@ -1133,7 +1155,6 @@ namespace OpenRA
|
||||
cachedTerrainIndexes.Clear(InvalidCachedTerrainIndex);
|
||||
}
|
||||
|
||||
var uv = cell.ToMPos(this);
|
||||
var terrainIndex = cachedTerrainIndexes[uv];
|
||||
|
||||
// PERF: Cache terrain indexes per cell on demand.
|
||||
@@ -1148,7 +1169,12 @@ namespace OpenRA
|
||||
|
||||
public TerrainTypeInfo GetTerrainInfo(CPos cell)
|
||||
{
|
||||
return Rules.TerrainInfo.TerrainTypes[GetTerrainIndex(cell)];
|
||||
return GetTerrainInfo(cell.ToMPos(this));
|
||||
}
|
||||
|
||||
public TerrainTypeInfo GetTerrainInfo(MPos uv)
|
||||
{
|
||||
return Rules.TerrainInfo.TerrainTypes[GetTerrainIndex(uv)];
|
||||
}
|
||||
|
||||
public CPos Clamp(CPos cell)
|
||||
@@ -1180,7 +1206,7 @@ namespace OpenRA
|
||||
// Project this guessed cell and take the first available cell
|
||||
// If it is projected outside the layer, then make another guess.
|
||||
var allProjected = ProjectedCellsCovering(uv);
|
||||
var projected = allProjected.Length > 0 ? allProjected.First()
|
||||
var projected = allProjected.Length > 0 ? allProjected[0]
|
||||
: new PPos(uv.U, uv.V.Clamp(Bounds.Top, Bounds.Bottom));
|
||||
|
||||
// Clamp the projected cell to the map area
|
||||
@@ -1249,7 +1275,7 @@ namespace OpenRA
|
||||
PPos edge;
|
||||
if (allProjected.Length > 0)
|
||||
{
|
||||
var puv = allProjected.First();
|
||||
var puv = allProjected[0];
|
||||
var horizontalBound = (puv.U - Bounds.Left < Bounds.Width / 2) ? Bounds.Left : Bounds.Right;
|
||||
var verticalBound = (puv.V - Bounds.Top < Bounds.Height / 2) ? Bounds.Top : Bounds.Bottom;
|
||||
|
||||
@@ -1349,13 +1375,18 @@ namespace OpenRA
|
||||
throw new ArgumentOutOfRangeException(nameof(maxRange),
|
||||
$"The requested range ({maxRange}) cannot exceed the value of MaximumTileSearchRange ({Grid.MaximumTileSearchRange})");
|
||||
|
||||
for (var i = minRange; i <= maxRange; i++)
|
||||
return FindTilesInAnnulus();
|
||||
|
||||
IEnumerable<CPos> FindTilesInAnnulus()
|
||||
{
|
||||
foreach (var offset in Grid.TilesByDistance[i])
|
||||
for (var i = minRange; i <= maxRange; i++)
|
||||
{
|
||||
var t = offset + center;
|
||||
if (allowOutsideBounds ? Tiles.Contains(t) : Contains(t))
|
||||
yield return t;
|
||||
foreach (var offset in Grid.TilesByDistance[i])
|
||||
{
|
||||
var t = offset + center;
|
||||
if (allowOutsideBounds ? Tiles.Contains(t) : Contains(t))
|
||||
yield return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1402,11 +1433,11 @@ namespace OpenRA
|
||||
return modData.DefaultFileSystem.Exists(filename);
|
||||
}
|
||||
|
||||
public bool IsExternalModFile(string filename)
|
||||
public bool IsExternalFile(string filename)
|
||||
{
|
||||
// Explicit package paths never refer to a map
|
||||
if (filename.Contains('|'))
|
||||
return modData.DefaultFileSystem.IsExternalModFile(filename);
|
||||
return modData.DefaultFileSystem.IsExternalFile(filename);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ using OpenRA.FileSystem;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Support;
|
||||
using FS = OpenRA.FileSystem.FileSystem;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
@@ -27,26 +28,27 @@ namespace OpenRA
|
||||
{
|
||||
public static readonly MapPreview UnknownMap = new(null, null, MapGridType.Rectangular, null);
|
||||
public IReadOnlyDictionary<IReadOnlyPackage, MapClassification> MapLocations => mapLocations;
|
||||
readonly Dictionary<IReadOnlyPackage, MapClassification> mapLocations = new();
|
||||
readonly Dictionary<IReadOnlyPackage, MapClassification> mapLocations = [];
|
||||
public bool LoadPreviewImages = true;
|
||||
|
||||
readonly Cache<string, MapPreview> previews;
|
||||
readonly ModData modData;
|
||||
readonly Manifest manifest;
|
||||
readonly FS modFiles;
|
||||
Cache<string, MapPreview> previews;
|
||||
readonly SheetBuilder sheetBuilder;
|
||||
Thread previewLoaderThread;
|
||||
bool previewLoaderThreadShutDown = true;
|
||||
readonly object syncRoot = new();
|
||||
readonly Queue<MapPreview> generateMinimap = new();
|
||||
readonly Queue<MapPreview> generateMinimap = [];
|
||||
|
||||
public Dictionary<string, string> StringPool { get; } = new Dictionary<string, string>();
|
||||
public HashSet<string> StringPool { get; } = [];
|
||||
|
||||
readonly List<MapDirectoryTracker> mapDirectoryTrackers = new();
|
||||
readonly List<MapDirectoryTracker> mapDirectoryTrackers = [];
|
||||
|
||||
/// <summary>
|
||||
/// The most recently modified or loaded map at runtime.
|
||||
/// </summary>
|
||||
public string LastModifiedMap { get; private set; } = null;
|
||||
readonly Dictionary<string, string> mapUpdates = new();
|
||||
readonly Dictionary<string, string> mapUpdates = [];
|
||||
|
||||
string lastLoadedLastModifiedMap;
|
||||
|
||||
@@ -66,13 +68,11 @@ namespace OpenRA
|
||||
return null;
|
||||
}
|
||||
|
||||
public MapCache(ModData modData)
|
||||
public MapCache(Manifest manifest, FS modFiles)
|
||||
{
|
||||
this.modData = modData;
|
||||
|
||||
var gridType = Exts.Lazy(() => modData.Manifest.Get<MapGrid>().Type);
|
||||
previews = new Cache<string, MapPreview>(uid => new MapPreview(modData, uid, gridType.Value, this));
|
||||
sheetBuilder = new SheetBuilder(SheetType.BGRA);
|
||||
this.manifest = manifest;
|
||||
this.modFiles = modFiles;
|
||||
sheetBuilder = new SheetBuilder(SheetType.BGRA, manifest.RendererConstants.MapPreviewSheetSize);
|
||||
}
|
||||
|
||||
public void UpdateMaps()
|
||||
@@ -81,23 +81,24 @@ namespace OpenRA
|
||||
tracker.UpdateMaps(this);
|
||||
}
|
||||
|
||||
public void LoadMaps()
|
||||
public void LoadMaps(ModData modData)
|
||||
{
|
||||
// Utility mod that does not support maps
|
||||
if (!modData.Manifest.Contains<MapGrid>())
|
||||
if (manifest.MapFolders.Count == 0)
|
||||
return;
|
||||
|
||||
var mapGrid = modData.Manifest.Get<MapGrid>();
|
||||
var gridType = modData.GetOrCreate<MapGrid>().Type;
|
||||
previews = new Cache<string, MapPreview>(uid => new MapPreview(modData, uid, gridType, this));
|
||||
|
||||
// Enumerate map directories
|
||||
foreach (var kv in modData.Manifest.MapFolders)
|
||||
foreach (var kv in manifest.MapFolders)
|
||||
{
|
||||
var name = kv.Key;
|
||||
var classification = string.IsNullOrEmpty(kv.Value)
|
||||
? MapClassification.Unknown : Enum<MapClassification>.Parse(kv.Value);
|
||||
? MapClassification.Unknown : Enum.Parse<MapClassification>(kv.Value);
|
||||
|
||||
IReadOnlyPackage package;
|
||||
var optional = name.StartsWith("~", StringComparison.Ordinal);
|
||||
var optional = name.StartsWith('~');
|
||||
if (optional)
|
||||
name = name[1..];
|
||||
|
||||
@@ -106,10 +107,10 @@ namespace OpenRA
|
||||
// HACK: If the path is inside the support directory then we may need to create it
|
||||
// Assume that the path is a directory if there is not an existing file with the same name
|
||||
var resolved = Platform.ResolvePath(name);
|
||||
if (resolved.StartsWith(Platform.SupportDir) && !File.Exists(resolved))
|
||||
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && !File.Exists(resolved))
|
||||
Directory.CreateDirectory(resolved);
|
||||
|
||||
package = modData.ModFiles.OpenPackage(name);
|
||||
package = modFiles.OpenPackage(name);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -120,38 +121,38 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
mapLocations.Add(package, classification);
|
||||
mapDirectoryTrackers.Add(new MapDirectoryTracker(mapGrid, package, classification));
|
||||
mapDirectoryTrackers.Add(new MapDirectoryTracker(package, classification));
|
||||
}
|
||||
|
||||
// PERF: Load the mod YAML once outside the loop, and reuse it when resolving each maps custom YAML.
|
||||
var modDataRules = modData.GetRulesYaml();
|
||||
foreach (var kv in MapLocations)
|
||||
{
|
||||
foreach (var map in kv.Key.Contents)
|
||||
LoadMapInternal(map, kv.Key, kv.Value, mapGrid, null, modDataRules);
|
||||
}
|
||||
LoadMapInternal(map, kv.Key, kv.Value, null, gridType, modDataRules);
|
||||
|
||||
// We only want to track maps in runtime, not at loadtime
|
||||
LastModifiedMap = null;
|
||||
}
|
||||
|
||||
public void LoadMap(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap)
|
||||
public void LoadMap(string map, IReadOnlyPackage package, MapClassification classification, string oldMap)
|
||||
{
|
||||
LoadMapInternal(map, package, classification, mapGrid, oldMap, null);
|
||||
LoadMapInternal(map, package, classification, oldMap);
|
||||
}
|
||||
|
||||
void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap, IEnumerable<List<MiniYamlNode>> modDataRules)
|
||||
void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, string oldMap,
|
||||
MapGridType? gridType = null, MiniYamlNode[][] modDataRules = null)
|
||||
{
|
||||
IReadOnlyPackage mapPackage = null;
|
||||
try
|
||||
{
|
||||
using (new PerfTimer(map))
|
||||
{
|
||||
mapPackage = package.OpenPackage(map, modData.ModFiles);
|
||||
mapPackage = package.OpenPackage(map, modFiles);
|
||||
if (mapPackage != null)
|
||||
{
|
||||
var uid = Map.ComputeUID(mapPackage);
|
||||
previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type, modDataRules);
|
||||
previews[uid].UpdateFromMapWithoutOwningPackage(mapPackage, package, classification, gridType, modDataRules);
|
||||
mapPackage.Dispose();
|
||||
|
||||
if (oldMap != uid)
|
||||
{
|
||||
@@ -176,12 +177,8 @@ namespace OpenRA
|
||||
|
||||
public IEnumerable<IReadWritePackage> EnumerateMapDirPackages(MapClassification classification = MapClassification.System)
|
||||
{
|
||||
// Utility mod that does not support maps
|
||||
if (!modData.Manifest.Contains<MapGrid>())
|
||||
yield break;
|
||||
|
||||
// Enumerate map directories
|
||||
foreach (var kv in modData.Manifest.MapFolders)
|
||||
foreach (var kv in manifest.MapFolders)
|
||||
{
|
||||
if (!Enum.TryParse(kv.Value, out MapClassification packageClassification))
|
||||
continue;
|
||||
@@ -190,16 +187,16 @@ namespace OpenRA
|
||||
continue;
|
||||
|
||||
var name = kv.Key;
|
||||
var optional = name.StartsWith("~", StringComparison.Ordinal);
|
||||
var optional = name.StartsWith('~');
|
||||
if (optional)
|
||||
name = name[1..];
|
||||
|
||||
// Don't try to open the map directory in the support directory if it doesn't exist
|
||||
var resolved = Platform.ResolvePath(name);
|
||||
if (resolved.StartsWith(Platform.SupportDir) && (!Directory.Exists(resolved) || !File.Exists(resolved)))
|
||||
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && (!Directory.Exists(resolved) || !File.Exists(resolved)))
|
||||
continue;
|
||||
|
||||
using (var package = (IReadWritePackage)modData.ModFiles.OpenPackage(name))
|
||||
using (var package = (IReadWritePackage)modFiles.OpenPackage(name))
|
||||
yield return package;
|
||||
}
|
||||
}
|
||||
@@ -219,11 +216,12 @@ namespace OpenRA
|
||||
|
||||
foreach (var mapDirPackage in mapDirPackages)
|
||||
foreach (var map in mapDirPackage.Contents)
|
||||
if (mapDirPackage.OpenPackage(map, modData.ModFiles) is IReadWritePackage mapPackage)
|
||||
if (mapDirPackage.OpenPackage(map, modFiles) is IReadWritePackage mapPackage)
|
||||
yield return mapPackage;
|
||||
}
|
||||
|
||||
public void QueryRemoteMapDetails(string repositoryUrl, IEnumerable<string> uids, Action<MapPreview> mapDetailsReceived = null, Action<MapPreview> mapQueryFailed = null)
|
||||
public void QueryRemoteMapDetails(string repositoryUrl, IEnumerable<string> uids,
|
||||
Action<MapPreview> mapDetailsReceived = null, Action<MapPreview> mapQueryFailed = null)
|
||||
{
|
||||
var queryUids = uids.Distinct()
|
||||
.Where(uid => uid != null)
|
||||
@@ -233,44 +231,37 @@ namespace OpenRA
|
||||
.ToList();
|
||||
|
||||
foreach (var uid in queryUids)
|
||||
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null);
|
||||
previews[uid].BeginRemoteSearch();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var client = HttpClientFactory.Create();
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
|
||||
// Limit each query to 50 maps at a time to avoid request size limits
|
||||
for (var i = 0; i < queryUids.Count; i += 50)
|
||||
foreach (var batchUids in queryUids.Chunk(50))
|
||||
{
|
||||
var batchUids = queryUids.Skip(i).Take(50).ToList();
|
||||
var url = repositoryUrl + "hash/" + string.Join(",", batchUids) + "/yaml";
|
||||
try
|
||||
using (new PerfTimer("RemoteMapDetails"))
|
||||
{
|
||||
var httpResponseMessage = await client.GetAsync(url);
|
||||
var result = await httpResponseMessage.Content.ReadAsStreamAsync();
|
||||
|
||||
var yaml = MiniYaml.FromStream(result);
|
||||
foreach (var kv in yaml)
|
||||
previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, mapDetailsReceived);
|
||||
|
||||
foreach (var uid in batchUids)
|
||||
try
|
||||
{
|
||||
var p = previews[uid];
|
||||
if (p.Status != MapStatus.DownloadAvailable)
|
||||
p.UpdateRemoteSearch(MapStatus.Unavailable, null);
|
||||
var result = await client.GetStreamAsync(url);
|
||||
foreach (var kv in MiniYaml.FromStream(result, url, stringPool: stringPool))
|
||||
previews[kv.Key].CompleteRemoteSearch(kv.Value, mapDetailsReceived);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Write("debug", "Remote map query failed with error:");
|
||||
Log.Write("debug", e);
|
||||
Log.Write("debug", $"URL was: {url}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Write("debug", "Remote map query failed with error:");
|
||||
Log.Write("debug", e);
|
||||
Log.Write("debug", $"URL was: {url}");
|
||||
|
||||
foreach (var uid in batchUids)
|
||||
{
|
||||
var p = previews[uid];
|
||||
p.UpdateRemoteSearch(MapStatus.Unavailable, null);
|
||||
mapQueryFailed?.Invoke(p);
|
||||
if (p.Status == MapStatus.Searching)
|
||||
p.CompleteRemoteSearch(null, mapQueryFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,11 +273,11 @@ namespace OpenRA
|
||||
Log.Write("debug", "MapCache.LoadAsyncInternal started");
|
||||
|
||||
// Milliseconds to wait on one loop when nothing to do
|
||||
var emptyDelay = 50;
|
||||
const int EmptyDelay = 50;
|
||||
|
||||
// Keep the thread alive for at least 5 seconds after the last minimap generation
|
||||
var maxKeepAlive = 5000 / emptyDelay;
|
||||
var keepAlive = maxKeepAlive;
|
||||
const int MaxKeepAlive = 5000 / EmptyDelay;
|
||||
var keepAlive = MaxKeepAlive;
|
||||
|
||||
while (true)
|
||||
{
|
||||
@@ -306,11 +297,11 @@ namespace OpenRA
|
||||
|
||||
if (todo.Count == 0)
|
||||
{
|
||||
Thread.Sleep(emptyDelay);
|
||||
Thread.Sleep(EmptyDelay);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
keepAlive = maxKeepAlive;
|
||||
keepAlive = MaxKeepAlive;
|
||||
|
||||
// Render the minimap into the shared sheet
|
||||
foreach (var p in todo)
|
||||
@@ -337,7 +328,9 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
// Release the buffer by forcing changes to be written out to the texture, allowing the buffer to be reclaimed by GC.
|
||||
Game.RunAfterTick(sheetBuilder.Current.ReleaseBuffer);
|
||||
if (sheetBuilder.Current != null)
|
||||
Game.RunAfterTick(sheetBuilder.Current.ReleaseBuffer);
|
||||
|
||||
Log.Write("debug", "MapCache.LoadAsyncInternal ended");
|
||||
}
|
||||
|
||||
@@ -348,8 +341,8 @@ namespace OpenRA
|
||||
|
||||
while (this[uid].Status != MapStatus.Available)
|
||||
{
|
||||
if (mapUpdates.ContainsKey(uid))
|
||||
uid = mapUpdates[uid];
|
||||
if (mapUpdates.TryGetValue(uid, out var newUid))
|
||||
uid = newUid;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
@@ -406,10 +399,16 @@ namespace OpenRA
|
||||
{
|
||||
UpdateMaps();
|
||||
var map = string.IsNullOrEmpty(initialUid) ? null : previews[initialUid];
|
||||
if (map == null || map.Status != MapStatus.Available || !map.Visibility.HasFlag(MapVisibility.Lobby) || (map.Class != MapClassification.System && map.Class != MapClassification.User))
|
||||
if (map == null ||
|
||||
map.Status != MapStatus.Available ||
|
||||
!map.Visibility.HasFlag(MapVisibility.Lobby) ||
|
||||
(map.Class != MapClassification.System && map.Class != MapClassification.User))
|
||||
{
|
||||
var selected = previews.Values.Where(IsSuitableInitialMap).RandomOrDefault(random) ??
|
||||
previews.Values.FirstOrDefault(m => m.Status == MapStatus.Available && m.Visibility.HasFlag(MapVisibility.Lobby) && (m.Class == MapClassification.System || m.Class == MapClassification.User));
|
||||
previews.Values.FirstOrDefault(m =>
|
||||
m.Status == MapStatus.Available &&
|
||||
m.Visibility.HasFlag(MapVisibility.Lobby) &&
|
||||
(m.Class == MapClassification.System || m.Class == MapClassification.User));
|
||||
return selected == null ? string.Empty : selected.Uid;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace OpenRA
|
||||
// Check for column overflow
|
||||
if (u > r.BottomRight.U)
|
||||
{
|
||||
v += 1;
|
||||
v++;
|
||||
u = r.TopLeft.U;
|
||||
|
||||
// Check for row overflow
|
||||
@@ -53,8 +53,8 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public MPos Current { get; private set; }
|
||||
object IEnumerator.Current => Current;
|
||||
public void Dispose() { }
|
||||
readonly object IEnumerator.Current => Current;
|
||||
public readonly void Dispose() { }
|
||||
}
|
||||
|
||||
public MapCoordsRegion(MPos mapTopLeft, MPos mapBottomRight)
|
||||
|
||||
@@ -20,26 +20,24 @@ namespace OpenRA
|
||||
public sealed class MapDirectoryTracker : IDisposable
|
||||
{
|
||||
readonly FileSystemWatcher watcher;
|
||||
readonly MapGrid mapGrid;
|
||||
readonly IReadOnlyPackage package;
|
||||
readonly MapClassification classification;
|
||||
|
||||
enum MapAction { Add, Delete, Update }
|
||||
readonly Dictionary<string, MapAction> mapActionQueue = new();
|
||||
readonly Dictionary<string, MapAction> mapActionQueue = [];
|
||||
|
||||
bool dirty = false;
|
||||
|
||||
public MapDirectoryTracker(MapGrid mapGrid, IReadOnlyPackage package, MapClassification classification)
|
||||
public MapDirectoryTracker(IReadOnlyPackage package, MapClassification classification)
|
||||
{
|
||||
this.mapGrid = mapGrid;
|
||||
this.package = package;
|
||||
this.classification = classification;
|
||||
|
||||
watcher = new FileSystemWatcher(package.Name);
|
||||
watcher.Changed += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Update, e.FullPath);
|
||||
watcher.Created += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Add, e.FullPath);
|
||||
watcher.Deleted += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Delete, e.FullPath);
|
||||
watcher.Renamed += (object sender, RenamedEventArgs e) => AddMapAction(MapAction.Add, e.FullPath, e.OldFullPath);
|
||||
watcher.Changed += (_, e) => AddMapAction(MapAction.Update, e.FullPath);
|
||||
watcher.Created += (_, e) => AddMapAction(MapAction.Add, e.FullPath);
|
||||
watcher.Deleted += (_, e) => AddMapAction(MapAction.Delete, e.FullPath);
|
||||
watcher.Renamed += (_, e) => AddMapAction(MapAction.Add, e.FullPath, e.OldFullPath);
|
||||
|
||||
watcher.IncludeSubdirectories = true;
|
||||
watcher.EnableRaisingEvents = true;
|
||||
@@ -86,7 +84,7 @@ namespace OpenRA
|
||||
dirty = false;
|
||||
foreach (var mapAction in mapActionQueue)
|
||||
{
|
||||
var map = mapcache.FirstOrDefault(x => x.Package?.Name == mapAction.Key && x.Status == MapStatus.Available);
|
||||
var map = mapcache.FirstOrDefault(x => x.Path == mapAction.Key && x.Status == MapStatus.Available);
|
||||
if (map != null)
|
||||
{
|
||||
if (mapAction.Value == MapAction.Delete)
|
||||
@@ -98,7 +96,7 @@ namespace OpenRA
|
||||
{
|
||||
Console.WriteLine(mapAction.Key + " was updated");
|
||||
map.Invalidate();
|
||||
mapcache.LoadMap(mapAction.Key.Replace(package.Name + Path.DirectorySeparatorChar, ""), package, classification, mapGrid, map.Uid);
|
||||
mapcache.LoadMap(mapAction.Key.Replace(package.Name + Path.DirectorySeparatorChar, ""), package, classification, map.Uid);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -106,7 +104,7 @@ namespace OpenRA
|
||||
if (mapAction.Value != MapAction.Delete)
|
||||
{
|
||||
Console.WriteLine(mapAction.Key + " was added");
|
||||
mapcache.LoadMap(mapAction.Key.Replace(package?.Name + Path.DirectorySeparatorChar, ""), package, classification, mapGrid, null);
|
||||
mapcache.LoadMap(mapAction.Key.Replace(package?.Name + Path.DirectorySeparatorChar, ""), package, classification, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
58
OpenRA.Game/Map/MapGenerationArgs.cs
Normal file
58
OpenRA.Game/Map/MapGenerationArgs.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public class MapGenerationArgs
|
||||
{
|
||||
[FieldLoader.Require]
|
||||
public string Uid = null;
|
||||
|
||||
[FieldLoader.Require]
|
||||
public string Generator = null;
|
||||
|
||||
[FieldLoader.Require]
|
||||
public string Tileset = null;
|
||||
|
||||
[FieldLoader.Require]
|
||||
public Size Size = default;
|
||||
|
||||
[FieldLoader.Require]
|
||||
public string Title = null;
|
||||
|
||||
[FieldLoader.Require]
|
||||
public string Author = null;
|
||||
|
||||
[FieldLoader.LoadUsing(nameof(LoadSettings))]
|
||||
public MiniYaml Settings = null;
|
||||
static MiniYaml LoadSettings(MiniYaml yaml)
|
||||
{
|
||||
return yaml.NodeWithKey("Settings").Value;
|
||||
}
|
||||
|
||||
public List<MiniYamlNode> Serialize()
|
||||
{
|
||||
return
|
||||
[
|
||||
new("Uid", Uid),
|
||||
new("Generator", Generator),
|
||||
new("Tileset", Tileset),
|
||||
new("Size", FieldSaver.FormatValue(Size)),
|
||||
new("Settings", Settings),
|
||||
new("Title", Title),
|
||||
new("Author", Author)
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA
|
||||
@@ -25,8 +25,8 @@ namespace OpenRA
|
||||
public readonly struct CellRamp
|
||||
{
|
||||
public readonly int CenterHeightOffset;
|
||||
public readonly WVec[] Corners;
|
||||
public readonly WVec[][] Polygons;
|
||||
public readonly ImmutableArray<WVec> Corners;
|
||||
public readonly ImmutableArray<ImmutableArray<WVec>> Polygons;
|
||||
public readonly WRot Orientation;
|
||||
|
||||
public CellRamp(MapGridType type, WRot orientation,
|
||||
@@ -37,43 +37,43 @@ namespace OpenRA
|
||||
Orientation = orientation;
|
||||
if (type == MapGridType.RectangularIsometric)
|
||||
{
|
||||
Corners = new[]
|
||||
{
|
||||
Corners =
|
||||
[
|
||||
new WVec(0, -724, 724 * (int)tl),
|
||||
new WVec(724, 0, 724 * (int)tr),
|
||||
new WVec(0, 724, 724 * (int)br),
|
||||
new WVec(-724, 0, 724 * (int)bl),
|
||||
};
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
Corners = new[]
|
||||
{
|
||||
Corners =
|
||||
[
|
||||
new WVec(-512, -512, 512 * (int)tl),
|
||||
new WVec(512, -512, 512 * (int)tr),
|
||||
new WVec(512, 512, 512 * (int)br),
|
||||
new WVec(-512, 512, 512 * (int)bl)
|
||||
};
|
||||
];
|
||||
}
|
||||
|
||||
if (split == RampSplit.X)
|
||||
{
|
||||
Polygons = new[]
|
||||
{
|
||||
new[] { Corners[0], Corners[1], Corners[3] },
|
||||
new[] { Corners[1], Corners[2], Corners[3] }
|
||||
};
|
||||
Polygons =
|
||||
[
|
||||
[Corners[0], Corners[1], Corners[3]],
|
||||
[Corners[1], Corners[2], Corners[3]]
|
||||
];
|
||||
}
|
||||
else if (split == RampSplit.Y)
|
||||
{
|
||||
Polygons = new[]
|
||||
{
|
||||
new[] { Corners[0], Corners[1], Corners[2] },
|
||||
new[] { Corners[0], Corners[2], Corners[3] }
|
||||
};
|
||||
Polygons =
|
||||
[
|
||||
[Corners[0], Corners[1], Corners[2]],
|
||||
[Corners[0], Corners[2], Corners[3]]
|
||||
];
|
||||
}
|
||||
else
|
||||
Polygons = new[] { Corners };
|
||||
Polygons = [Corners];
|
||||
|
||||
// Initial value must be assigned before HeightOffset can be called
|
||||
CenterHeightOffset = 0;
|
||||
@@ -84,10 +84,11 @@ namespace OpenRA
|
||||
{
|
||||
// Enumerate over the polygons, assuming that they are triangles
|
||||
// If the ramp is not split we will take the first three vertices of the corners as a valid triangle
|
||||
WVec[] p = null;
|
||||
var u = 0;
|
||||
var v = 0;
|
||||
for (var i = 0; i < Polygons.Length; i++)
|
||||
int u;
|
||||
int v;
|
||||
ImmutableArray<WVec> p;
|
||||
var i = 0;
|
||||
do
|
||||
{
|
||||
p = Polygons[i];
|
||||
u = ((p[1].Y - p[2].Y) * (dX - p[2].X) - (p[1].X - p[2].X) * (dY - p[2].Y)) / 1024;
|
||||
@@ -96,7 +97,10 @@ namespace OpenRA
|
||||
// Point is within the triangle if 0 <= u,v <= 1024
|
||||
if (u >= 0 && u <= 1024 && v >= 0 && v <= 1024)
|
||||
break;
|
||||
|
||||
i++;
|
||||
}
|
||||
while (i < Polygons.Length);
|
||||
|
||||
// Calculate w from u,v and interpolate height
|
||||
return (u * p[0].Z + v * p[1].Z + (1024 - u - v) * p[2].Z) / 1024;
|
||||
@@ -106,7 +110,6 @@ namespace OpenRA
|
||||
public class MapGrid : IGlobalModData
|
||||
{
|
||||
public readonly MapGridType Type = MapGridType.Rectangular;
|
||||
public readonly Size TileSize = new(24, 24);
|
||||
public readonly byte MaximumTerrainHeight = 0;
|
||||
public readonly SubCell DefaultSubCell = (SubCell)byte.MaxValue;
|
||||
|
||||
@@ -114,19 +117,19 @@ namespace OpenRA
|
||||
|
||||
public readonly bool EnableDepthBuffer = false;
|
||||
|
||||
public readonly WVec[] SubCellOffsets =
|
||||
{
|
||||
new WVec(0, 0, 0), // full cell - index 0
|
||||
new WVec(-299, -256, 0), // top left - index 1
|
||||
new WVec(256, -256, 0), // top right - index 2
|
||||
new WVec(0, 0, 0), // center - index 3
|
||||
new WVec(-299, 256, 0), // bottom left - index 4
|
||||
new WVec(256, 256, 0), // bottom right - index 5
|
||||
};
|
||||
public readonly ImmutableArray<WVec> SubCellOffsets =
|
||||
[
|
||||
new(0, 0, 0), // full cell - index 0
|
||||
new(-299, -256, 0), // top left - index 1
|
||||
new(256, -256, 0), // top right - index 2
|
||||
new(0, 0, 0), // center - index 3
|
||||
new(-299, 256, 0), // bottom left - index 4
|
||||
new(256, 256, 0), // bottom right - index 5
|
||||
];
|
||||
|
||||
public CellRamp[] Ramps { get; }
|
||||
public ImmutableArray<CellRamp> Ramps { get; }
|
||||
|
||||
internal readonly CVec[][] TilesByDistance;
|
||||
internal readonly ImmutableArray<ImmutableArray<CVec>> TilesByDistance;
|
||||
|
||||
public int TileScale { get; }
|
||||
|
||||
@@ -159,8 +162,9 @@ namespace OpenRA
|
||||
var halfBackward = -halfForward;
|
||||
|
||||
// Slope types are hardcoded following the convention from the TS and RA2 map format
|
||||
Ramps = new[]
|
||||
{
|
||||
Ramps =
|
||||
[
|
||||
|
||||
// Flat
|
||||
new CellRamp(Type, WRot.None),
|
||||
|
||||
@@ -193,16 +197,16 @@ namespace OpenRA
|
||||
new CellRamp(Type, WRot.None, tl: RampCornerHeight.Half, br: RampCornerHeight.Half, split: RampSplit.Y),
|
||||
new CellRamp(Type, WRot.None, tr: RampCornerHeight.Half, bl: RampCornerHeight.Half, split: RampSplit.X),
|
||||
new CellRamp(Type, WRot.None, tl: RampCornerHeight.Half, br: RampCornerHeight.Half, split: RampSplit.X),
|
||||
};
|
||||
];
|
||||
|
||||
TilesByDistance = CreateTilesByDistance();
|
||||
}
|
||||
|
||||
CVec[][] CreateTilesByDistance()
|
||||
ImmutableArray<ImmutableArray<CVec>> CreateTilesByDistance()
|
||||
{
|
||||
var ts = new List<CVec>[MaximumTileSearchRange + 1];
|
||||
for (var i = 0; i < MaximumTileSearchRange + 1; i++)
|
||||
ts[i] = new List<CVec>();
|
||||
ts[i] = [];
|
||||
|
||||
for (var j = -MaximumTileSearchRange; j <= MaximumTileSearchRange; j++)
|
||||
for (var i = -MaximumTileSearchRange; i <= MaximumTileSearchRange; i++)
|
||||
@@ -234,7 +238,7 @@ namespace OpenRA
|
||||
});
|
||||
}
|
||||
|
||||
return ts.Select(list => list.ToArray()).ToArray();
|
||||
return ts.Select(list => list.ToImmutableArray()).ToImmutableArray();
|
||||
}
|
||||
|
||||
public WVec OffsetOfSubCell(SubCell subCell)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using OpenRA.Traits;
|
||||
|
||||
@@ -24,7 +25,7 @@ namespace OpenRA
|
||||
public readonly Dictionary<string, PlayerReference> Players;
|
||||
|
||||
public MapPlayers()
|
||||
: this(new List<MiniYamlNode>()) { }
|
||||
: this([]) { }
|
||||
|
||||
public MapPlayers(IEnumerable<MiniYamlNode> playerDefinitions)
|
||||
{
|
||||
@@ -54,7 +55,7 @@ namespace OpenRA
|
||||
Name = "Creeps",
|
||||
Faction = firstFaction,
|
||||
NonCombatant = true,
|
||||
Enemies = Exts.MakeArray(playerCount, i => $"Multi{i}")
|
||||
Enemies = Exts.MakeArray(playerCount, i => $"Multi{i}").ToImmutableArray()
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -66,7 +67,7 @@ namespace OpenRA
|
||||
Name = $"Multi{index}",
|
||||
Faction = "Random",
|
||||
Playable = true,
|
||||
Enemies = new[] { "Creeps" }
|
||||
Enemies = ["Creeps"]
|
||||
};
|
||||
Players.Add(p.Name, p);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -23,10 +24,11 @@ using OpenRA.FileSystem;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Support;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError }
|
||||
public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError, Generatable, Generating }
|
||||
|
||||
// Used for grouping maps in the UI
|
||||
[Flags]
|
||||
@@ -35,7 +37,8 @@ namespace OpenRA
|
||||
Unknown = 0,
|
||||
System = 1,
|
||||
User = 2,
|
||||
Remote = 4
|
||||
Remote = 4,
|
||||
Generated = 8
|
||||
}
|
||||
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules",
|
||||
@@ -48,10 +51,10 @@ namespace OpenRA
|
||||
{
|
||||
public readonly string title;
|
||||
public readonly string author;
|
||||
public readonly string[] categories;
|
||||
public readonly ImmutableArray<string> categories;
|
||||
public readonly int players;
|
||||
public readonly Rectangle bounds;
|
||||
public readonly short[] spawnpoints = Array.Empty<short>();
|
||||
public readonly ImmutableArray<short> spawnpoints = [];
|
||||
public readonly MapGridType map_grid_type;
|
||||
public readonly string minimap;
|
||||
public readonly bool downloading;
|
||||
@@ -59,6 +62,7 @@ namespace OpenRA
|
||||
public readonly string rules;
|
||||
public readonly string players_block;
|
||||
public readonly int mapformat;
|
||||
public readonly string game_mod;
|
||||
}
|
||||
|
||||
public sealed class MapPreview : IDisposable, IReadOnlyFileSystem
|
||||
@@ -68,12 +72,12 @@ namespace OpenRA
|
||||
{
|
||||
public int MapFormat;
|
||||
public string Title;
|
||||
public string[] Categories;
|
||||
public ImmutableArray<string> Categories;
|
||||
public string Author;
|
||||
public string TileSet;
|
||||
public MapPlayers Players;
|
||||
public int PlayerCount;
|
||||
public CPos[] SpawnPoints;
|
||||
public ImmutableArray<CPos> SpawnPoints;
|
||||
public MapGridType GridType;
|
||||
public Rectangle Bounds;
|
||||
public Png Preview;
|
||||
@@ -81,6 +85,7 @@ namespace OpenRA
|
||||
public MapClassification Class;
|
||||
public MapVisibility Visibility;
|
||||
public DateTime ModifiedDate;
|
||||
public MapGenerationArgs GenerationArgs;
|
||||
|
||||
public MiniYaml RuleDefinitions;
|
||||
public MiniYaml WeaponDefinitions;
|
||||
@@ -89,8 +94,9 @@ namespace OpenRA
|
||||
public MiniYaml NotificationDefinitions;
|
||||
public MiniYaml SequenceDefinitions;
|
||||
public MiniYaml ModelSequenceDefinitions;
|
||||
public MiniYaml FluentMessageDefinitions;
|
||||
|
||||
public Translation Translation { get; private set; }
|
||||
public FluentBundle FluentBundle { get; private set; }
|
||||
public ActorInfo WorldActorInfo { get; private set; }
|
||||
public ActorInfo PlayerActorInfo { get; private set; }
|
||||
|
||||
@@ -111,7 +117,7 @@ namespace OpenRA
|
||||
return key == "world" || key == "player";
|
||||
}
|
||||
|
||||
public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary<string, MiniYaml> yaml, IEnumerable<List<MiniYamlNode>> modDataRules)
|
||||
public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary<string, MiniYaml> yaml, MiniYamlNode[][] modDataRules)
|
||||
{
|
||||
RuleDefinitions = LoadRuleSection(yaml, "Rules");
|
||||
WeaponDefinitions = LoadRuleSection(yaml, "Weapons");
|
||||
@@ -120,13 +126,32 @@ namespace OpenRA
|
||||
NotificationDefinitions = LoadRuleSection(yaml, "Notifications");
|
||||
SequenceDefinitions = LoadRuleSection(yaml, "Sequences");
|
||||
ModelSequenceDefinitions = LoadRuleSection(yaml, "ModelSequences");
|
||||
|
||||
Translation = yaml.TryGetValue("Translations", out var node) && node != null
|
||||
? new Translation(Game.Settings.Player.Language, FieldLoader.GetValue<string[]>("value", node.Value), fileSystem)
|
||||
: null;
|
||||
FluentMessageDefinitions = LoadRuleSection(yaml, "FluentMessages");
|
||||
|
||||
try
|
||||
{
|
||||
if (FluentMessageDefinitions != null)
|
||||
{
|
||||
var files = ImmutableArray<string>.Empty;
|
||||
if (FluentMessageDefinitions.Value != null)
|
||||
files = FieldLoader.GetValue<ImmutableArray<string>>("value", FluentMessageDefinitions.Value);
|
||||
|
||||
string text = null;
|
||||
if (FluentMessageDefinitions.Nodes.Length > 0)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
foreach (var node in FluentMessageDefinitions.Nodes)
|
||||
if (node.Key == "base64")
|
||||
builder.Append(Encoding.UTF8.GetString(Convert.FromBase64String(node.Value.Value)));
|
||||
|
||||
text = builder.ToString();
|
||||
}
|
||||
|
||||
FluentBundle = new FluentBundle(modData.Manifest.FluentCulture, files, fileSystem, text);
|
||||
}
|
||||
else
|
||||
FluentBundle = null;
|
||||
|
||||
// PERF: Implement a minimal custom loader for custom world and player actors to minimize loading time
|
||||
// This assumes/enforces that these actor types can only inherit abstract definitions (starting with ^)
|
||||
if (RuleDefinitions != null)
|
||||
@@ -135,19 +160,26 @@ namespace OpenRA
|
||||
var files = Enumerable.Empty<string>();
|
||||
if (RuleDefinitions.Value != null)
|
||||
{
|
||||
var mapFiles = FieldLoader.GetValue<string[]>("value", RuleDefinitions.Value);
|
||||
files = files.Append(mapFiles);
|
||||
var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", RuleDefinitions.Value);
|
||||
files = files.Concat(mapFiles);
|
||||
}
|
||||
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
var sources =
|
||||
modDataRules.Select(x => x.Where(IsLoadableRuleDefinition).ToList())
|
||||
.Concat(files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s).Where(IsLoadableRuleDefinition).ToList()));
|
||||
if (RuleDefinitions.Nodes.Count > 0)
|
||||
.Concat(files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool).Where(IsLoadableRuleDefinition).ToList()));
|
||||
if (RuleDefinitions.Nodes.Length > 0)
|
||||
sources = sources.Append(RuleDefinitions.Nodes.Where(IsLoadableRuleDefinition).ToList());
|
||||
|
||||
var yamlNodes = MiniYaml.Merge(sources);
|
||||
WorldActorInfo = new ActorInfo(modData.ObjectCreator, "world", yamlNodes.First(n => string.Equals(n.Key, "world", StringComparison.InvariantCultureIgnoreCase)).Value);
|
||||
PlayerActorInfo = new ActorInfo(modData.ObjectCreator, "player", yamlNodes.First(n => string.Equals(n.Key, "player", StringComparison.InvariantCultureIgnoreCase)).Value);
|
||||
WorldActorInfo = new ActorInfo(
|
||||
modData.ObjectCreator,
|
||||
"world",
|
||||
yamlNodes.First(n => string.Equals(n.Key, "world", StringComparison.InvariantCultureIgnoreCase)).Value);
|
||||
PlayerActorInfo = new ActorInfo(
|
||||
modData.ObjectCreator,
|
||||
"player",
|
||||
yamlNodes.First(n => string.Equals(n.Key, "player", StringComparison.InvariantCultureIgnoreCase)).Value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -167,24 +199,40 @@ namespace OpenRA
|
||||
}
|
||||
}
|
||||
|
||||
static readonly CPos[] NoSpawns = Array.Empty<CPos>();
|
||||
readonly object syncRoot = new();
|
||||
readonly MapCache cache;
|
||||
readonly ModData modData;
|
||||
IReadOnlyPackage package;
|
||||
|
||||
public readonly string Uid;
|
||||
public IReadOnlyPackage Package { get; private set; }
|
||||
|
||||
public string Path { get; private set; }
|
||||
|
||||
void LoadPackage()
|
||||
{
|
||||
if (package == null && parentPackage != null)
|
||||
package = parentPackage.OpenPackage(Path, modData.ModFiles);
|
||||
}
|
||||
|
||||
public Map ToMap()
|
||||
{
|
||||
LoadPackage();
|
||||
using (new PerfTimer("Map"))
|
||||
return new Map(modData, package);
|
||||
}
|
||||
|
||||
IReadOnlyPackage parentPackage;
|
||||
|
||||
volatile InnerData innerData;
|
||||
|
||||
public int MapFormat => innerData.MapFormat;
|
||||
public string Title => innerData.Title;
|
||||
public string[] Categories => innerData.Categories;
|
||||
public ImmutableArray<string> Categories => innerData.Categories;
|
||||
public string Author => innerData.Author;
|
||||
public string TileSet => innerData.TileSet;
|
||||
public MapPlayers Players => innerData.Players;
|
||||
public int PlayerCount => innerData.PlayerCount;
|
||||
public CPos[] SpawnPoints => innerData.SpawnPoints;
|
||||
public ImmutableArray<CPos> SpawnPoints => innerData.SpawnPoints;
|
||||
public MapGridType GridType => innerData.GridType;
|
||||
public Rectangle Bounds => innerData.Bounds;
|
||||
public Png Preview => innerData.Preview;
|
||||
@@ -200,20 +248,37 @@ namespace OpenRA
|
||||
public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo;
|
||||
public DateTime ModifiedDate => innerData.ModifiedDate;
|
||||
|
||||
public MapGenerationArgs GenerationArgs => innerData.GenerationArgs;
|
||||
|
||||
public long DownloadBytes { get; private set; }
|
||||
public int DownloadPercentage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Functionality mirrors <see cref="TranslationProvider.GetString"/>, except instead of using
|
||||
/// loaded <see cref="Map"/>'s translations as backup, we use this <see cref="MapPreview"/>'s.
|
||||
/// Functionality mirrors <see cref="FluentProvider.GetMessage"/>, except instead of using
|
||||
/// loaded <see cref="Map"/>'s fluent bundle as backup, we use this <see cref="MapPreview"/>'s.
|
||||
/// </summary>
|
||||
public string GetLocalisedString(string key, IDictionary<string, object> args = null)
|
||||
public string GetMessage(string key, object[] args = null)
|
||||
{
|
||||
// PERF: instead of loading mod level Translation per each MapPreview, reuse the already loaded one in TranslationProvider.
|
||||
if (TranslationProvider.TryGetModString(key, out var message, args))
|
||||
if (TryGetMessage(key, out var message, args))
|
||||
return message;
|
||||
|
||||
return innerData.Translation?.GetString(key, args) ?? key;
|
||||
return key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Functionality mirrors <see cref="FluentProvider.TryGetMessage"/>, except instead of using
|
||||
/// loaded <see cref="Map"/>'s fluent bundle as backup, we use this <see cref="MapPreview"/>'s.
|
||||
/// </summary>
|
||||
public bool TryGetMessage(string key, out string message, object[] args = null)
|
||||
{
|
||||
// PERF: instead of loading mod level strings per each MapPreview, reuse the already loaded one in FluentProvider.
|
||||
if (FluentProvider.TryGetModMessage(key, out message, args))
|
||||
return true;
|
||||
|
||||
if (innerData.FluentBundle == null)
|
||||
return false;
|
||||
|
||||
return innerData.FluentBundle.TryGetMessage(key, out message, args);
|
||||
}
|
||||
|
||||
Sprite minimap;
|
||||
@@ -262,12 +327,12 @@ namespace OpenRA
|
||||
{
|
||||
MapFormat = 0,
|
||||
Title = "Unknown Map",
|
||||
Categories = new[] { "Unknown" },
|
||||
Categories = ["Unknown"],
|
||||
Author = "Unknown Author",
|
||||
TileSet = "unknown",
|
||||
Players = null,
|
||||
PlayerCount = 0,
|
||||
SpawnPoints = NoSpawns,
|
||||
SpawnPoints = [],
|
||||
GridType = gridType,
|
||||
Bounds = Rectangle.Empty,
|
||||
Preview = null,
|
||||
@@ -277,71 +342,40 @@ namespace OpenRA
|
||||
};
|
||||
}
|
||||
|
||||
// For linting purposes only!
|
||||
public MapPreview(Map map, ModData modData)
|
||||
/// <summary>
|
||||
/// Updates internal state from a map without taking ownership of its package.
|
||||
/// A new copy of the map package will be opened lazily when needed.
|
||||
/// </summary>
|
||||
public void UpdateFromMapWithoutOwningPackage(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification,
|
||||
MapGridType? gridType = null, MiniYamlNode[][] modDataRules = null)
|
||||
{
|
||||
this.modData = modData;
|
||||
cache = modData.MapCache;
|
||||
|
||||
Uid = map.Uid;
|
||||
Package = map.Package;
|
||||
|
||||
var mapPlayers = new MapPlayers(map.PlayerDefinitions);
|
||||
var spawns = new List<CPos>();
|
||||
foreach (var kv in map.ActorDefinitions.Where(d => d.Value.Value == "mpspawn"))
|
||||
{
|
||||
var s = new ActorReference(kv.Value.Value, kv.Value.ToDictionary());
|
||||
spawns.Add(s.Get<LocationInit>().Value);
|
||||
}
|
||||
|
||||
innerData = new InnerData
|
||||
{
|
||||
MapFormat = map.MapFormat,
|
||||
Title = map.Title,
|
||||
Categories = map.Categories,
|
||||
Author = map.Author,
|
||||
TileSet = map.Tileset,
|
||||
Players = mapPlayers,
|
||||
PlayerCount = mapPlayers.Players.Count(x => x.Value.Playable),
|
||||
SpawnPoints = spawns.ToArray(),
|
||||
GridType = map.Grid.Type,
|
||||
Bounds = map.Bounds,
|
||||
Preview = null,
|
||||
Status = MapStatus.Available,
|
||||
Class = MapClassification.Unknown,
|
||||
Visibility = map.Visibility,
|
||||
};
|
||||
|
||||
innerData.SetCustomRules(modData, this, new Dictionary<string, MiniYaml>()
|
||||
{
|
||||
{ "Rules", map.RuleDefinitions },
|
||||
{ "Translations", map.TranslationDefinitions },
|
||||
{ "Weapons", map.WeaponDefinitions },
|
||||
{ "Voices", map.VoiceDefinitions },
|
||||
{ "Music", map.MusicDefinitions },
|
||||
{ "Notifications", map.NotificationDefinitions },
|
||||
{ "Sequences", map.SequenceDefinitions },
|
||||
{ "ModelSequences", map.ModelSequenceDefinitions }
|
||||
}, null);
|
||||
UpdateFromMap(p, classification, gridType, modDataRules);
|
||||
parentPackage = parent;
|
||||
package = null;
|
||||
}
|
||||
|
||||
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType, IEnumerable<List<MiniYamlNode>> modDataRules)
|
||||
/// <summary>
|
||||
/// Updates internal state from a map and takes ownership of its package.
|
||||
/// The package remains in memory and must not be disposed.
|
||||
/// </summary>
|
||||
public void UpdateFromMap(IReadOnlyPackage p, MapClassification classification,
|
||||
MapGridType? gridType = null, MiniYamlNode[][] modDataRules = null)
|
||||
{
|
||||
Path = p.Name;
|
||||
package = p;
|
||||
|
||||
Dictionary<string, MiniYaml> yaml;
|
||||
using (var yamlStream = p.GetStream("map.yaml"))
|
||||
{
|
||||
if (yamlStream == null)
|
||||
throw new FileNotFoundException("Required file map.yaml not present in this map");
|
||||
|
||||
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, "map.yaml", stringPool: cache.StringPool)).ToDictionary();
|
||||
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, $"{p.Name}:map.yaml", stringPool: cache.StringPool)).ToDictionary();
|
||||
}
|
||||
|
||||
Package = p;
|
||||
parentPackage = parent;
|
||||
|
||||
var newData = innerData.Clone();
|
||||
newData.GridType = gridType;
|
||||
newData.Class = classification;
|
||||
newData.GridType = gridType ?? modData.GetOrCreate<MapGrid>().Type;
|
||||
|
||||
if (yaml.TryGetValue("MapFormat", out var temp))
|
||||
{
|
||||
@@ -354,7 +388,7 @@ namespace OpenRA
|
||||
newData.Title = temp.Value;
|
||||
|
||||
if (yaml.TryGetValue("Categories", out temp))
|
||||
newData.Categories = FieldLoader.GetValue<string[]>("Categories", temp.Value);
|
||||
newData.Categories = FieldLoader.GetValue<ImmutableArray<string>>("Categories", temp.Value);
|
||||
|
||||
if (yaml.TryGetValue("Tileset", out temp))
|
||||
newData.TileSet = temp.Value;
|
||||
@@ -375,7 +409,7 @@ namespace OpenRA
|
||||
if (yaml.TryGetValue("MapFormat", out temp))
|
||||
newData.MapFormat = FieldLoader.GetValue<int>("MapFormat", temp.Value);
|
||||
|
||||
newData.Status = mapCompatibility == null || mapCompatibility.Contains(requiresMod) ?
|
||||
newData.Status = modData.Manifest.MapCompatibility.Contains(requiresMod) ?
|
||||
MapStatus.Available : MapStatus.Unavailable;
|
||||
|
||||
try
|
||||
@@ -386,18 +420,18 @@ namespace OpenRA
|
||||
var spawns = new List<CPos>();
|
||||
foreach (var kv in actorDefinitions.Nodes.Where(d => d.Value.Value == "mpspawn"))
|
||||
{
|
||||
var s = new ActorReference(kv.Value.Value, kv.Value.ToDictionary());
|
||||
var s = new ActorReference(kv.Value.Value, kv.Value);
|
||||
spawns.Add(s.Get<LocationInit>().Value);
|
||||
}
|
||||
|
||||
newData.SpawnPoints = spawns.ToArray();
|
||||
newData.SpawnPoints = spawns.ToImmutableArray();
|
||||
}
|
||||
else
|
||||
newData.SpawnPoints = Array.Empty<CPos>();
|
||||
newData.SpawnPoints = [];
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
newData.SpawnPoints = Array.Empty<CPos>();
|
||||
newData.SpawnPoints = [];
|
||||
newData.Status = MapStatus.Unavailable;
|
||||
}
|
||||
|
||||
@@ -421,31 +455,122 @@ namespace OpenRA
|
||||
using (var dataStream = p.GetStream("map.png"))
|
||||
newData.Preview = new Png(dataStream);
|
||||
|
||||
newData.ModifiedDate = File.GetLastWriteTime(p.Name);
|
||||
newData.ModifiedDate = p.Name != null ? File.GetLastWriteTime(p.Name) : DateTime.Now;
|
||||
|
||||
// Assign the new data atomically
|
||||
innerData = newData;
|
||||
// Local maps have higher precedence than remote/generated maps,
|
||||
// so should always replace their metadata
|
||||
lock (syncRoot)
|
||||
innerData = newData;
|
||||
}
|
||||
|
||||
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action<MapPreview> parseMetadata = null)
|
||||
public void UpdateFromGenerationArgs(MapGenerationArgs args)
|
||||
{
|
||||
var newData = innerData.Clone();
|
||||
newData.Status = status;
|
||||
newData.Class = MapClassification.Remote;
|
||||
newData.Class = MapClassification.Generated;
|
||||
newData.Status = MapStatus.Generatable;
|
||||
newData.Title = args.Title;
|
||||
newData.Author = args.Author;
|
||||
newData.TileSet = args.Tileset;
|
||||
newData.GenerationArgs = args;
|
||||
newData.MapFormat = Map.CurrentMapFormat;
|
||||
|
||||
if (status == MapStatus.DownloadAvailable)
|
||||
try
|
||||
{
|
||||
var generator = modData.DefaultRules.Actors[SystemActors.EditorWorld]
|
||||
.TraitInfos<IMapGeneratorInfo>()
|
||||
.FirstOrDefault(info => info.Type == args.Generator);
|
||||
|
||||
if (generator == null)
|
||||
throw new Exception($"Unknown map generator type {args.Generator}");
|
||||
|
||||
if (!generator.TryGenerateMetadata(modData, args, out var players, out var ruleDefinitions))
|
||||
throw new Exception("Failed to generate map metadata");
|
||||
|
||||
newData.Players = players;
|
||||
newData.PlayerCount = newData.Players.Players.Count(x => x.Value.Playable);
|
||||
newData.SetCustomRules(modData, this, ruleDefinitions, null);
|
||||
|
||||
// Placeholder to satisfy server-side lint checks
|
||||
newData.SpawnPoints = Exts.MakeArray(newData.PlayerCount, i => new CPos(i, i)).ToImmutableArray();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Write("debug", "Map generation failed with error:");
|
||||
Log.Write("debug", e);
|
||||
|
||||
newData.Status = MapStatus.Unavailable;
|
||||
}
|
||||
|
||||
lock (syncRoot)
|
||||
innerData = newData;
|
||||
}
|
||||
|
||||
public void Generate()
|
||||
{
|
||||
if (Class != MapClassification.Generated || Status != MapStatus.Generatable)
|
||||
return;
|
||||
|
||||
lock (syncRoot)
|
||||
innerData.Status = MapStatus.Generating;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var generator = modData.DefaultRules.Actors[SystemActors.EditorWorld]
|
||||
.TraitInfos<IMapGeneratorInfo>()
|
||||
.FirstOrDefault(info => info.Type == GenerationArgs.Generator);
|
||||
|
||||
if (generator == null)
|
||||
throw new Exception($"Unknown map generator type {GenerationArgs.Generator}");
|
||||
|
||||
var map = generator.Generate(modData, GenerationArgs);
|
||||
|
||||
// Uid is generated when the map is saved
|
||||
map.Save(new ZipFileLoader.ReadWriteZipFile());
|
||||
|
||||
if (map.Uid != GenerationArgs.Uid)
|
||||
throw new InvalidOperationException("Map generation UID mismatch");
|
||||
|
||||
Game.RunAfterTick(() => UpdateFromMap(map.Package, MapClassification.Generated));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Write("debug", "Map generation failed with error:");
|
||||
Log.Write("debug", e);
|
||||
|
||||
lock (syncRoot)
|
||||
innerData.Status = MapStatus.Unavailable;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void BeginRemoteSearch()
|
||||
{
|
||||
var newData = innerData.Clone();
|
||||
newData.Class = MapClassification.Remote;
|
||||
newData.Status = MapStatus.Searching;
|
||||
|
||||
// We may have been resolved to a local/generated map by another
|
||||
// async task. Make sure we don't stomp over their state!
|
||||
lock (syncRoot)
|
||||
if (innerData.Class == MapClassification.Unknown || innerData.Class == MapClassification.Remote)
|
||||
innerData = newData;
|
||||
}
|
||||
|
||||
public void CompleteRemoteSearch(MiniYaml yaml, Action<MapPreview> parseMetadata = null)
|
||||
{
|
||||
var newData = innerData.Clone();
|
||||
newData.Class = MapClassification.Remote;
|
||||
newData.Status = MapStatus.Unavailable;
|
||||
|
||||
if (yaml != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var r = FieldLoader.Load<RemoteMapData>(yaml);
|
||||
|
||||
// Map download has been disabled server side
|
||||
if (!r.downloading)
|
||||
{
|
||||
newData.Status = MapStatus.Unavailable;
|
||||
return;
|
||||
}
|
||||
|
||||
newData.Status = r.downloading ? MapStatus.DownloadAvailable : MapStatus.Unavailable;
|
||||
newData.Title = r.title;
|
||||
newData.Categories = r.categories;
|
||||
newData.Author = r.author;
|
||||
@@ -457,7 +582,7 @@ namespace OpenRA
|
||||
var spawns = new CPos[r.spawnpoints.Length / 2];
|
||||
for (var j = 0; j < r.spawnpoints.Length; j += 2)
|
||||
spawns[j / 2] = new CPos(r.spawnpoints[j], r.spawnpoints[j + 1]);
|
||||
newData.SpawnPoints = spawns;
|
||||
newData.SpawnPoints = spawns.ToImmutableArray();
|
||||
newData.GridType = r.map_grid_type;
|
||||
if (cache.LoadPreviewImages)
|
||||
{
|
||||
@@ -474,32 +599,48 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
var playersString = Encoding.UTF8.GetString(Convert.FromBase64String(r.players_block));
|
||||
newData.Players = new MapPlayers(MiniYaml.FromString(playersString));
|
||||
newData.Players = new MapPlayers(MiniYaml.FromString(playersString,
|
||||
$"{yaml.NodeWithKey(nameof(r.players_block)).Location.Name}:{nameof(r.players_block)}"));
|
||||
|
||||
var rulesString = Encoding.UTF8.GetString(Convert.FromBase64String(r.rules));
|
||||
var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString)).ToDictionary();
|
||||
var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString,
|
||||
$"{yaml.NodeWithKey(nameof(r.rules)).Location.Name}:{nameof(r.rules)}")).ToDictionary();
|
||||
newData.SetCustomRules(modData, this, rulesYaml, null);
|
||||
|
||||
// Map is for a different mod: update its information so it can be displayed
|
||||
// in the cross-mod server browser UI, but mark it as unavailable so it can't
|
||||
// be selected in a server for the current mod.
|
||||
if (!modData.Manifest.MapCompatibility.Contains(r.game_mod))
|
||||
newData.Status = MapStatus.Unavailable;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Write("debug", "Failed parsing mapserver response:");
|
||||
Log.Write("debug", e);
|
||||
newData.Status = MapStatus.Unavailable;
|
||||
}
|
||||
}
|
||||
|
||||
// Commit updated data before running the callbacks
|
||||
innerData = newData;
|
||||
// We may have been resolved to a local/generated map by another
|
||||
// async task. Make sure we don't stomp over their state!
|
||||
MapClassification mapClassification;
|
||||
lock (syncRoot)
|
||||
{
|
||||
mapClassification = innerData.Class;
|
||||
if (mapClassification == MapClassification.Remote)
|
||||
innerData = newData;
|
||||
}
|
||||
|
||||
if (mapClassification == MapClassification.Remote)
|
||||
{
|
||||
if (innerData.Preview != null)
|
||||
cache.CacheMinimap(this);
|
||||
|
||||
parseMetadata?.Invoke(this);
|
||||
}
|
||||
|
||||
// Update the status and class unconditionally
|
||||
innerData = newData;
|
||||
}
|
||||
|
||||
public void Install(string mapRepositoryUrl, Action onSuccess)
|
||||
public void Install(string mapRepositoryUrl)
|
||||
{
|
||||
if ((Status != MapStatus.DownloadError && Status != MapStatus.DownloadAvailable) || !Game.Settings.Game.AllowDownloading)
|
||||
return;
|
||||
@@ -552,14 +693,11 @@ namespace OpenRA
|
||||
mapInstallPackage.Update(mapFilename, fileStream.ToArray());
|
||||
Log.Write("debug", $"Downloaded map to '{mapFilename}'");
|
||||
|
||||
var package = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles);
|
||||
if (package == null)
|
||||
var p = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles);
|
||||
if (p == null)
|
||||
innerData.Status = MapStatus.DownloadError;
|
||||
else
|
||||
{
|
||||
UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType, null);
|
||||
Game.RunAfterTick(onSuccess);
|
||||
}
|
||||
UpdateFromMapWithoutOwningPackage(p, mapInstallPackage, MapClassification.User, GridType);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -572,29 +710,31 @@ namespace OpenRA
|
||||
|
||||
public void Invalidate()
|
||||
{
|
||||
innerData.Status = MapStatus.Unavailable;
|
||||
lock (syncRoot)
|
||||
{
|
||||
innerData.Class = MapClassification.Unknown;
|
||||
innerData.Status = MapStatus.Unavailable;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Package != null)
|
||||
{
|
||||
Package.Dispose();
|
||||
Package = null;
|
||||
}
|
||||
package?.Dispose();
|
||||
package = null;
|
||||
}
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
Invalidate();
|
||||
(parentPackage as IReadWritePackage)?.Delete(Package.Name);
|
||||
(parentPackage as IReadWritePackage)?.Delete(Path);
|
||||
}
|
||||
|
||||
Stream IReadOnlyFileSystem.Open(string filename)
|
||||
{
|
||||
// Explicit package paths never refer to a map
|
||||
if (!filename.Contains('|') && Package.Contains(filename))
|
||||
return Package.GetStream(filename);
|
||||
LoadPackage();
|
||||
if (!filename.Contains('|') && package.Contains(filename))
|
||||
return package.GetStream(filename);
|
||||
|
||||
return modData.DefaultFileSystem.Open(filename);
|
||||
}
|
||||
@@ -610,7 +750,8 @@ namespace OpenRA
|
||||
// Explicit package paths never refer to a map
|
||||
if (!filename.Contains('|'))
|
||||
{
|
||||
s = Package.GetStream(filename);
|
||||
LoadPackage();
|
||||
s = package.GetStream(filename);
|
||||
if (s != null)
|
||||
return true;
|
||||
}
|
||||
@@ -621,17 +762,18 @@ namespace OpenRA
|
||||
bool IReadOnlyFileSystem.Exists(string filename)
|
||||
{
|
||||
// Explicit package paths never refer to a map
|
||||
if (!filename.Contains('|') && Package.Contains(filename))
|
||||
LoadPackage();
|
||||
if (!filename.Contains('|') && package.Contains(filename))
|
||||
return true;
|
||||
|
||||
return modData.DefaultFileSystem.Exists(filename);
|
||||
}
|
||||
|
||||
bool IReadOnlyFileSystem.IsExternalModFile(string filename)
|
||||
bool IReadOnlyFileSystem.IsExternalFile(string filename)
|
||||
{
|
||||
// Explicit package paths never refer to a map
|
||||
if (filename.Contains('|'))
|
||||
return modData.DefaultFileSystem.IsExternalModFile(filename);
|
||||
return modData.DefaultFileSystem.IsExternalFile(filename);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA
|
||||
@@ -31,7 +31,7 @@ namespace OpenRA
|
||||
public string Faction;
|
||||
|
||||
public bool LockColor = false;
|
||||
public Color Color = Game.ModData.Manifest.Get<DefaultPlayer>().Color;
|
||||
public Color Color = Game.ModData.GetOrCreate<DefaultPlayer>().Color;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the "Home" location, which can be used by traits and scripts to e.g. set the initial camera
|
||||
@@ -54,8 +54,8 @@ namespace OpenRA
|
||||
public bool LockHandicap = false;
|
||||
public int Handicap = 0;
|
||||
|
||||
public string[] Allies = Array.Empty<string>();
|
||||
public string[] Enemies = Array.Empty<string>();
|
||||
public ImmutableArray<string> Allies = [];
|
||||
public ImmutableArray<string> Enemies = [];
|
||||
|
||||
public PlayerReference() { }
|
||||
public PlayerReference(MiniYaml my) { FieldLoader.Load(this, my); }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user