Compare commits
772 Commits
d40f5292b6
...
ef0c9533a4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef0c9533a4 | ||
|
|
3aa84277a3 | ||
|
|
f9bca71b15 | ||
|
|
0bbe6e58f6 | ||
|
|
ee4403f923 | ||
|
|
aacf0e2b8d | ||
|
|
499a40e32d | ||
|
|
68fd987856 | ||
|
|
b4f3d8ae02 | ||
|
|
d70c30763b | ||
|
|
27297a1ba9 | ||
|
|
2890ccf2b1 | ||
|
|
f9f1889112 | ||
|
|
d9c5821329 | ||
|
|
7fc415de19 | ||
|
|
ec01f63317 | ||
|
|
c9ac702217 | ||
|
|
1913c608a6 | ||
|
|
94ac6cc400 | ||
|
|
97d9520bf0 | ||
|
|
e7300cc430 | ||
|
|
677d7e47a4 | ||
|
|
5f287d38ab | ||
|
|
3a00d5a679 | ||
|
|
d5d43ee97c | ||
|
|
fd12ef1535 | ||
|
|
de8317d9b8 | ||
|
|
b953f4a40d | ||
|
|
b565f0903a | ||
|
|
da71a8271d | ||
|
|
54558169e1 | ||
|
|
bcb953309a | ||
|
|
a371826613 | ||
|
|
82ee3d779c | ||
|
|
4ea0e70589 | ||
|
|
3f4e4db836 | ||
|
|
85020bc2af | ||
|
|
70bb01a2c7 | ||
|
|
38d4719073 | ||
|
|
aebb97967d | ||
|
|
97f81d0aa4 | ||
|
|
2030c17a8b | ||
|
|
b6dcd8d9af | ||
|
|
9a78f22e86 | ||
|
|
19ca3d610b | ||
|
|
dd74d3572a | ||
|
|
ad7e5d62cd | ||
|
|
ece7547695 | ||
|
|
944bc7667a | ||
|
|
0ac28e1f5f | ||
|
|
ca3a57289e | ||
|
|
a4641be7e7 | ||
|
|
0651308210 | ||
|
|
cac6aebb6c | ||
|
|
0baddf46e2 | ||
|
|
e0d893b736 | ||
|
|
2ccfa425b2 | ||
|
|
52102da839 | ||
|
|
fc08f18663 | ||
|
|
cc064d0dad | ||
|
|
8b997a5656 | ||
|
|
7d8fff535d | ||
|
|
0855a9e5bf | ||
|
|
3fa2b9fe79 | ||
|
|
c561509c82 | ||
|
|
0566b6f787 | ||
|
|
e2821933a4 | ||
|
|
34c29f872a | ||
|
|
2192a6b4f9 | ||
|
|
68ad9163ff | ||
|
|
5f6eecc1f6 | ||
|
|
5edca87622 | ||
|
|
fa01c24f23 | ||
|
|
593c2735ce | ||
|
|
48e72c62f3 | ||
|
|
f316d4c757 | ||
|
|
d6dba3e545 | ||
|
|
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 |
944
.editorconfig
944
.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
|
||||
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@@ -14,11 +14,14 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Remove System .NET
|
||||
run: sudo apt-get remove -y dotnet*
|
||||
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
@@ -39,7 +42,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check Code
|
||||
run: |
|
||||
@@ -57,10 +60,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
|
||||
147
.github/workflows/documentation.yml
vendored
147
.github/workflows/documentation.yml
vendored
@@ -1,6 +1,9 @@
|
||||
name: Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ bleed ]
|
||||
tags: [ 'release-*', 'playtest-*' ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
@@ -12,18 +15,49 @@ 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: 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 }}
|
||||
|
||||
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@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag }}
|
||||
ref: ${{ needs.prepare.outputs.git_tag }}
|
||||
|
||||
- name: Install .NET 6
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
@@ -32,49 +66,53 @@ jobs:
|
||||
make all
|
||||
|
||||
- name: Clone Wiki
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
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: Debug output
|
||||
run: |
|
||||
echo ${{ needs.prepare.outputs.git_tag }}
|
||||
echo ${{ needs.prepare.outputs.version_type }}
|
||||
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag }}
|
||||
ref: ${{ needs.prepare.outputs.git_tag }}
|
||||
|
||||
- name: Install .NET 6
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
@@ -82,62 +120,31 @@ jobs:
|
||||
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@v4
|
||||
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 }}
|
||||
|
||||
54
.github/workflows/packaging.yml
vendored
54
.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@v4
|
||||
|
||||
- name: Prepare Environment
|
||||
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
|
||||
@@ -40,10 +40,10 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
@@ -53,27 +53,25 @@ 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-13
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
@@ -92,23 +90,21 @@ 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@v4
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
@@ -124,10 +120,8 @@ jobs:
|
||||
./packaging/windows/buildpackage.sh "${GIT_TAG}" "${PWD}/build/windows"
|
||||
|
||||
- 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/windows/*
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
gh release upload ${{ github.ref_name }} build/windows/*
|
||||
|
||||
60
AUTHORS
60
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)
|
||||
@@ -98,6 +97,7 @@ Also thanks to:
|
||||
* Kanar
|
||||
* Kenny Hoxworth (hoxworth)
|
||||
* Kevin Azzam (ChaoticMind)
|
||||
* Kevin Streser
|
||||
* Krishnakanth Mallik
|
||||
* Kyle Smith (Smitty)
|
||||
* Kyrre Soerensen (zypres)
|
||||
@@ -150,6 +150,7 @@ Also thanks to:
|
||||
* Teemu Nieminen (Temeez)
|
||||
* Thomas Christlieb (ThomasChr)
|
||||
* Tim Mylemans (gecko)
|
||||
* Tinix
|
||||
* Tirili
|
||||
* Tomas Einarsson (Mesacer)
|
||||
* Tom van Leth (tovl)
|
||||
@@ -161,61 +162,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.
|
||||
|
||||
|
||||
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>.
|
||||
|
||||
@@ -33,7 +33,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 +51,11 @@
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<!-- StyleCop -->
|
||||
<!-- StyleCop/Roslynator -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
|
||||
<!-- Roslynator analyzers fail to run under Mono (AD0001) -->
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.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"]
|
||||
@@ -7,7 +7,7 @@ Windows
|
||||
=======
|
||||
|
||||
Compiling OpenRA requires the following dependencies:
|
||||
* [Windows PowerShell >= 4.0](http://microsoft.com/powershell) (included by default in recent Windows 10 versions)
|
||||
* [Windows PowerShell >= 4.0](https://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)
|
||||
|
||||
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.
|
||||
@@ -25,7 +25,7 @@ To compile OpenRA, run `make` from the command line (or `make RUNTIME=mono` if u
|
||||
|
||||
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:
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -153,12 +153,12 @@ 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'
|
||||
|
||||
@@ -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);
|
||||
@@ -155,6 +160,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 +187,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 +202,7 @@ namespace OpenRA
|
||||
EnabledTargetablePositions = targetablePositions.Where(Exts.IsTraitEnabled);
|
||||
enabledTargetableWorldPositions = EnabledTargetablePositions.SelectMany(tp => tp.TargetablePositions(this));
|
||||
SyncHashes = syncHashesList.ToArray();
|
||||
crushables = crushablesList.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -96,7 +97,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 +107,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 +121,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 +149,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;
|
||||
|
||||
@@ -61,14 +63,14 @@ namespace OpenRA
|
||||
|
||||
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
|
||||
@@ -76,7 +78,8 @@ namespace OpenRA
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace OpenRA
|
||||
// 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 char[] HexUpperAlphabet = "0123456789ABCDEF".ToArray();
|
||||
static readonly char[] HexLowerAlphabet = "0123456789abcdef".ToArray();
|
||||
|
||||
public static string PublicKeyFingerprint(RSAParameters parameters)
|
||||
{
|
||||
// Public key fingerprint is defined as the SHA1 of the modulus + exponent bytes
|
||||
@@ -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)
|
||||
@@ -249,19 +252,44 @@ 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());
|
||||
using var csp = SHA1.Create();
|
||||
return ToHex(csp.ComputeHash(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());
|
||||
using var csp = SHA1.Create();
|
||||
return ToHex(csp.ComputeHash(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ namespace OpenRA
|
||||
{
|
||||
public readonly string Id;
|
||||
public readonly string Version;
|
||||
public readonly string Title;
|
||||
public readonly string LaunchPath;
|
||||
public readonly string[] LaunchArgs;
|
||||
public Sprite Icon { get; internal set; }
|
||||
@@ -66,6 +65,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 +76,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 +94,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 +122,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[]
|
||||
{
|
||||
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 +204,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);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Support;
|
||||
using OpenRA.Traits;
|
||||
@@ -22,15 +23,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); }
|
||||
@@ -131,11 +141,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 +182,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 +337,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 +378,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 +390,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);
|
||||
@@ -394,15 +433,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,14 +463,15 @@ namespace OpenRA
|
||||
continue;
|
||||
|
||||
// Check for a key conflict:
|
||||
if (!d.TryAdd(key, element))
|
||||
if (!output.TryAdd(key, element))
|
||||
{
|
||||
dupKeys ??= new Dictionary<TKey, List<string>>();
|
||||
if (!dupKeys.TryGetValue(key, out var dupKeyMessages))
|
||||
{
|
||||
// Log the initial conflicting value already inserted:
|
||||
dupKeyMessages = new List<string>
|
||||
{
|
||||
logValue(d[key])
|
||||
logValue(output[key])
|
||||
};
|
||||
dupKeys.Add(key, dupKeyMessages);
|
||||
}
|
||||
@@ -431,15 +482,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($"{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)
|
||||
@@ -493,17 +543,22 @@ namespace OpenRA
|
||||
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 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 bool TryParseInt32Invariant(string s, out int i)
|
||||
{
|
||||
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
||||
}
|
||||
@@ -513,6 +568,26 @@ namespace OpenRA
|
||||
return long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
||||
}
|
||||
|
||||
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 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;
|
||||
@@ -562,6 +637,11 @@ namespace OpenRA
|
||||
{
|
||||
return new LineSplitEnumerator(str.AsSpan(), separator);
|
||||
}
|
||||
|
||||
public static bool TryParseInt32Invariant(string s, out int i)
|
||||
{
|
||||
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
||||
}
|
||||
}
|
||||
|
||||
public ref struct LineSplitEnumerator
|
||||
@@ -576,7 +656,7 @@ namespace OpenRA
|
||||
Current = default;
|
||||
}
|
||||
|
||||
public LineSplitEnumerator GetEnumerator() => this;
|
||||
public readonly LineSplitEnumerator GetEnumerator() => this;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
|
||||
@@ -87,6 +87,7 @@ namespace OpenRA
|
||||
{ typeof(WAngle), ParseWAngle },
|
||||
{ typeof(WRot), ParseWRot },
|
||||
{ typeof(CPos), ParseCPos },
|
||||
{ typeof(CPos[]), ParseCPosArray },
|
||||
{ typeof(CVec), ParseCVec },
|
||||
{ typeof(CVec[]), ParseCVecArray },
|
||||
{ typeof(BooleanExpression), ParseBooleanExpression },
|
||||
@@ -118,7 +119,7 @@ namespace OpenRA
|
||||
|
||||
static object ParseInt(string fieldName, Type fieldType, string value, MemberInfo field)
|
||||
{
|
||||
if (Exts.TryParseIntegerInvariant(value, out var res))
|
||||
if (Exts.TryParseInt32Invariant(value, out var res))
|
||||
{
|
||||
if (res >= 0 && res < BoxedInts.Length)
|
||||
return BoxedInts[res];
|
||||
@@ -195,11 +196,11 @@ namespace OpenRA
|
||||
if (value != null)
|
||||
{
|
||||
var parts = value.Split(SplitComma);
|
||||
if (parts.Length == 3)
|
||||
{
|
||||
if (WDist.TryParse(parts[0], out var rx) && WDist.TryParse(parts[1], out var ry) && WDist.TryParse(parts[2], out var rz))
|
||||
return new WVec(rx, ry, rz);
|
||||
}
|
||||
if (parts.Length == 3
|
||||
&& WDist.TryParse(parts[0], out var rx)
|
||||
&& WDist.TryParse(parts[1], out var ry)
|
||||
&& WDist.TryParse(parts[2], out var rz))
|
||||
return new WVec(rx, ry, rz);
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -219,8 +220,8 @@ namespace OpenRA
|
||||
for (var i = 0; i < vecs.Length; ++i)
|
||||
{
|
||||
if (WDist.TryParse(parts[3 * i], out var rx)
|
||||
&& WDist.TryParse(parts[3 * i + 1], out var ry)
|
||||
&& WDist.TryParse(parts[3 * i + 2], out var rz))
|
||||
&& WDist.TryParse(parts[3 * i + 1], out var ry)
|
||||
&& WDist.TryParse(parts[3 * i + 2], out var rz))
|
||||
vecs[i] = new WVec(rx, ry, rz);
|
||||
}
|
||||
|
||||
@@ -235,13 +236,11 @@ namespace OpenRA
|
||||
if (value != null)
|
||||
{
|
||||
var parts = value.Split(SplitComma);
|
||||
if (parts.Length == 3)
|
||||
{
|
||||
if (WDist.TryParse(parts[0], out var rx)
|
||||
&& WDist.TryParse(parts[1], out var ry)
|
||||
&& WDist.TryParse(parts[2], out var rz))
|
||||
return new WPos(rx, ry, rz);
|
||||
}
|
||||
if (parts.Length == 3
|
||||
&& WDist.TryParse(parts[0], out var rx)
|
||||
&& WDist.TryParse(parts[1], out var ry)
|
||||
&& WDist.TryParse(parts[2], out var rz))
|
||||
return new WPos(rx, ry, rz);
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -249,7 +248,7 @@ namespace OpenRA
|
||||
|
||||
static object ParseWAngle(string fieldName, Type fieldType, string value, MemberInfo field)
|
||||
{
|
||||
if (Exts.TryParseIntegerInvariant(value, out var res))
|
||||
if (Exts.TryParseInt32Invariant(value, out var res))
|
||||
return new WAngle(res);
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
}
|
||||
@@ -259,13 +258,11 @@ namespace OpenRA
|
||||
if (value != null)
|
||||
{
|
||||
var parts = value.Split(SplitComma);
|
||||
if (parts.Length == 3)
|
||||
{
|
||||
if (Exts.TryParseIntegerInvariant(parts[0], out var rr)
|
||||
&& Exts.TryParseIntegerInvariant(parts[1], out var rp)
|
||||
&& Exts.TryParseIntegerInvariant(parts[2], out var ry))
|
||||
return new WRot(new WAngle(rr), new WAngle(rp), new WAngle(ry));
|
||||
}
|
||||
if (parts.Length == 3
|
||||
&& Exts.TryParseInt32Invariant(parts[0], out var rr)
|
||||
&& Exts.TryParseInt32Invariant(parts[1], out var rp)
|
||||
&& Exts.TryParseInt32Invariant(parts[2], out var ry))
|
||||
return new WRot(new WAngle(rr), new WAngle(rp), new WAngle(ry));
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -278,10 +275,33 @@ namespace OpenRA
|
||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 3)
|
||||
return new CPos(
|
||||
Exts.ParseIntegerInvariant(parts[0]),
|
||||
Exts.ParseIntegerInvariant(parts[1]),
|
||||
Exts.ParseByte(parts[2]));
|
||||
return new CPos(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
|
||||
Exts.ParseInt32Invariant(parts[0]),
|
||||
Exts.ParseInt32Invariant(parts[1]),
|
||||
Exts.ParseByteInvariant(parts[2]));
|
||||
return new CPos(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
}
|
||||
|
||||
static object ParseCPosArray(string fieldName, Type fieldType, string value, MemberInfo field)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
var parts = value.Split(SplitComma);
|
||||
|
||||
if (parts.Length % 2 != 0)
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
|
||||
var vecs = new CPos[parts.Length / 2];
|
||||
for (var i = 0; i < vecs.Length; i++)
|
||||
{
|
||||
if (int.TryParse(parts[2 * i], out var rx)
|
||||
&& int.TryParse(parts[2 * i + 1], out var ry))
|
||||
vecs[i] = new CPos(rx, ry);
|
||||
}
|
||||
|
||||
return vecs;
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -292,7 +312,7 @@ namespace OpenRA
|
||||
if (value != null)
|
||||
{
|
||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
||||
return new CVec(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
|
||||
return new CVec(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -385,7 +405,7 @@ namespace OpenRA
|
||||
|
||||
var ints = new int2[parts.Length / 2];
|
||||
for (var i = 0; i < ints.Length; i++)
|
||||
ints[i] = new int2(Exts.ParseIntegerInvariant(parts[2 * i]), Exts.ParseIntegerInvariant(parts[2 * i + 1]));
|
||||
ints[i] = new int2(Exts.ParseInt32Invariant(parts[2 * i]), Exts.ParseInt32Invariant(parts[2 * i + 1]));
|
||||
|
||||
return ints;
|
||||
}
|
||||
@@ -398,7 +418,7 @@ namespace OpenRA
|
||||
if (value != null)
|
||||
{
|
||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
||||
return new Size(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
|
||||
return new Size(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -412,7 +432,7 @@ namespace OpenRA
|
||||
if (parts.Length != 2)
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
|
||||
return new int2(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
|
||||
return new int2(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -460,10 +480,10 @@ namespace OpenRA
|
||||
{
|
||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
||||
return new Rectangle(
|
||||
Exts.ParseIntegerInvariant(parts[0]),
|
||||
Exts.ParseIntegerInvariant(parts[1]),
|
||||
Exts.ParseIntegerInvariant(parts[2]),
|
||||
Exts.ParseIntegerInvariant(parts[3]));
|
||||
Exts.ParseInt32Invariant(parts[0]),
|
||||
Exts.ParseInt32Invariant(parts[1]),
|
||||
Exts.ParseInt32Invariant(parts[2]),
|
||||
Exts.ParseInt32Invariant(parts[3]));
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -500,7 +520,7 @@ namespace OpenRA
|
||||
if (yaml == null)
|
||||
return Activator.CreateInstance(fieldType);
|
||||
|
||||
var dict = Activator.CreateInstance(fieldType, yaml.Nodes.Count);
|
||||
var dict = Activator.CreateInstance(fieldType, yaml.Nodes.Length);
|
||||
var arguments = fieldType.GetGenericArguments();
|
||||
var addMethod = fieldType.GetMethod(nameof(Dictionary<object, object>.Add), arguments);
|
||||
var addArgs = new object[2];
|
||||
@@ -531,7 +551,7 @@ namespace OpenRA
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return null;
|
||||
|
||||
var innerType = fieldType.GetGenericArguments().First();
|
||||
var innerType = fieldType.GetGenericArguments()[0];
|
||||
var innerValue = GetValue("Nullable<T>", innerType, value, field);
|
||||
return fieldType.GetConstructor(new[] { innerType }).Invoke(new[] { innerValue });
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA
|
||||
@@ -58,7 +59,7 @@ namespace OpenRA
|
||||
|
||||
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)
|
||||
@@ -84,7 +85,7 @@ namespace OpenRA
|
||||
// This is only for documentation generation
|
||||
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
var result = "";
|
||||
var result = new StringBuilder();
|
||||
var dict = (System.Collections.IDictionary)v;
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
@@ -94,10 +95,10 @@ namespace OpenRA
|
||||
var formattedKey = FormatValue(key);
|
||||
var formattedValue = FormatValue(value);
|
||||
|
||||
result += $"{formattedKey}: {formattedValue}{Environment.NewLine}";
|
||||
result.Append($"{formattedKey}: {formattedValue}{Environment.NewLine}");
|
||||
}
|
||||
|
||||
return result;
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
if (v is DateTime d)
|
||||
|
||||
@@ -16,6 +16,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using ICSharpCode.SharpZipLib.Checksum;
|
||||
using ICSharpCode.SharpZipLib.Zip.Compression;
|
||||
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Primitives;
|
||||
@@ -45,12 +46,13 @@ namespace OpenRA.FileFormats
|
||||
var data = new List<byte>();
|
||||
Type = SpriteFrameType.Rgba32;
|
||||
|
||||
byte bitDepth = 8;
|
||||
while (true)
|
||||
{
|
||||
var length = IPAddress.NetworkToHostOrder(s.ReadInt32());
|
||||
var type = Encoding.UTF8.GetString(s.ReadBytes(4));
|
||||
var type = s.ReadASCII(4);
|
||||
var content = s.ReadBytes(length);
|
||||
/*var crc = */s.ReadInt32();
|
||||
s.ReadInt32(); // crc
|
||||
|
||||
if (!headerParsed && type != "IHDR")
|
||||
throw new InvalidDataException("Invalid PNG file - header does not appear first.");
|
||||
@@ -66,7 +68,7 @@ namespace OpenRA.FileFormats
|
||||
Width = IPAddress.NetworkToHostOrder(ms.ReadInt32());
|
||||
Height = IPAddress.NetworkToHostOrder(ms.ReadInt32());
|
||||
|
||||
var bitDepth = ms.ReadUInt8();
|
||||
bitDepth = ms.ReadUInt8();
|
||||
var colorType = (PngColorType)ms.ReadUInt8();
|
||||
if (IsPaletted(bitDepth, colorType))
|
||||
Type = SpriteFrameType.Indexed8;
|
||||
@@ -76,7 +78,7 @@ namespace OpenRA.FileFormats
|
||||
Data = new byte[Width * Height * PixelStride];
|
||||
|
||||
var compression = ms.ReadUInt8();
|
||||
/*var filter = */ms.ReadUInt8();
|
||||
ms.ReadUInt8(); // filter
|
||||
var interlace = ms.ReadUInt8();
|
||||
|
||||
if (compression != 0)
|
||||
@@ -92,8 +94,8 @@ namespace OpenRA.FileFormats
|
||||
|
||||
case "PLTE":
|
||||
{
|
||||
Palette = new Color[256];
|
||||
for (var i = 0; i < length / 3; i++)
|
||||
Palette = new Color[length / 3];
|
||||
for (var i = 0; i < Palette.Length; i++)
|
||||
{
|
||||
var r = ms.ReadUInt8(); var g = ms.ReadUInt8(); var b = ms.ReadUInt8();
|
||||
Palette[i] = Color.FromArgb(r, g, b);
|
||||
@@ -136,14 +138,34 @@ namespace OpenRA.FileFormats
|
||||
{
|
||||
var pxStride = PixelStride;
|
||||
var rowStride = Width * pxStride;
|
||||
var pixelsPerByte = 8 / bitDepth;
|
||||
var sourceRowStride = Exts.IntegerDivisionRoundingAwayFromZero(rowStride, pixelsPerByte);
|
||||
|
||||
Span<byte> prevLine = new byte[rowStride];
|
||||
for (var y = 0; y < Height; y++)
|
||||
{
|
||||
var filter = (PngFilter)ds.ReadUInt8();
|
||||
ds.ReadBytes(Data, y * rowStride, rowStride);
|
||||
ds.ReadBytes(Data, y * rowStride, sourceRowStride);
|
||||
var line = Data.AsSpan(y * rowStride, rowStride);
|
||||
|
||||
// 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 = 0xFF >> (8 - bitDepth);
|
||||
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:
|
||||
@@ -269,7 +291,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))
|
||||
@@ -287,10 +309,10 @@ namespace OpenRA.FileFormats
|
||||
|
||||
var typeBytes = Encoding.ASCII.GetBytes(type);
|
||||
output.Write(IPAddress.HostToNetworkOrder((int)input.Length));
|
||||
output.WriteArray(typeBytes);
|
||||
output.Write(typeBytes);
|
||||
|
||||
var data = input.ReadAllBytes();
|
||||
output.WriteArray(data);
|
||||
output.Write(data);
|
||||
|
||||
var crc32 = new Crc32();
|
||||
crc32.Update(typeBytes);
|
||||
@@ -302,7 +324,7 @@ namespace OpenRA.FileFormats
|
||||
{
|
||||
using (var output = new MemoryStream())
|
||||
{
|
||||
output.WriteArray(Signature);
|
||||
output.Write(Signature);
|
||||
using (var header = new MemoryStream())
|
||||
{
|
||||
header.Write(IPAddress.HostToNetworkOrder(Width));
|
||||
@@ -350,13 +372,14 @@ namespace OpenRA.FileFormats
|
||||
|
||||
using (var data = new MemoryStream())
|
||||
{
|
||||
using (var compressed = new DeflaterOutputStream(data))
|
||||
using (var compressed = new DeflaterOutputStream(data, new Deflater(Deflater.BEST_COMPRESSION)))
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -371,7 +394,7 @@ namespace OpenRA.FileFormats
|
||||
{
|
||||
using (var text = new MemoryStream())
|
||||
{
|
||||
text.WriteArray(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
|
||||
text.Write(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
|
||||
WritePngChunk(output, "tEXt", text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,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 +62,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,7 +23,7 @@ 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
|
||||
@@ -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
|
||||
@@ -185,11 +181,13 @@ namespace OpenRA.FileSystem
|
||||
fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => new 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;
|
||||
|
||||
@@ -41,7 +41,11 @@ namespace OpenRA.FileSystem
|
||||
|
||||
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,7 +94,7 @@ namespace OpenRA.FileSystem
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage
|
||||
public sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage
|
||||
{
|
||||
readonly MemoryStream pkgStream = new();
|
||||
|
||||
@@ -113,14 +114,15 @@ 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;
|
||||
}
|
||||
|
||||
void Commit()
|
||||
{
|
||||
var pos = pkgStream.Position;
|
||||
pkgStream.Position = 0;
|
||||
File.WriteAllBytes(Name, pkgStream.ReadBytes((int)pkgStream.Length));
|
||||
pkgStream.Position = pos;
|
||||
File.WriteAllBytes(Name, pkgStream.ToArray());
|
||||
}
|
||||
|
||||
public void Update(string filename, byte[] contents)
|
||||
@@ -147,7 +149,7 @@ namespace OpenRA.FileSystem
|
||||
|
||||
public ZipFolder(ReadOnlyZipFile parent, string path)
|
||||
{
|
||||
if (path.EndsWith("/", StringComparison.Ordinal))
|
||||
if (path.EndsWith('/'))
|
||||
path = path[..^1];
|
||||
|
||||
Name = path;
|
||||
|
||||
167
OpenRA.Game/FluentBundle.cs
Normal file
167
OpenRA.Game/FluentBundle.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
#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.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;
|
||||
}
|
||||
}
|
||||
|
||||
public class FluentBundle
|
||||
{
|
||||
readonly Linguini.Bundle.FluentBundle bundle;
|
||||
|
||||
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem)
|
||||
: this(culture, paths, fileSystem, error => Log.Write("debug", error.Message)) { }
|
||||
|
||||
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, string text)
|
||||
: this(culture, paths, fileSystem, text, error => Log.Write("debug", error.Message)) { }
|
||||
|
||||
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, Action<ParseError> onError)
|
||||
: this(culture, paths, fileSystem, null, onError) { }
|
||||
|
||||
public FluentBundle(string culture, string text, Action<ParseError> onError)
|
||||
: this(culture, null, null, text, onError) { }
|
||||
|
||||
public FluentBundle(string culture, 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)
|
||||
{
|
||||
if (key == null)
|
||||
throw new ArgumentNullException(nameof(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 = new Dictionary<string, IFluentType>();
|
||||
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)
|
||||
{
|
||||
93
OpenRA.Game/FluentProvider.cs
Normal file
93
OpenRA.Game/FluentProvider.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
#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.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(ModData modData, IReadOnlyFileSystem fileSystem)
|
||||
{
|
||||
lock (SyncObject)
|
||||
{
|
||||
modFluentBundle = new FluentBundle(modData.Manifest.FluentCulture, modData.Manifest.FluentMessages, fileSystem);
|
||||
if (fileSystem is Map map && map.FluentMessageDefinitions != null)
|
||||
{
|
||||
var files = Array.Empty<string>();
|
||||
if (map.FluentMessageDefinitions.Value != null)
|
||||
files = FieldLoader.GetValue<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(modData.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ namespace OpenRA
|
||||
{
|
||||
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 +86,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
|
||||
@@ -179,6 +180,7 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public static event Action BeforeGameStart = () => { };
|
||||
public static event Action AfterGameStart = () => { };
|
||||
internal static void StartGame(string mapUID, WorldType type)
|
||||
{
|
||||
// Dispose of the old world before creating a new one.
|
||||
@@ -222,6 +224,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()
|
||||
@@ -360,22 +368,7 @@ 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);
|
||||
var platform = CreatePlatform(p);
|
||||
Renderer = new Renderer(platform, Settings.Graphics);
|
||||
Sound = new Sound(platform, Settings.Sound);
|
||||
|
||||
@@ -402,7 +395,7 @@ namespace OpenRA
|
||||
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})");
|
||||
Console.WriteLine($"\t{mod.Key} ({mod.Value.Metadata.Version})");
|
||||
|
||||
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
|
||||
|
||||
@@ -415,7 +408,7 @@ namespace OpenRA
|
||||
|
||||
// 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() == '"')
|
||||
if (launchPath != null && launchPath[0] == '"' && launchPath.Last() == '"')
|
||||
launchPath = launchPath[1..^1];
|
||||
|
||||
// Metadata registration requires an explicit launch path
|
||||
@@ -427,11 +420,31 @@ namespace OpenRA
|
||||
|
||||
Console.WriteLine("External mods:");
|
||||
foreach (var mod in ExternalMods)
|
||||
Console.WriteLine($"\t{mod.Key}: {mod.Value.Title} ({mod.Value.Version})");
|
||||
Console.WriteLine($"\t{mod.Key} ({mod.Value.Version})");
|
||||
|
||||
InitializeMod(modID, args);
|
||||
}
|
||||
|
||||
public static IPlatform CreatePlatform(string platformName)
|
||||
{
|
||||
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + platformName + ".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.");
|
||||
|
||||
return (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null);
|
||||
}
|
||||
|
||||
public static void InitializeMod(string mod, Arguments args)
|
||||
{
|
||||
// Clear static state if we have switched mods
|
||||
@@ -483,11 +496,11 @@ namespace OpenRA
|
||||
Renderer.InitializeDepthBuffer(grid);
|
||||
|
||||
Cursor?.Dispose();
|
||||
Cursor = new CursorManager(ModData.CursorProvider);
|
||||
Cursor = new CursorManager(ModData.CursorProvider, ModData.Manifest.CursorSheetSize);
|
||||
|
||||
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;
|
||||
@@ -523,10 +536,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)
|
||||
@@ -581,7 +595,7 @@ namespace OpenRA
|
||||
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 +614,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 +626,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())
|
||||
{
|
||||
@@ -663,7 +684,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 +694,7 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
Ui.PrepareRenderables();
|
||||
Renderer.WorldModelRenderer.EndFrame();
|
||||
worldRenderer?.EndFrame();
|
||||
}
|
||||
|
||||
// worldRenderer is null during the initial install/download screen
|
||||
@@ -777,7 +798,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
|
||||
@@ -912,15 +933,15 @@ 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()
|
||||
{
|
||||
@@ -934,9 +955,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 +985,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.PackageName) == launchMap);
|
||||
if (map == null)
|
||||
throw new ArgumentException($"Could not find map '{launchMap}'.");
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ namespace OpenRA
|
||||
{
|
||||
public class GameInformation
|
||||
{
|
||||
[FluentReference("name", "number")]
|
||||
const string EnumeratedBotName = "enumerated-bot-name";
|
||||
|
||||
public string Mod;
|
||||
public string Version;
|
||||
|
||||
@@ -49,13 +52,13 @@ namespace OpenRA
|
||||
playersByRuntime = new Dictionary<OpenRA.Player, Player>();
|
||||
}
|
||||
|
||||
public static GameInformation Deserialize(string data)
|
||||
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('@');
|
||||
@@ -85,7 +88,7 @@ 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++)
|
||||
@@ -118,11 +121,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 +147,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 +170,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>
|
||||
@@ -33,7 +33,7 @@ namespace OpenRA
|
||||
/// </summary>
|
||||
public readonly string Name;
|
||||
readonly TypeDictionary traits = new();
|
||||
List<TraitInfo> constructOrderCache = null;
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -124,7 +124,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 +182,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 +226,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 +260,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<string[]>("value", mapRules.Value);
|
||||
foreach (var f in mapFiles)
|
||||
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -40,16 +40,16 @@ namespace OpenRA.GameRules
|
||||
static Dictionary<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);
|
||||
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);
|
||||
|
||||
|
||||
@@ -139,13 +139,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(new IReadOnlyCollection<MiniYamlNode>[] { 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 +160,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)
|
||||
|
||||
@@ -10,13 +10,12 @@
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public class GameSpeed
|
||||
{
|
||||
[TranslationReference]
|
||||
[FluentReference]
|
||||
[FieldLoader.Require]
|
||||
public readonly string Name;
|
||||
|
||||
@@ -38,7 +37,7 @@ namespace OpenRA
|
||||
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!");
|
||||
|
||||
|
||||
@@ -22,6 +22,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 +44,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,13 +60,18 @@ 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 };
|
||||
}
|
||||
|
||||
@@ -77,11 +77,12 @@ namespace OpenRA.Graphics
|
||||
cachedPanelSprites = new Dictionary<string, Sprite[]>();
|
||||
cachedCollectionSheets = new Dictionary<Collection, (Sheet, int)>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -227,14 +228,18 @@ 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())
|
||||
if (collection.Regions.Count == 0)
|
||||
return Array.Empty<Sprite>();
|
||||
|
||||
// Support manual definitions for unusual dialog layouts
|
||||
|
||||
@@ -11,11 +11,12 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public sealed class CursorManager
|
||||
public sealed class CursorManager : IDisposable
|
||||
{
|
||||
sealed class Cursor
|
||||
{
|
||||
@@ -38,13 +39,16 @@ namespace OpenRA.Graphics
|
||||
readonly bool hardwareCursorsDisabled = false;
|
||||
bool hardwareCursorsDoubled = false;
|
||||
|
||||
public CursorManager(CursorProvider cursorProvider)
|
||||
public CursorManager(CursorProvider cursorProvider, int cursorSheetSize)
|
||||
{
|
||||
hardwareCursorsDisabled = Game.Settings.Graphics.DisableHardwareCursors;
|
||||
|
||||
graphicSettings = Game.Settings.Graphics;
|
||||
sheetBuilder = new SheetBuilder(SheetType.BGRA);
|
||||
foreach (var kv in cursorProvider.Cursors)
|
||||
sheetBuilder = new SheetBuilder(SheetType.BGRA, cursorSheetSize);
|
||||
|
||||
// Sort the cursors for better packing onto the sheet.
|
||||
foreach (var kv in cursorProvider.Cursors
|
||||
.OrderBy(kvp => kvp.Value.Frames.Max(f => f.Size.Height)))
|
||||
{
|
||||
var frames = kv.Value.Frames;
|
||||
var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? cursorProvider.Palettes[kv.Value.Palette] : null;
|
||||
@@ -91,10 +95,6 @@ namespace OpenRA.Graphics
|
||||
}
|
||||
|
||||
CreateOrUpdateHardwareCursors();
|
||||
|
||||
foreach (var s in sheetBuilder.AllSheets)
|
||||
s.ReleaseBuffer();
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
@@ -128,6 +128,8 @@ namespace OpenRA.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
sheetBuilder.Current.ReleaseBuffer();
|
||||
|
||||
hardwareCursorsDoubled = graphicSettings.CursorDouble;
|
||||
}
|
||||
|
||||
@@ -229,6 +231,10 @@ namespace OpenRA.Graphics
|
||||
|
||||
var width = frame.Size.Width;
|
||||
var height = frame.Size.Height;
|
||||
|
||||
if (width == 0 || height == 0)
|
||||
return Array.Empty<byte>();
|
||||
|
||||
var data = new byte[4 * width * height];
|
||||
unsafe
|
||||
{
|
||||
|
||||
@@ -24,10 +24,11 @@ namespace OpenRA.Graphics
|
||||
public CursorProvider(ModData modData)
|
||||
{
|
||||
var fileSystem = modData.DefaultFileSystem;
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
var sequenceYaml = MiniYaml.Merge(modData.Manifest.Cursors.Select(
|
||||
s => MiniYaml.FromStream(fileSystem.Open(s), s)));
|
||||
s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool)));
|
||||
|
||||
var nodesDict = new MiniYaml(null, sequenceYaml).ToDictionary();
|
||||
var cursorsYaml = new MiniYaml(null, sequenceYaml).NodeWithKey("Cursors").Value;
|
||||
|
||||
// Overwrite previous definitions if there are duplicates
|
||||
var pals = new Dictionary<string, IProvidesCursorPaletteInfo>();
|
||||
@@ -35,14 +36,14 @@ namespace OpenRA.Graphics
|
||||
if (p.Palette != null)
|
||||
pals[p.Palette] = p;
|
||||
|
||||
Palettes = nodesDict["Cursors"].Nodes.Select(n => n.Value.Value)
|
||||
Palettes = cursorsYaml.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 s in cursorsYaml.Nodes)
|
||||
foreach (var sequence in s.Value.Nodes)
|
||||
cursors.Add(sequence.Key, new CursorSequence(frameCache, sequence.Key, s.Key, s.Value.Value, sequence.Value));
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
var d = info.ToDictionary();
|
||||
|
||||
Start = Exts.ParseIntegerInvariant(d["Start"].Value);
|
||||
Start = Exts.ParseInt32Invariant(d["Start"].Value);
|
||||
Palette = palette;
|
||||
Name = name;
|
||||
|
||||
@@ -38,9 +38,9 @@ namespace OpenRA.Graphics
|
||||
(d.TryGetValue("End", out yaml) && yaml.Value == "*"))
|
||||
Length = Frames.Length;
|
||||
else if (d.TryGetValue("Length", out yaml))
|
||||
Length = Exts.ParseIntegerInvariant(yaml.Value);
|
||||
Length = Exts.ParseInt32Invariant(yaml.Value);
|
||||
else if (d.TryGetValue("End", out yaml))
|
||||
Length = Exts.ParseIntegerInvariant(yaml.Value) - Start;
|
||||
Length = Exts.ParseInt32Invariant(yaml.Value) - Start;
|
||||
else
|
||||
Length = 1;
|
||||
|
||||
@@ -54,13 +54,13 @@ namespace OpenRA.Graphics
|
||||
|
||||
if (d.TryGetValue("X", out 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +29,14 @@ namespace OpenRA.Graphics
|
||||
Rectangle AggregateBounds { get; }
|
||||
}
|
||||
|
||||
public interface IModelWidget
|
||||
{
|
||||
public string Palette { get; }
|
||||
public float Scale { get; }
|
||||
public void Setup(Func<bool> isVisible, Func<string> getPalette, Func<string> getPlayerPalette,
|
||||
Func<float> getScale, Func<IModel> getVoxel, Func<WRot> getRotation);
|
||||
}
|
||||
|
||||
public readonly struct ModelRenderData
|
||||
{
|
||||
public readonly int Start;
|
||||
@@ -44,51 +51,13 @@ namespace OpenRA.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
public interface IModelCache : IDisposable
|
||||
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[]
|
||||
{
|
||||
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
|
||||
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
|
||||
new ShaderVertexAttribute("aVertexTexMetadata", ShaderVertexAttributeType.Float, 2, 28),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,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)
|
||||
@@ -103,7 +103,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 +142,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 +153,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);
|
||||
}
|
||||
@@ -83,16 +83,18 @@ namespace OpenRA
|
||||
|
||||
public interface IGraphicsContext : IDisposable
|
||||
{
|
||||
IVertexBuffer<Vertex> CreateVertexBuffer(int size);
|
||||
Vertex[] CreateVertices(int size);
|
||||
IVertexBuffer<T> CreateVertexBuffer<T>(int size) 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 +104,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,6 +123,11 @@ 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);
|
||||
@@ -124,6 +138,17 @@ namespace OpenRA
|
||||
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 +157,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; }
|
||||
|
||||
64
OpenRA.Game/Graphics/RenderPostProcessPassVertex.cs
Normal file
64
OpenRA.Game/Graphics/RenderPostProcessPassVertex.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
#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 RenderPostProcessPassVertex
|
||||
{
|
||||
public readonly float X, Y;
|
||||
|
||||
public RenderPostProcessPassVertex(float x, float y)
|
||||
{
|
||||
X = x; Y = y;
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly struct RenderPostProcessPassTexturedVertex
|
||||
{
|
||||
// 3d position
|
||||
public readonly float X, Y;
|
||||
public readonly float S, T;
|
||||
|
||||
public RenderPostProcessPassTexturedVertex(float x, float y, float s, float t)
|
||||
{
|
||||
X = x; Y = y;
|
||||
S = s; T = t;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RenderPostProcessPassShaderBindings : ShaderBindings
|
||||
{
|
||||
public RenderPostProcessPassShaderBindings(string name)
|
||||
: base("postprocess", "postprocess_" + name) { }
|
||||
|
||||
public override ShaderVertexAttribute[] Attributes { get; } = new[]
|
||||
{
|
||||
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[]
|
||||
{
|
||||
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;
|
||||
@@ -200,20 +194,6 @@ namespace OpenRA.Graphics
|
||||
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);
|
||||
}
|
||||
|
||||
public void FillRect(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha)
|
||||
{
|
||||
var tr = new float3(br.X, tl.Y, tl.Z);
|
||||
@@ -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)
|
||||
|
||||
@@ -94,7 +94,7 @@ 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);
|
||||
|
||||
70
OpenRA.Game/Graphics/ShaderBindings.cs
Normal file
70
OpenRA.Game/Graphics/ShaderBindings.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
#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 struct ShaderVertexAttribute
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly ShaderVertexAttributeType Type;
|
||||
public readonly int Components;
|
||||
public readonly int Offset;
|
||||
|
||||
public ShaderVertexAttribute(string name, ShaderVertexAttributeType type, int components, int offset)
|
||||
{
|
||||
Name = name;
|
||||
Type = type;
|
||||
Components = components;
|
||||
Offset = 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 commiting 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();
|
||||
|
||||
@@ -82,16 +82,16 @@ namespace OpenRA.Graphics
|
||||
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)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
@@ -130,8 +130,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 +147,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,28 +14,30 @@ using System.Collections.Generic;
|
||||
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<
|
||||
int,
|
||||
(int[] Frames, MiniYamlNode.SourceLocation Location, AdjustFrame AdjustFrame, bool Premultiplied)> spriteReservations = new();
|
||||
readonly Dictionary<string, List<int>> reservationsByFilename = new();
|
||||
|
||||
readonly Dictionary<int, ISpriteFrame[]> resolvedFrames = new();
|
||||
readonly Dictionary<int, Sprite[]> resolvedSprites = new();
|
||||
|
||||
readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = new();
|
||||
|
||||
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,97 +49,110 @@ namespace OpenRA.Graphics
|
||||
this.loaders = loaders;
|
||||
}
|
||||
|
||||
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
|
||||
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location,
|
||||
AdjustFrame adjustFrame = null, bool premultiplied = false)
|
||||
{
|
||||
var token = nextReservationToken++;
|
||||
spriteReservations[token] = (frames?.ToArray(), location);
|
||||
spriteReservations[token] = (frames?.ToArray(), location, adjustFrame, premultiplied);
|
||||
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).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 ?? Enumerable.Range(0, loadedFrames.Length);
|
||||
var total = 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();
|
||||
@@ -145,18 +160,11 @@ namespace OpenRA.Graphics
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = new();
|
||||
readonly IndexBufferRc indexBufferWrapper;
|
||||
|
||||
public readonly BlendMode BlendMode;
|
||||
|
||||
@@ -28,7 +31,8 @@ namespace OpenRA.Graphics
|
||||
readonly Vertex[] vertices;
|
||||
readonly bool[] ignoreTint;
|
||||
readonly HashSet<int> dirtyRows = new();
|
||||
readonly int rowStride;
|
||||
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];
|
||||
vertexRowStride = 4 * map.MapSize.X;
|
||||
vertices = new Vertex[vertexRowStride * map.MapSize.Y];
|
||||
vertexBuffer = Game.Renderer.Context.CreateVertexBuffer<Vertex>(vertices.Length);
|
||||
|
||||
indexRowStride = 6 * map.MapSize.X;
|
||||
lock (IndexBuffers)
|
||||
{
|
||||
indexBufferWrapper = IndexBuffers.GetValue(world, world => new IndexBufferRc(world));
|
||||
indexBufferWrapper.AddRef();
|
||||
}
|
||||
|
||||
palettes = new PaletteReference[map.MapSize.X * map.MapSize.Y];
|
||||
vertexBuffer = Game.Renderer.Context.CreateVertexBuffer(vertices.Length);
|
||||
|
||||
wr.PaletteInvalidated += UpdatePaletteIndices;
|
||||
|
||||
if (wr.TerrainLighting != null)
|
||||
{
|
||||
ignoreTint = new bool[rowStride * map.MapSize.Y];
|
||||
ignoreTint = new bool[vertexRowStride * map.MapSize.Y];
|
||||
wr.TerrainLighting.CellChanged += UpdateTint;
|
||||
}
|
||||
}
|
||||
@@ -65,8 +75,9 @@ 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++)
|
||||
@@ -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,7 +191,7 @@ 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;
|
||||
|
||||
@@ -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,29 @@ 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.X * world.Map.MapSize.Y));
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -20,7 +21,17 @@ namespace OpenRA.Graphics
|
||||
// yes, our channel order is nuts.
|
||||
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 = stackalloc uint[] { 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
{
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +215,9 @@ namespace OpenRA.Graphics
|
||||
MinZoom = CalculateMinimumZoom(range.X, range.Y) * viewportSizes.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 * viewportSizes.DefaultScale / viewportSizes.MaxZoomWindowHeight);
|
||||
|
||||
if (unlockMinZoom)
|
||||
{
|
||||
@@ -231,11 +233,12 @@ 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)
|
||||
@@ -282,11 +285,23 @@ 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--)
|
||||
@@ -299,10 +314,13 @@ namespace OpenRA.Graphics
|
||||
|
||||
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)
|
||||
|
||||
@@ -44,6 +44,8 @@ namespace OpenRA.Graphics
|
||||
readonly List<IFinalizedRenderable> preparedAnnotationRenderables = new();
|
||||
|
||||
readonly List<IRenderable> renderablesBuffer = new();
|
||||
readonly IRenderer[] renderers;
|
||||
readonly IRenderPostProcessPass[] postProcessPasses;
|
||||
|
||||
internal WorldRenderer(ModData modData, World world)
|
||||
{
|
||||
@@ -60,15 +62,29 @@ namespace OpenRA.Graphics
|
||||
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>());
|
||||
|
||||
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)
|
||||
@@ -270,6 +286,8 @@ namespace OpenRA.Graphics
|
||||
if (enableDepthBuffer)
|
||||
Game.Renderer.ClearDepthBuffer();
|
||||
|
||||
ApplyPostProcessing(PostProcessPassType.AfterActors);
|
||||
|
||||
World.ApplyToActorsWithTrait<IRenderAboveWorld>((actor, trait) =>
|
||||
{
|
||||
if (actor.IsInWorld && !actor.Disposed)
|
||||
@@ -279,6 +297,8 @@ namespace OpenRA.Graphics
|
||||
if (enableDepthBuffer)
|
||||
Game.Renderer.ClearDepthBuffer();
|
||||
|
||||
ApplyPostProcessing(PostProcessPassType.AfterWorld);
|
||||
|
||||
World.ApplyToActorsWithTrait<IRenderShroud>((actor, trait) => trait.RenderShroud(this));
|
||||
|
||||
if (enableDepthBuffer)
|
||||
@@ -292,9 +312,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();
|
||||
|
||||
@@ -16,11 +16,19 @@ 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();
|
||||
|
||||
[FluentReference]
|
||||
public readonly HashSet<string> Contexts = new();
|
||||
|
||||
public readonly bool Readonly = false;
|
||||
public bool HasDuplicates { get; internal set; }
|
||||
|
||||
@@ -31,29 +39,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<HashSet<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<HashSet<string>>("Contexts", contextYaml.Value)
|
||||
.Select(c => ContextFluentPrefix + "." + c).ToHashSet();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,16 +91,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
|
||||
{
|
||||
@@ -61,6 +62,33 @@ namespace OpenRA
|
||||
Meta = 8,
|
||||
}
|
||||
|
||||
public static class ModifiersExts
|
||||
{
|
||||
[FluentReference]
|
||||
public 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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -43,25 +43,22 @@ namespace OpenRA
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
readonly string Title;
|
||||
public readonly string Version;
|
||||
public readonly string Website;
|
||||
public readonly string WebIcon32;
|
||||
[FluentReference]
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Describes what is to be loaded in order to run a mod.</summary>
|
||||
@@ -72,27 +69,34 @@ namespace OpenRA
|
||||
public readonly ModMetadata Metadata;
|
||||
public readonly 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 MiniYaml FileSystem;
|
||||
public readonly MiniYaml LoadScreen;
|
||||
public readonly string DefaultOrderGenerator;
|
||||
|
||||
public readonly string[] Assemblies = Array.Empty<string>();
|
||||
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 int FontSheetSize = 512;
|
||||
public readonly int CursorSheetSize = 512;
|
||||
|
||||
// TODO: This should be controlled by a user-selected translation bundle!
|
||||
public readonly string FluentCulture = "en";
|
||||
public readonly bool AllowUnusedFluentMessagesInExternalPackages = true;
|
||||
|
||||
readonly string[] reservedModuleNames =
|
||||
{
|
||||
"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"
|
||||
"RequiresMods", "PackageFormats", "AllowUnusedFluentMessagesInExternalPackages", "FontSheetSize", "CursorSheetSize"
|
||||
};
|
||||
|
||||
readonly TypeDictionary modules = new();
|
||||
@@ -105,7 +109,8 @@ namespace OpenRA
|
||||
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);
|
||||
for (var i = nodes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (nodes[i].Key != "Include")
|
||||
@@ -118,7 +123,7 @@ 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
|
||||
@@ -129,21 +134,20 @@ namespace OpenRA
|
||||
// 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");
|
||||
@@ -165,6 +169,9 @@ namespace OpenRA
|
||||
if (yaml.TryGetValue("DefaultOrderGenerator", out entry))
|
||||
DefaultOrderGenerator = entry.Value;
|
||||
|
||||
if (yaml.TryGetValue("Assemblies", out entry))
|
||||
Assemblies = FieldLoader.GetValue<string[]>("Assemblies", entry.Value);
|
||||
|
||||
if (yaml.TryGetValue("PackageFormats", out entry))
|
||||
PackageFormats = FieldLoader.GetValue<string[]>("PackageFormats", entry.Value);
|
||||
|
||||
@@ -176,6 +183,16 @@ namespace OpenRA
|
||||
|
||||
if (yaml.TryGetValue("VideoFormats", out entry))
|
||||
VideoFormats = FieldLoader.GetValue<string[]>("VideoFormats", entry.Value);
|
||||
|
||||
if (yaml.TryGetValue("AllowUnusedFluentMessagesInExternalPackages", out entry))
|
||||
AllowUnusedFluentMessagesInExternalPackages =
|
||||
FieldLoader.GetValue<bool>("AllowUnusedFluentMessagesInExternalPackages", entry.Value);
|
||||
|
||||
if (yaml.TryGetValue("FontSheetSize", out entry))
|
||||
FontSheetSize = FieldLoader.GetValue<int>("FontSheetSize", entry.Value);
|
||||
|
||||
if (yaml.TryGetValue("CursorSheetSize", out entry))
|
||||
CursorSheetSize = FieldLoader.GetValue<int>("CursorSheetSize", entry.Value);
|
||||
}
|
||||
|
||||
public void LoadCustomData(ObjectCreator oc)
|
||||
@@ -211,18 +228,18 @@ namespace OpenRA
|
||||
|
||||
static string[] YamlList(Dictionary<string, MiniYaml> yaml, string key)
|
||||
{
|
||||
if (!yaml.ContainsKey(key))
|
||||
if (!yaml.TryGetValue(key, out var value))
|
||||
return Array.Empty<string>();
|
||||
|
||||
return yaml[key].ToDictionary().Keys.ToArray();
|
||||
return value.Nodes.Select(n => n.Key).ToArray();
|
||||
}
|
||||
|
||||
static IReadOnlyDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
|
||||
{
|
||||
if (!yaml.ContainsKey(key))
|
||||
if (!yaml.TryGetValue(key, out var value))
|
||||
return new Dictionary<string, string>();
|
||||
|
||||
return yaml[key].ToDictionary(my => my.Value);
|
||||
return value.ToDictionary(my => my.Value);
|
||||
}
|
||||
|
||||
public bool Contains<T>() where T : IGlobalModData
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -84,7 +84,7 @@ namespace OpenRA
|
||||
|
||||
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 +98,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(); }
|
||||
@@ -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));
|
||||
|
||||
95
OpenRA.Game/Map/CellCoordsRegion.cs
Normal file
95
OpenRA.Game/Map/CellCoordsRegion.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
#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;
|
||||
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();
|
||||
}
|
||||
|
||||
public CPos TopLeft { get; }
|
||||
public CPos BottomRight { get; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -158,26 +159,26 @@ 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
|
||||
@@ -197,18 +198,18 @@ namespace OpenRA
|
||||
public int2 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 = ImmutableArray<MiniYamlNode>.Empty;
|
||||
public IReadOnlyCollection<MiniYamlNode> ActorDefinitions = ImmutableArray<MiniYamlNode>.Empty;
|
||||
|
||||
// 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();
|
||||
|
||||
@@ -274,7 +275,10 @@ 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
|
||||
@@ -357,15 +361,15 @@ 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 ?? ImmutableArray<MiniYamlNode>.Empty;
|
||||
ActorDefinitions = yaml.NodeWithKeyOrDefault("Actors")?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty;
|
||||
|
||||
Grid = modData.Manifest.Get<MapGrid>();
|
||||
|
||||
@@ -607,7 +611,7 @@ namespace OpenRA
|
||||
|
||||
// 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 +650,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;
|
||||
|
||||
@@ -681,16 +688,16 @@ namespace OpenRA
|
||||
writer.Write((ushort)MapSize.Y);
|
||||
|
||||
// Data offsets
|
||||
var tilesOffset = 17;
|
||||
const int 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;
|
||||
|
||||
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++)
|
||||
{
|
||||
@@ -772,19 +779,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 +799,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 +821,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 +844,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 +858,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)
|
||||
@@ -1180,7 +1200,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 +1269,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 +1369,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 +1427,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;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenRA.FileSystem;
|
||||
@@ -38,7 +39,7 @@ namespace OpenRA
|
||||
readonly object syncRoot = new();
|
||||
readonly Queue<MapPreview> generateMinimap = new();
|
||||
|
||||
public Dictionary<string, string> StringPool { get; } = new Dictionary<string, string>();
|
||||
public HashSet<string> StringPool { get; } = new();
|
||||
|
||||
readonly List<MapDirectoryTracker> mapDirectoryTrackers = new();
|
||||
|
||||
@@ -97,7 +98,7 @@ namespace OpenRA
|
||||
? MapClassification.Unknown : Enum<MapClassification>.Parse(kv.Value);
|
||||
|
||||
IReadOnlyPackage package;
|
||||
var optional = name.StartsWith("~", StringComparison.Ordinal);
|
||||
var optional = name.StartsWith('~');
|
||||
if (optional)
|
||||
name = name[1..];
|
||||
|
||||
@@ -106,7 +107,7 @@ 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);
|
||||
@@ -140,7 +141,8 @@ namespace OpenRA
|
||||
LoadMapInternal(map, package, classification, mapGrid, oldMap, null);
|
||||
}
|
||||
|
||||
void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap, IEnumerable<List<MiniYamlNode>> modDataRules)
|
||||
void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap,
|
||||
IEnumerable<List<MiniYamlNode>> modDataRules)
|
||||
{
|
||||
IReadOnlyPackage mapPackage = null;
|
||||
try
|
||||
@@ -153,6 +155,9 @@ namespace OpenRA
|
||||
var uid = Map.ComputeUID(mapPackage);
|
||||
previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type, modDataRules);
|
||||
|
||||
// Freeing the package to save memory if there is a lot of Maps
|
||||
previews[uid].DisposePackage();
|
||||
|
||||
if (oldMap != uid)
|
||||
{
|
||||
LastModifiedMap = uid;
|
||||
@@ -190,13 +195,13 @@ 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))
|
||||
@@ -223,7 +228,8 @@ namespace OpenRA
|
||||
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 +239,67 @@ namespace OpenRA
|
||||
.ToList();
|
||||
|
||||
foreach (var uid in queryUids)
|
||||
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null);
|
||||
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null, null);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var client = HttpClientFactory.Create();
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
// Limit each query to 50 maps at a time to avoid request size limits
|
||||
for (var i = 0; i < queryUids.Count; i += 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);
|
||||
await using (var resultStream = await client.GetStreamAsync(url))
|
||||
{
|
||||
using (var resultReader = new StreamReader(resultStream))
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var line = await resultReader.ReadLineAsync();
|
||||
if (line == null || !line.StartsWith('\t'))
|
||||
{
|
||||
var yaml = MiniYaml.FromString(buffer.ToString(), url, stringPool: stringPool);
|
||||
buffer.Clear();
|
||||
foreach (var kv in yaml)
|
||||
previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, modData.Manifest.MapCompatibility, mapDetailsReceived);
|
||||
|
||||
if (line == null)
|
||||
break;
|
||||
}
|
||||
|
||||
buffer.Append(line);
|
||||
buffer.Append('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var uid in batchUids)
|
||||
{
|
||||
var p = previews[uid];
|
||||
if (p.Status != MapStatus.DownloadAvailable)
|
||||
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
catch (Exception e)
|
||||
{
|
||||
var p = previews[uid];
|
||||
p.UpdateRemoteSearch(MapStatus.Unavailable, null);
|
||||
mapQueryFailed?.Invoke(p);
|
||||
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, null);
|
||||
mapQueryFailed?.Invoke(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,11 +311,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 +335,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)
|
||||
@@ -348,8 +377,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 +435,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)
|
||||
|
||||
@@ -86,7 +86,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.PackageName == mapAction.Key && x.Status == MapStatus.Available);
|
||||
if (map != null)
|
||||
{
|
||||
if (mapAction.Value == MapAction.Delete)
|
||||
|
||||
@@ -116,12 +116,12 @@ namespace OpenRA
|
||||
|
||||
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
|
||||
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; }
|
||||
|
||||
@@ -59,6 +59,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
|
||||
@@ -89,8 +90,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; }
|
||||
|
||||
@@ -120,13 +122,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 = Array.Empty<string>();
|
||||
if (FluentMessageDefinitions.Value != null)
|
||||
files = FieldLoader.GetValue<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)
|
||||
@@ -139,15 +160,22 @@ namespace OpenRA
|
||||
files = files.Append(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;
|
||||
}
|
||||
}
|
||||
@@ -172,7 +200,19 @@ namespace OpenRA
|
||||
readonly ModData modData;
|
||||
|
||||
public readonly string Uid;
|
||||
public IReadOnlyPackage Package { get; private set; }
|
||||
public string PackageName { get; private set; }
|
||||
IReadOnlyPackage package;
|
||||
public IReadOnlyPackage Package
|
||||
{
|
||||
get
|
||||
{
|
||||
package ??= parentPackage.OpenPackage(PackageName, modData.ModFiles);
|
||||
return package;
|
||||
}
|
||||
|
||||
private set => package = value;
|
||||
}
|
||||
|
||||
IReadOnlyPackage parentPackage;
|
||||
|
||||
volatile InnerData innerData;
|
||||
@@ -204,16 +244,31 @@ namespace OpenRA
|
||||
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;
|
||||
@@ -284,7 +339,7 @@ namespace OpenRA
|
||||
cache = modData.MapCache;
|
||||
|
||||
Uid = map.Uid;
|
||||
Package = map.Package;
|
||||
PackageName = map.Package.Name;
|
||||
|
||||
var mapPlayers = new MapPlayers(map.PlayerDefinitions);
|
||||
var spawns = new List<CPos>();
|
||||
@@ -315,7 +370,7 @@ namespace OpenRA
|
||||
innerData.SetCustomRules(modData, this, new Dictionary<string, MiniYaml>()
|
||||
{
|
||||
{ "Rules", map.RuleDefinitions },
|
||||
{ "Translations", map.TranslationDefinitions },
|
||||
{ "FluentMessages", map.FluentMessageDefinitions },
|
||||
{ "Weapons", map.WeaponDefinitions },
|
||||
{ "Voices", map.VoiceDefinitions },
|
||||
{ "Music", map.MusicDefinitions },
|
||||
@@ -325,7 +380,8 @@ namespace OpenRA
|
||||
}, null);
|
||||
}
|
||||
|
||||
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType, IEnumerable<List<MiniYamlNode>> modDataRules)
|
||||
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification,
|
||||
string[] mapCompatibility, MapGridType gridType, IEnumerable<List<MiniYamlNode>> modDataRules)
|
||||
{
|
||||
Dictionary<string, MiniYaml> yaml;
|
||||
using (var yamlStream = p.GetStream("map.yaml"))
|
||||
@@ -333,10 +389,10 @@ namespace OpenRA
|
||||
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;
|
||||
PackageName = p.Name;
|
||||
parentPackage = parent;
|
||||
|
||||
var newData = innerData.Clone();
|
||||
@@ -427,7 +483,7 @@ namespace OpenRA
|
||||
innerData = newData;
|
||||
}
|
||||
|
||||
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action<MapPreview> parseMetadata = null)
|
||||
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, string[] mapCompatibility, Action<MapPreview> parseMetadata = null)
|
||||
{
|
||||
var newData = innerData.Clone();
|
||||
newData.Status = status;
|
||||
@@ -474,11 +530,19 @@ 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 (!mapCompatibility.Contains(r.game_mod))
|
||||
newData.Status = MapStatus.Unavailable;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -577,10 +641,15 @@ namespace OpenRA
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Package != null)
|
||||
DisposePackage();
|
||||
}
|
||||
|
||||
public void DisposePackage()
|
||||
{
|
||||
if (package != null)
|
||||
{
|
||||
Package.Dispose();
|
||||
Package = null;
|
||||
package.Dispose();
|
||||
package = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,11 +696,11 @@ namespace OpenRA
|
||||
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,6 +9,7 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA
|
||||
@@ -53,5 +54,15 @@ namespace OpenRA
|
||||
{
|
||||
return Bounds.Contains(uv.U, uv.V);
|
||||
}
|
||||
|
||||
public int IndexOf(T value, int startIndex)
|
||||
{
|
||||
return Array.IndexOf(Entries, value, startIndex);
|
||||
}
|
||||
|
||||
public void SetAll(T value)
|
||||
{
|
||||
Array.Fill(Entries, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,12 +93,12 @@ namespace OpenRA
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
u += 1;
|
||||
u++;
|
||||
|
||||
// Check for column overflow
|
||||
if (u > r.BottomRight.U)
|
||||
{
|
||||
v += 1;
|
||||
v++;
|
||||
u = r.TopLeft.U;
|
||||
|
||||
// Check for row overflow
|
||||
@@ -118,8 +118,8 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public PPos Current { get; private set; }
|
||||
object IEnumerator.Current => Current;
|
||||
public void Dispose() { }
|
||||
readonly object IEnumerator.Current => Current;
|
||||
public readonly void Dispose() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,18 +20,36 @@ namespace OpenRA
|
||||
{
|
||||
public static class MiniYamlExts
|
||||
{
|
||||
public static void WriteToFile(this List<MiniYamlNode> y, string filename)
|
||||
public static void WriteToFile(this IEnumerable<MiniYamlNode> y, string filename)
|
||||
{
|
||||
File.WriteAllLines(filename, y.ToLines().Select(x => x.TrimEnd()).ToArray());
|
||||
}
|
||||
|
||||
public static string WriteToString(this List<MiniYamlNode> y)
|
||||
public static string WriteToString(this IEnumerable<MiniYamlNode> y)
|
||||
{
|
||||
// Remove all trailing newlines and restore the final EOF newline
|
||||
return y.ToLines().JoinWith("\n").TrimEnd('\n') + "\n";
|
||||
}
|
||||
|
||||
public static IEnumerable<string> ToLines(this List<MiniYamlNode> y)
|
||||
public static IEnumerable<string> ToLines(this IEnumerable<MiniYamlNode> y)
|
||||
{
|
||||
foreach (var kv in y)
|
||||
foreach (var line in kv.Value.ToLines(kv.Key, kv.Comment))
|
||||
yield return line;
|
||||
}
|
||||
|
||||
public static void WriteToFile(this IEnumerable<MiniYamlNodeBuilder> y, string filename)
|
||||
{
|
||||
File.WriteAllLines(filename, y.ToLines().Select(x => x.TrimEnd()).ToArray());
|
||||
}
|
||||
|
||||
public static string WriteToString(this IEnumerable<MiniYamlNodeBuilder> y)
|
||||
{
|
||||
// Remove all trailing newlines and restore the final EOF newline
|
||||
return y.ToLines().JoinWith("\n").TrimEnd('\n') + "\n";
|
||||
}
|
||||
|
||||
public static IEnumerable<string> ToLines(this IEnumerable<MiniYamlNodeBuilder> y)
|
||||
{
|
||||
foreach (var kv in y)
|
||||
foreach (var line in kv.Value.ToLines(kv.Key, kv.Comment))
|
||||
@@ -43,22 +61,29 @@ namespace OpenRA
|
||||
{
|
||||
public readonly struct SourceLocation
|
||||
{
|
||||
public readonly string Filename;
|
||||
public readonly string Name;
|
||||
public readonly int Line;
|
||||
|
||||
public SourceLocation(string filename, int line)
|
||||
public SourceLocation(string name, int line)
|
||||
{
|
||||
Filename = filename;
|
||||
Name = name;
|
||||
Line = line;
|
||||
}
|
||||
|
||||
public override string ToString() { return $"{Filename}:{Line}"; }
|
||||
public override string ToString() { return $"{Name}:{Line}"; }
|
||||
}
|
||||
|
||||
public SourceLocation Location;
|
||||
public string Key;
|
||||
public MiniYaml Value;
|
||||
public string Comment;
|
||||
public readonly SourceLocation Location;
|
||||
public readonly string Key;
|
||||
public readonly MiniYaml Value;
|
||||
public readonly string Comment;
|
||||
|
||||
public MiniYamlNode WithValue(MiniYaml value)
|
||||
{
|
||||
if (Value == value)
|
||||
return this;
|
||||
return new MiniYamlNode(Key, value, Comment, Location);
|
||||
}
|
||||
|
||||
public MiniYamlNode(string k, MiniYaml v, string c = null)
|
||||
{
|
||||
@@ -74,26 +99,15 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public MiniYamlNode(string k, string v, string c = null)
|
||||
: this(k, v, c, null) { }
|
||||
: this(k, new MiniYaml(v, Enumerable.Empty<MiniYamlNode>()), c) { }
|
||||
|
||||
public MiniYamlNode(string k, string v, List<MiniYamlNode> n)
|
||||
public MiniYamlNode(string k, string v, IEnumerable<MiniYamlNode> n)
|
||||
: this(k, new MiniYaml(v, n), null) { }
|
||||
|
||||
public MiniYamlNode(string k, string v, string c, List<MiniYamlNode> n)
|
||||
: this(k, new MiniYaml(v, n), c) { }
|
||||
|
||||
public MiniYamlNode(string k, string v, string c, List<MiniYamlNode> n, SourceLocation loc)
|
||||
: this(k, new MiniYaml(v, n), c, loc) { }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{{YamlNode: {Key} @ {Location}}}";
|
||||
}
|
||||
|
||||
public MiniYamlNode Clone()
|
||||
{
|
||||
return new MiniYamlNode(Key, Value.Clone(), Comment, Location);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MiniYaml
|
||||
@@ -101,15 +115,59 @@ namespace OpenRA
|
||||
const int SpacesPerLevel = 4;
|
||||
static readonly Func<string, string> StringIdentity = s => s;
|
||||
static readonly Func<MiniYaml, MiniYaml> MiniYamlIdentity = my => my;
|
||||
public string Value;
|
||||
public List<MiniYamlNode> Nodes;
|
||||
static readonly Dictionary<string, MiniYamlNode> ConflictScratch = new();
|
||||
|
||||
public MiniYaml Clone()
|
||||
public readonly string Value;
|
||||
public readonly ImmutableArray<MiniYamlNode> Nodes;
|
||||
|
||||
public MiniYaml WithValue(string value)
|
||||
{
|
||||
var clonedNodes = new List<MiniYamlNode>(Nodes.Count);
|
||||
if (Value == value)
|
||||
return this;
|
||||
return new MiniYaml(value, Nodes);
|
||||
}
|
||||
|
||||
public MiniYaml WithNodes(IEnumerable<MiniYamlNode> nodes)
|
||||
{
|
||||
if (nodes is ImmutableArray<MiniYamlNode> n && Nodes == n)
|
||||
return this;
|
||||
return new MiniYaml(Value, nodes);
|
||||
}
|
||||
|
||||
public MiniYaml WithNodesAppended(IEnumerable<MiniYamlNode> nodes)
|
||||
{
|
||||
var newNodes = Nodes.AddRange(nodes);
|
||||
if (Nodes == newNodes)
|
||||
return this;
|
||||
return new MiniYaml(Value, newNodes);
|
||||
}
|
||||
|
||||
public MiniYamlNode NodeWithKey(string key)
|
||||
{
|
||||
var result = NodeWithKeyOrDefault(key);
|
||||
if (result == null)
|
||||
throw new InvalidDataException($"No node with key '{key}'");
|
||||
return result;
|
||||
}
|
||||
|
||||
public MiniYamlNode NodeWithKeyOrDefault(string key)
|
||||
{
|
||||
// PERF: Avoid LINQ.
|
||||
var first = true;
|
||||
MiniYamlNode result = null;
|
||||
foreach (var node in Nodes)
|
||||
clonedNodes.Add(node.Clone());
|
||||
return new MiniYaml(Value, clonedNodes);
|
||||
{
|
||||
if (node.Key != key)
|
||||
continue;
|
||||
|
||||
if (!first)
|
||||
throw new InvalidDataException($"Duplicate key '{node.Key}' in {node.Location}");
|
||||
|
||||
first = false;
|
||||
result = node;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Dictionary<string, MiniYaml> ToDictionary()
|
||||
@@ -125,7 +183,7 @@ namespace OpenRA
|
||||
public Dictionary<TKey, TElement> ToDictionary<TKey, TElement>(
|
||||
Func<string, TKey> keySelector, Func<MiniYaml, TElement> elementSelector)
|
||||
{
|
||||
var ret = new Dictionary<TKey, TElement>(Nodes.Count);
|
||||
var ret = new Dictionary<TKey, TElement>(Nodes.Length);
|
||||
foreach (var y in Nodes)
|
||||
{
|
||||
var key = keySelector(y.Key);
|
||||
@@ -138,28 +196,27 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public MiniYaml(string value)
|
||||
: this(value, null) { }
|
||||
: this(value, Enumerable.Empty<MiniYamlNode>()) { }
|
||||
|
||||
public MiniYaml(string value, List<MiniYamlNode> nodes)
|
||||
public MiniYaml(string value, IEnumerable<MiniYamlNode> nodes)
|
||||
{
|
||||
Value = value;
|
||||
Nodes = nodes ?? new List<MiniYamlNode>();
|
||||
Nodes = ImmutableArray.CreateRange(nodes);
|
||||
}
|
||||
|
||||
public static List<MiniYamlNode> NodesOrEmpty(MiniYaml y, string s)
|
||||
static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string name, bool discardCommentsAndWhitespace, HashSet<string> stringPool)
|
||||
{
|
||||
var nd = y.ToDictionary();
|
||||
return nd.TryGetValue(s, out var v) ? v.Nodes : new List<MiniYamlNode>();
|
||||
}
|
||||
// YAML config often contains repeated strings for key, values, comments.
|
||||
// Pool these strings so we only need one copy of each unique string.
|
||||
// This saves on long-term memory usage as parsed values can often live a long time.
|
||||
// A caller can also provide a pool as input, allowing de-duplication across multiple parses.
|
||||
stringPool ??= new HashSet<string>();
|
||||
|
||||
static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string filename, bool discardCommentsAndWhitespace, Dictionary<string, string> stringPool)
|
||||
{
|
||||
stringPool ??= new Dictionary<string, string>();
|
||||
|
||||
var levels = new List<List<MiniYamlNode>>
|
||||
var result = new List<List<MiniYamlNode>>
|
||||
{
|
||||
new List<MiniYamlNode>()
|
||||
new()
|
||||
};
|
||||
var parsedLines = new List<(int Level, string Key, string Value, string Comment, MiniYamlNode.SourceLocation Location)>();
|
||||
|
||||
var lineNo = 0;
|
||||
foreach (var ll in lines)
|
||||
@@ -175,7 +232,7 @@ namespace OpenRA
|
||||
ReadOnlySpan<char> key = default;
|
||||
ReadOnlySpan<char> value = default;
|
||||
ReadOnlySpan<char> comment = default;
|
||||
var location = new MiniYamlNode.SourceLocation(filename, lineNo);
|
||||
var location = new MiniYamlNode.SourceLocation(name, lineNo);
|
||||
|
||||
if (line.Length > 0)
|
||||
{
|
||||
@@ -206,15 +263,6 @@ namespace OpenRA
|
||||
}
|
||||
}
|
||||
|
||||
if (levels.Count <= level)
|
||||
throw new YamlException($"Bad indent in miniyaml at {location}");
|
||||
|
||||
while (levels.Count > level + 1)
|
||||
{
|
||||
levels[^1].TrimExcess();
|
||||
levels.RemoveAt(levels.Count - 1);
|
||||
}
|
||||
|
||||
// Extract key, value, comment from line as `<key>: <value>#<comment>`
|
||||
// The # character is allowed in the value if escaped (\#).
|
||||
// Leading and trailing whitespace is always trimmed from keys.
|
||||
@@ -236,7 +284,7 @@ namespace OpenRA
|
||||
if (commentStart < 0 && line[i] == '#' && (i == 0 || line[i - 1] != '\\'))
|
||||
{
|
||||
commentStart = i + 1;
|
||||
if (commentStart <= keyLength)
|
||||
if (i <= keyStart + keyLength)
|
||||
keyLength = i - keyStart;
|
||||
else
|
||||
valueLength = i - valueStart;
|
||||
@@ -274,46 +322,81 @@ namespace OpenRA
|
||||
|
||||
if (!key.IsEmpty || !discardCommentsAndWhitespace)
|
||||
{
|
||||
if (parsedLines.Count > 0 && parsedLines[^1].Level < level - 1)
|
||||
throw new YamlException($"Bad indent in miniyaml at {location}");
|
||||
|
||||
while (parsedLines.Count > 0 && parsedLines[^1].Level > level)
|
||||
BuildCompletedSubNode(level);
|
||||
|
||||
var keyString = key.IsEmpty ? null : key.ToString();
|
||||
var valueString = value.IsEmpty ? null : value.ToString();
|
||||
|
||||
// Note: We need to support empty comments here to ensure that empty comments
|
||||
// (i.e. a lone # at the end of a line) can be correctly re-serialized
|
||||
var commentString = comment == default ? null : comment.ToString();
|
||||
var commentString = comment == ReadOnlySpan<char>.Empty ? null : comment.ToString();
|
||||
|
||||
keyString = keyString == null ? null : stringPool.GetOrAdd(keyString, keyString);
|
||||
valueString = valueString == null ? null : stringPool.GetOrAdd(valueString, valueString);
|
||||
commentString = commentString == null ? null : stringPool.GetOrAdd(commentString, commentString);
|
||||
keyString = keyString == null ? null : stringPool.GetOrAdd(keyString);
|
||||
valueString = valueString == null ? null : stringPool.GetOrAdd(valueString);
|
||||
commentString = commentString == null ? null : stringPool.GetOrAdd(commentString);
|
||||
|
||||
var nodes = new List<MiniYamlNode>();
|
||||
levels[level].Add(new MiniYamlNode(keyString, valueString, commentString, nodes, location));
|
||||
|
||||
levels.Add(nodes);
|
||||
parsedLines.Add((level, keyString, valueString, commentString, location));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var nodes in levels)
|
||||
nodes.TrimExcess();
|
||||
if (parsedLines.Count > 0)
|
||||
BuildCompletedSubNode(0);
|
||||
|
||||
return levels[0];
|
||||
return result[0];
|
||||
|
||||
void BuildCompletedSubNode(int level)
|
||||
{
|
||||
var lastLevel = parsedLines[^1].Level;
|
||||
while (lastLevel >= result.Count)
|
||||
result.Add(new List<MiniYamlNode>());
|
||||
|
||||
while (parsedLines.Count > 0 && parsedLines[^1].Level >= level)
|
||||
{
|
||||
var parent = parsedLines[^1];
|
||||
var startOfRange = parsedLines.Count - 1;
|
||||
while (startOfRange > 0 && parsedLines[startOfRange - 1].Level == parent.Level)
|
||||
startOfRange--;
|
||||
|
||||
for (var i = startOfRange; i < parsedLines.Count - 1; i++)
|
||||
{
|
||||
var sibling = parsedLines[i];
|
||||
result[parent.Level].Add(
|
||||
new MiniYamlNode(sibling.Key, new MiniYaml(sibling.Value), sibling.Comment, sibling.Location));
|
||||
}
|
||||
|
||||
var childNodes = parent.Level + 1 < result.Count ? result[parent.Level + 1] : null;
|
||||
result[parent.Level].Add(new MiniYamlNode(
|
||||
parent.Key,
|
||||
new MiniYaml(parent.Value, childNodes ?? Enumerable.Empty<MiniYamlNode>()),
|
||||
parent.Comment,
|
||||
parent.Location));
|
||||
childNodes?.Clear();
|
||||
|
||||
parsedLines.RemoveRange(startOfRange, parsedLines.Count - startOfRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
|
||||
public static List<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
|
||||
{
|
||||
return FromStream(File.OpenRead(path), path, discardCommentsAndWhitespace, stringPool);
|
||||
}
|
||||
|
||||
public static List<MiniYamlNode> FromStream(Stream s, string fileName = "<no filename available>", bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
|
||||
public static List<MiniYamlNode> FromStream(Stream s, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
|
||||
{
|
||||
return FromLines(s.ReadAllLinesAsMemory(), fileName, discardCommentsAndWhitespace, stringPool);
|
||||
return FromLines(s.ReadAllLinesAsMemory(), name, discardCommentsAndWhitespace, stringPool);
|
||||
}
|
||||
|
||||
public static List<MiniYamlNode> FromString(string text, string fileName = "<no filename available>", bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
|
||||
public static List<MiniYamlNode> FromString(string text, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
|
||||
{
|
||||
return FromLines(text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(s => s.AsMemory()), fileName, discardCommentsAndWhitespace, stringPool);
|
||||
return FromLines(text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(s => s.AsMemory()), name, discardCommentsAndWhitespace, stringPool);
|
||||
}
|
||||
|
||||
public static List<MiniYamlNode> Merge(IEnumerable<List<MiniYamlNode>> sources)
|
||||
public static List<MiniYamlNode> Merge(IEnumerable<IReadOnlyCollection<MiniYamlNode>> sources)
|
||||
{
|
||||
var sourcesList = sources.ToList();
|
||||
if (sourcesList.Count == 0)
|
||||
@@ -336,28 +419,41 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
// Resolve any top-level removals (e.g. removing whole actor blocks)
|
||||
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)).ToList());
|
||||
return ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty);
|
||||
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)));
|
||||
var result = ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty);
|
||||
return result as List<MiniYamlNode> ?? result.ToList();
|
||||
}
|
||||
|
||||
static void MergeIntoResolved(MiniYamlNode overrideNode, List<MiniYamlNode> existingNodes, HashSet<string> existingNodeKeys,
|
||||
Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
|
||||
{
|
||||
if (existingNodeKeys.Add(overrideNode.Key))
|
||||
var existingNodeIndex = -1;
|
||||
MiniYamlNode existingNode = null;
|
||||
if (!existingNodeKeys.Add(overrideNode.Key))
|
||||
{
|
||||
existingNodes.Add(overrideNode.Clone());
|
||||
return;
|
||||
existingNodeIndex = IndexOfKey(existingNodes, overrideNode.Key);
|
||||
existingNode = existingNodes[existingNodeIndex];
|
||||
}
|
||||
|
||||
var existingNode = existingNodes.Find(n => n.Key == overrideNode.Key);
|
||||
existingNode.Value = MergePartial(existingNode.Value, overrideNode.Value);
|
||||
existingNode.Value.Nodes = ResolveInherits(existingNode.Value, tree, inherited);
|
||||
var value = MergePartial(existingNode?.Value, overrideNode.Value);
|
||||
var nodes = ResolveInherits(value, tree, inherited);
|
||||
if (!value.Nodes.SequenceEqual(nodes))
|
||||
value = value.WithNodes(nodes);
|
||||
|
||||
if (existingNode != null)
|
||||
existingNodes[existingNodeIndex] = existingNode.WithValue(value);
|
||||
else
|
||||
existingNodes.Add(overrideNode.WithValue(value));
|
||||
}
|
||||
|
||||
static List<MiniYamlNode> ResolveInherits(MiniYaml node, Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
|
||||
static IReadOnlyCollection<MiniYamlNode> ResolveInherits(
|
||||
MiniYaml node, Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
|
||||
{
|
||||
var resolved = new List<MiniYamlNode>(node.Nodes.Count);
|
||||
var resolvedKeys = new HashSet<string>(node.Nodes.Count);
|
||||
if (node.Nodes.Length == 0)
|
||||
return node.Nodes;
|
||||
|
||||
var resolved = new List<MiniYamlNode>(node.Nodes.Length);
|
||||
var resolvedKeys = new HashSet<string>(node.Nodes.Length);
|
||||
|
||||
foreach (var n in node.Nodes)
|
||||
{
|
||||
@@ -373,13 +469,14 @@ namespace OpenRA
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
throw new YamlException($"{n.Location}: Parent type `{n.Value.Value}` was already inherited by this yaml tree at {inherited[n.Value.Value]} (note: may be from a derived tree)");
|
||||
throw new YamlException(
|
||||
$"{n.Location}: Parent type `{n.Value.Value}` was already inherited by this yaml tree at {inherited[n.Value.Value]} (note: may be from a derived tree)");
|
||||
}
|
||||
|
||||
foreach (var r in ResolveInherits(parent, tree, inherited))
|
||||
MergeIntoResolved(r, resolved, resolvedKeys, tree, inherited);
|
||||
}
|
||||
else if (n.Key.StartsWith("-", StringComparison.Ordinal))
|
||||
else if (n.Key.StartsWith('-'))
|
||||
{
|
||||
var removed = n.Key[1..];
|
||||
if (resolved.RemoveAll(r => r.Key == removed) == 0)
|
||||
@@ -390,7 +487,6 @@ namespace OpenRA
|
||||
MergeIntoResolved(n, resolved, resolvedKeys, tree, inherited);
|
||||
}
|
||||
|
||||
resolved.TrimExcess();
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@@ -398,8 +494,11 @@ namespace OpenRA
|
||||
/// Merges any duplicate keys that are defined within the same set of nodes.
|
||||
/// Does not resolve inheritance or node removals.
|
||||
/// </summary>
|
||||
static List<MiniYamlNode> MergeSelfPartial(List<MiniYamlNode> existingNodes)
|
||||
static IReadOnlyCollection<MiniYamlNode> MergeSelfPartial(IReadOnlyCollection<MiniYamlNode> existingNodes)
|
||||
{
|
||||
if (existingNodes.Count == 0)
|
||||
return existingNodes;
|
||||
|
||||
var keys = new HashSet<string>(existingNodes.Count);
|
||||
var ret = new List<MiniYamlNode>(existingNodes.Count);
|
||||
foreach (var n in existingNodes)
|
||||
@@ -409,19 +508,26 @@ namespace OpenRA
|
||||
else
|
||||
{
|
||||
// Node with the same key has already been added: merge new node over the existing one
|
||||
var original = ret.First(r => r.Key == n.Key);
|
||||
original.Value = MergePartial(original.Value, n.Value);
|
||||
var originalIndex = IndexOfKey(ret, n.Key);
|
||||
var original = ret[originalIndex];
|
||||
ret[originalIndex] = original.WithValue(MergePartial(original.Value, n.Value));
|
||||
}
|
||||
}
|
||||
|
||||
ret.TrimExcess();
|
||||
return ret;
|
||||
}
|
||||
|
||||
static MiniYaml MergePartial(MiniYaml existingNodes, MiniYaml overrideNodes)
|
||||
{
|
||||
existingNodes?.Nodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
|
||||
overrideNodes?.Nodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
|
||||
lock (ConflictScratch)
|
||||
{
|
||||
// PERF: Reuse ConflictScratch for all conflict checks to avoid allocations.
|
||||
existingNodes?.Nodes.IntoDictionaryWithConflictLog(
|
||||
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
|
||||
overrideNodes?.Nodes.IntoDictionaryWithConflictLog(
|
||||
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
|
||||
ConflictScratch.Clear();
|
||||
}
|
||||
|
||||
if (existingNodes == null)
|
||||
return overrideNodes;
|
||||
@@ -432,7 +538,7 @@ namespace OpenRA
|
||||
return new MiniYaml(overrideNodes.Value ?? existingNodes.Value, MergePartial(existingNodes.Nodes, overrideNodes.Nodes));
|
||||
}
|
||||
|
||||
static List<MiniYamlNode> MergePartial(List<MiniYamlNode> existingNodes, List<MiniYamlNode> overrideNodes)
|
||||
static IReadOnlyCollection<MiniYamlNode> MergePartial(IReadOnlyCollection<MiniYamlNode> existingNodes, IReadOnlyCollection<MiniYamlNode> overrideNodes)
|
||||
{
|
||||
if (existingNodes.Count == 0)
|
||||
return overrideNodes;
|
||||
@@ -452,7 +558,7 @@ namespace OpenRA
|
||||
{
|
||||
// Append Removal nodes to the result.
|
||||
// Therefore: we know the remainder of the method deals with a plain node.
|
||||
if (node.Key.StartsWith("-", StringComparison.Ordinal))
|
||||
if (node.Key.StartsWith('-'))
|
||||
{
|
||||
ret.Add(node);
|
||||
return;
|
||||
@@ -468,9 +574,8 @@ namespace OpenRA
|
||||
// A Removal node is closer than the previous node.
|
||||
// We should not merge the new node, as the data being merged will jump before the Removal.
|
||||
// Instead, append it so the previous node is applied, then removed, then the new node is applied.
|
||||
var removalKey = $"-{node.Key}";
|
||||
var previousNodeIndex = ret.FindLastIndex(n => n.Key == node.Key);
|
||||
var previousRemovalNodeIndex = ret.FindLastIndex(n => n.Key == removalKey);
|
||||
var previousNodeIndex = LastIndexOfKey(ret, node.Key);
|
||||
var previousRemovalNodeIndex = LastIndexOfKey(ret, $"-{node.Key}");
|
||||
if (previousRemovalNodeIndex != -1 && previousRemovalNodeIndex > previousNodeIndex)
|
||||
{
|
||||
ret.Add(node);
|
||||
@@ -479,13 +584,30 @@ namespace OpenRA
|
||||
|
||||
// A previous node is present with no intervening Removal.
|
||||
// We should merge the new one into it, in place.
|
||||
ret[previousNodeIndex] = new MiniYamlNode(node.Key, MergePartial(ret[previousNodeIndex].Value, node.Value), node.Comment, node.Location);
|
||||
ret[previousNodeIndex] = node.WithValue(MergePartial(ret[previousNodeIndex].Value, node.Value));
|
||||
}
|
||||
|
||||
ret.TrimExcess();
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int IndexOfKey(List<MiniYamlNode> nodes, string key)
|
||||
{
|
||||
// PERF: Avoid LINQ.
|
||||
for (var i = 0; i < nodes.Count; i++)
|
||||
if (nodes[i].Key == key)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int LastIndexOfKey(List<MiniYamlNode> nodes, string key)
|
||||
{
|
||||
// PERF: Avoid LINQ.
|
||||
for (var i = nodes.Count - 1; i >= 0; i--)
|
||||
if (nodes[i].Key == key)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public IEnumerable<string> ToLines(string key, string comment = null)
|
||||
{
|
||||
var hasKey = !string.IsNullOrEmpty(key);
|
||||
@@ -508,14 +630,100 @@ namespace OpenRA
|
||||
files = files.Append(mapFiles);
|
||||
}
|
||||
|
||||
var yaml = files.Select(s => FromStream(fileSystem.Open(s), s));
|
||||
if (mapRules != null && mapRules.Nodes.Count > 0)
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
IEnumerable<IReadOnlyCollection<MiniYamlNode>> yaml = files.Select(s => FromStream(fileSystem.Open(s), s, stringPool: stringPool));
|
||||
if (mapRules != null && mapRules.Nodes.Length > 0)
|
||||
yaml = yaml.Append(mapRules.Nodes);
|
||||
|
||||
return Merge(yaml);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MiniYamlNodeBuilder
|
||||
{
|
||||
public MiniYamlNode.SourceLocation Location;
|
||||
public string Key;
|
||||
public MiniYamlBuilder Value;
|
||||
public string Comment;
|
||||
|
||||
public MiniYamlNodeBuilder(MiniYamlNode node)
|
||||
{
|
||||
Location = node.Location;
|
||||
Key = node.Key;
|
||||
Value = new MiniYamlBuilder(node.Value);
|
||||
Comment = node.Comment;
|
||||
}
|
||||
|
||||
public MiniYamlNodeBuilder(string k, MiniYamlBuilder v, string c = null)
|
||||
{
|
||||
Key = k;
|
||||
Value = v;
|
||||
Comment = c;
|
||||
}
|
||||
|
||||
public MiniYamlNodeBuilder(string k, MiniYamlBuilder v, string c, MiniYamlNode.SourceLocation loc)
|
||||
: this(k, v, c)
|
||||
{
|
||||
Location = loc;
|
||||
}
|
||||
|
||||
public MiniYamlNodeBuilder(string k, string v, string c = null)
|
||||
: this(k, new MiniYamlBuilder(v, null), c) { }
|
||||
|
||||
public MiniYamlNodeBuilder(string k, string v, List<MiniYamlNode> n)
|
||||
: this(k, new MiniYamlBuilder(v, n), null) { }
|
||||
|
||||
public MiniYamlNode Build()
|
||||
{
|
||||
return new MiniYamlNode(Key, Value.Build(), Comment, Location);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MiniYamlBuilder
|
||||
{
|
||||
public string Value;
|
||||
public List<MiniYamlNodeBuilder> Nodes;
|
||||
|
||||
public MiniYamlBuilder(MiniYaml yaml)
|
||||
{
|
||||
Value = yaml.Value;
|
||||
Nodes = yaml.Nodes.Select(n => new MiniYamlNodeBuilder(n)).ToList();
|
||||
}
|
||||
|
||||
public MiniYamlBuilder(string value)
|
||||
: this(value, null) { }
|
||||
|
||||
public MiniYamlBuilder(string value, List<MiniYamlNode> nodes)
|
||||
{
|
||||
Value = value;
|
||||
Nodes = nodes == null ? new List<MiniYamlNodeBuilder>() : nodes.ConvertAll(x => new MiniYamlNodeBuilder(x));
|
||||
}
|
||||
|
||||
public MiniYaml Build()
|
||||
{
|
||||
return new MiniYaml(Value, Nodes.Select(n => n.Build()));
|
||||
}
|
||||
|
||||
public IEnumerable<string> ToLines(string key, string comment = null)
|
||||
{
|
||||
var hasKey = !string.IsNullOrEmpty(key);
|
||||
var hasValue = !string.IsNullOrEmpty(Value);
|
||||
var hasComment = comment != null;
|
||||
yield return (hasKey ? key + ":" : "")
|
||||
+ (hasValue ? " " + Value.Replace("#", "\\#") : "")
|
||||
+ (hasComment ? (hasKey || hasValue ? " " : "") + "#" + comment : "");
|
||||
|
||||
if (Nodes != null)
|
||||
foreach (var line in Nodes.ToLines())
|
||||
yield return "\t" + line;
|
||||
}
|
||||
|
||||
public MiniYamlNodeBuilder NodeWithKeyOrDefault(string key)
|
||||
{
|
||||
return Nodes.SingleOrDefault(n => n.Key == key);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class YamlException : Exception
|
||||
{
|
||||
|
||||
@@ -33,9 +33,10 @@ namespace OpenRA
|
||||
public readonly ISpriteLoader[] SpriteLoaders;
|
||||
public readonly ITerrainLoader TerrainLoader;
|
||||
public readonly ISpriteSequenceLoader SpriteSequenceLoader;
|
||||
public readonly IModelSequenceLoader ModelSequenceLoader;
|
||||
public readonly IVideoLoader[] VideoLoaders;
|
||||
public readonly HotkeyManager Hotkeys;
|
||||
public readonly IFileSystemLoader FileSystemLoader;
|
||||
|
||||
public ILoadScreen LoadScreen { get; }
|
||||
public CursorProvider CursorProvider { get; private set; }
|
||||
public FS ModFiles;
|
||||
@@ -55,11 +56,17 @@ namespace OpenRA
|
||||
Manifest = new Manifest(mod.Id, mod.Package);
|
||||
ObjectCreator = new ObjectCreator(Manifest, mods);
|
||||
PackageLoaders = ObjectCreator.GetLoaders<IPackageLoader>(Manifest.PackageFormats, "package");
|
||||
|
||||
ModFiles = new FS(mod.Id, mods, PackageLoaders);
|
||||
ModFiles.LoadFromManifest(Manifest);
|
||||
|
||||
FileSystemLoader = ObjectCreator.GetLoader<IFileSystemLoader>(Manifest.FileSystem.Value, "filesystem");
|
||||
FieldLoader.Load(FileSystemLoader, Manifest.FileSystem);
|
||||
FileSystemLoader.Mount(ModFiles, ObjectCreator);
|
||||
ModFiles.TrimExcess();
|
||||
|
||||
Manifest.LoadCustomData(ObjectCreator);
|
||||
|
||||
FluentProvider.Initialize(this, DefaultFileSystem);
|
||||
|
||||
if (useLoadScreen)
|
||||
{
|
||||
LoadScreen = ObjectCreator.CreateObject<ILoadScreen>(Manifest.LoadScreen.Value);
|
||||
@@ -90,15 +97,6 @@ namespace OpenRA
|
||||
|
||||
SpriteSequenceLoader = (ISpriteSequenceLoader)sequenceCtor.Invoke(new[] { this });
|
||||
|
||||
var modelFormat = Manifest.Get<ModelSequenceFormat>();
|
||||
var modelLoader = ObjectCreator.FindType(modelFormat.Type + "Loader");
|
||||
var modelCtor = modelLoader?.GetConstructor(new[] { typeof(ModData) });
|
||||
if (modelLoader == null || !modelLoader.GetInterfaces().Contains(typeof(IModelSequenceLoader)) || modelCtor == null)
|
||||
throw new InvalidOperationException($"Unable to find a model loader for type '{modelFormat.Type}'.");
|
||||
|
||||
ModelSequenceLoader = (IModelSequenceLoader)modelCtor.Invoke(new[] { this });
|
||||
ModelSequenceLoader.OnMissingModelError = s => Log.Write("debug", s);
|
||||
|
||||
Hotkeys = new HotkeyManager(ModFiles, Game.Settings.Keys, Manifest);
|
||||
|
||||
defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this));
|
||||
@@ -134,7 +132,7 @@ namespace OpenRA
|
||||
// horribly when you use ModData in unexpected ways.
|
||||
ChromeMetrics.Initialize(this);
|
||||
ChromeProvider.Initialize(this);
|
||||
TranslationProvider.Initialize(this, fileSystem);
|
||||
FluentProvider.Initialize(this, fileSystem);
|
||||
|
||||
Game.Sound.Initialize(SoundLoaders, fileSystem);
|
||||
|
||||
@@ -168,7 +166,8 @@ namespace OpenRA
|
||||
|
||||
public List<MiniYamlNode>[] GetRulesYaml()
|
||||
{
|
||||
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s)).ToArray();
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s, stringPool: stringPool)).ToArray();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -199,4 +198,9 @@ namespace OpenRA
|
||||
/// <summary>Called when the engine expects to connect to a server/replay or load the shellmap.</summary>
|
||||
void StartGame(Arguments args);
|
||||
}
|
||||
|
||||
public interface IFileSystemLoader
|
||||
{
|
||||
void Mount(FS fileSystem, ObjectCreator objectCreator);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,15 +260,15 @@ namespace OpenRA.Network
|
||||
try
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
ms.WriteArray(BitConverter.GetBytes(packet.Length));
|
||||
ms.WriteArray(packet);
|
||||
ms.Write(packet.Length);
|
||||
ms.Write(packet);
|
||||
|
||||
foreach (var s in queuedSyncPackets)
|
||||
{
|
||||
var q = OrderIO.SerializeSync(s);
|
||||
|
||||
ms.WriteArray(BitConverter.GetBytes(q.Length));
|
||||
ms.WriteArray(q);
|
||||
ms.Write(q.Length);
|
||||
ms.Write(q);
|
||||
|
||||
sentSync.Enqueue(s);
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Linguini.Shared.Types.Bundle;
|
||||
|
||||
namespace OpenRA.Network
|
||||
@@ -50,74 +48,75 @@ namespace OpenRA.Network
|
||||
}
|
||||
}
|
||||
|
||||
public class LocalizedMessage
|
||||
public class FluentMessage
|
||||
{
|
||||
public const int ProtocolVersion = 1;
|
||||
|
||||
public readonly string Key = string.Empty;
|
||||
|
||||
[FieldLoader.LoadUsing(nameof(LoadArguments))]
|
||||
public readonly FluentArgument[] Arguments = Array.Empty<FluentArgument>();
|
||||
|
||||
public string TranslatedText { get; }
|
||||
public readonly object[] Arguments;
|
||||
|
||||
static object LoadArguments(MiniYaml yaml)
|
||||
{
|
||||
var arguments = new List<FluentArgument>();
|
||||
var argumentsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Arguments");
|
||||
var arguments = new List<object>();
|
||||
var argumentsNode = yaml.NodeWithKeyOrDefault("Arguments");
|
||||
if (argumentsNode != null)
|
||||
{
|
||||
var regex = new Regex(@"Argument@\d+");
|
||||
foreach (var argument in argumentsNode.Value.Nodes)
|
||||
if (regex.IsMatch(argument.Key))
|
||||
arguments.Add(FieldLoader.Load<FluentArgument>(argument.Value));
|
||||
foreach (var argumentNode in argumentsNode.Value.Nodes)
|
||||
{
|
||||
var argument = FieldLoader.Load<FluentArgument>(argumentNode.Value);
|
||||
arguments.Add(argument.Key);
|
||||
if (argument.Type == FluentArgument.FluentArgumentType.Number)
|
||||
{
|
||||
if (!double.TryParse(argument.Value, out var number))
|
||||
Log.Write("debug", $"Failed to parse {argument.Value}");
|
||||
|
||||
arguments.Add(number);
|
||||
}
|
||||
else
|
||||
arguments.Add(argument.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return arguments.ToArray();
|
||||
}
|
||||
|
||||
public LocalizedMessage(MiniYaml yaml)
|
||||
public FluentMessage(MiniYaml yaml)
|
||||
{
|
||||
// Let the FieldLoader do the dirty work of loading the public fields.
|
||||
FieldLoader.Load(this, yaml);
|
||||
|
||||
var argumentDictionary = new Dictionary<string, object>();
|
||||
foreach (var argument in Arguments)
|
||||
{
|
||||
if (argument.Type == FluentArgument.FluentArgumentType.Number)
|
||||
{
|
||||
if (!double.TryParse(argument.Value, out var number))
|
||||
Log.Write("debug", $"Failed to parse {argument.Value}");
|
||||
|
||||
argumentDictionary.Add(argument.Key, number);
|
||||
}
|
||||
else
|
||||
argumentDictionary.Add(argument.Key, argument.Value);
|
||||
}
|
||||
|
||||
TranslatedText = TranslationProvider.GetString(Key, argumentDictionary);
|
||||
}
|
||||
|
||||
public static string Serialize(string key, Dictionary<string, object> arguments = null)
|
||||
public static string Serialize(string key, object[] args)
|
||||
{
|
||||
var root = new List<MiniYamlNode>
|
||||
{
|
||||
new MiniYamlNode("Protocol", ProtocolVersion.ToString()),
|
||||
new MiniYamlNode("Key", key)
|
||||
new("Protocol", ProtocolVersion.ToStringInvariant()),
|
||||
new("Key", key),
|
||||
};
|
||||
|
||||
if (arguments != null)
|
||||
if (args != null)
|
||||
{
|
||||
var argumentsNode = new MiniYaml("");
|
||||
var i = 0;
|
||||
foreach (var argument in arguments.Select(a => new FluentArgument(a.Key, a.Value)))
|
||||
argumentsNode.Nodes.Add(new MiniYamlNode("Argument@" + i++, FieldSaver.Save(argument)));
|
||||
var nodes = new List<MiniYamlNode>();
|
||||
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));
|
||||
|
||||
root.Add(new MiniYamlNode("Arguments", argumentsNode));
|
||||
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");
|
||||
|
||||
nodes.Add(new MiniYamlNode($"Argument@{i / 2}", FieldSaver.Save(new FluentArgument(argKey, argValue))));
|
||||
}
|
||||
|
||||
root.Add(new MiniYamlNode("Arguments", new MiniYaml("", nodes)));
|
||||
}
|
||||
|
||||
return new MiniYaml("", root)
|
||||
.ToLines("LocalizedMessage")
|
||||
.ToLines("FluentMessage")
|
||||
.JoinWith("\n");
|
||||
}
|
||||
}
|
||||
@@ -122,10 +122,10 @@ namespace OpenRA.Network
|
||||
LastSyncFrame = rs.ReadInt32();
|
||||
lastSyncPacket = rs.ReadBytes(Order.SyncHashOrderLength);
|
||||
|
||||
var globalSettings = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
|
||||
var globalSettings = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:globalSettings");
|
||||
GlobalSettings = Session.Global.Deserialize(globalSettings[0].Value);
|
||||
|
||||
var slots = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
|
||||
var slots = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:slots");
|
||||
Slots = new Dictionary<string, Session.Slot>();
|
||||
foreach (var s in slots)
|
||||
{
|
||||
@@ -133,7 +133,7 @@ namespace OpenRA.Network
|
||||
Slots.Add(slot.PlayerReference, slot);
|
||||
}
|
||||
|
||||
var slotClients = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
|
||||
var slotClients = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:slotClients");
|
||||
SlotClients = new Dictionary<string, SlotClient>();
|
||||
foreach (var s in slotClients)
|
||||
{
|
||||
@@ -144,9 +144,9 @@ namespace OpenRA.Network
|
||||
if (rs.Position != traitDataOffset || rs.ReadInt32() != TraitDataMarker)
|
||||
throw new InvalidDataException("Invalid orasav file");
|
||||
|
||||
var traitData = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
|
||||
var traitData = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:traitData");
|
||||
foreach (var td in traitData)
|
||||
TraitData.Add(int.Parse(td.Key), td.Value);
|
||||
TraitData.Add(Exts.ParseInt32Invariant(td.Key), td.Value);
|
||||
|
||||
rs.Seek(0, SeekOrigin.Begin);
|
||||
ordersStream.Write(rs.ReadBytes(metadataOffset), 0, metadataOffset);
|
||||
@@ -226,10 +226,10 @@ namespace OpenRA.Network
|
||||
clientSlot = firstBotSlotIndex;
|
||||
}
|
||||
|
||||
ordersStream.WriteArray(BitConverter.GetBytes(data.Length + 8));
|
||||
ordersStream.WriteArray(BitConverter.GetBytes(frame));
|
||||
ordersStream.WriteArray(BitConverter.GetBytes(clientSlot));
|
||||
ordersStream.WriteArray(data);
|
||||
ordersStream.Write(data.Length + 8);
|
||||
ordersStream.Write(frame);
|
||||
ordersStream.Write(clientSlot);
|
||||
ordersStream.Write(data);
|
||||
LastOrdersFrame = frame;
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ namespace OpenRA.Network
|
||||
// Send the trait data first to guarantee that it is available when needed
|
||||
foreach (var kv in TraitData)
|
||||
{
|
||||
var data = new List<MiniYamlNode>() { new MiniYamlNode(kv.Key.ToString(), kv.Value) }.WriteToString();
|
||||
var data = new List<MiniYamlNode>() { new(kv.Key.ToStringInvariant(), kv.Value) }.WriteToString();
|
||||
packetFn(0, 0, Order.FromTargetString("SaveTraitData", data, true).Serialize());
|
||||
}
|
||||
|
||||
@@ -288,35 +288,35 @@ namespace OpenRA.Network
|
||||
{
|
||||
ordersStream.Seek(0, SeekOrigin.Begin);
|
||||
ordersStream.CopyTo(file);
|
||||
file.Write(BitConverter.GetBytes(MetadataMarker), 0, 4);
|
||||
file.Write(BitConverter.GetBytes(LastOrdersFrame), 0, 4);
|
||||
file.Write(BitConverter.GetBytes(LastSyncFrame), 0, 4);
|
||||
file.Write(MetadataMarker);
|
||||
file.Write(LastOrdersFrame);
|
||||
file.Write(LastSyncFrame);
|
||||
file.Write(lastSyncPacket, 0, Order.SyncHashOrderLength);
|
||||
|
||||
var globalSettingsNodes = new List<MiniYamlNode>() { GlobalSettings.Serialize() };
|
||||
file.WriteString(Encoding.UTF8, globalSettingsNodes.WriteToString());
|
||||
file.WriteLengthPrefixedString(Encoding.UTF8, globalSettingsNodes.WriteToString());
|
||||
|
||||
var slotNodes = Slots
|
||||
.Select(s => s.Value.Serialize())
|
||||
.ToList();
|
||||
file.WriteString(Encoding.UTF8, slotNodes.WriteToString());
|
||||
file.WriteLengthPrefixedString(Encoding.UTF8, slotNodes.WriteToString());
|
||||
|
||||
var slotClientNodes = SlotClients
|
||||
.Select(s => s.Value.Serialize(s.Key))
|
||||
.ToList();
|
||||
file.WriteString(Encoding.UTF8, slotClientNodes.WriteToString());
|
||||
file.WriteLengthPrefixedString(Encoding.UTF8, slotClientNodes.WriteToString());
|
||||
|
||||
var traitDataOffset = file.Length;
|
||||
file.Write(BitConverter.GetBytes(TraitDataMarker), 0, 4);
|
||||
file.Write(TraitDataMarker);
|
||||
|
||||
var traitDataNodes = TraitData
|
||||
.Select(kv => new MiniYamlNode(kv.Key.ToString(), kv.Value))
|
||||
.Select(kv => new MiniYamlNode(kv.Key.ToStringInvariant(), kv.Value))
|
||||
.ToList();
|
||||
file.WriteString(Encoding.UTF8, traitDataNodes.WriteToString());
|
||||
file.WriteLengthPrefixedString(Encoding.UTF8, traitDataNodes.WriteToString());
|
||||
|
||||
file.Write(BitConverter.GetBytes(ordersStream.Length), 0, 4);
|
||||
file.Write(BitConverter.GetBytes(traitDataOffset), 0, 4);
|
||||
file.Write(BitConverter.GetBytes(EOFMarker), 0, 4);
|
||||
file.Write((int)ordersStream.Length);
|
||||
file.Write((int)traitDataOffset);
|
||||
file.Write(EOFMarker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace OpenRA.Network
|
||||
static object LoadClients(MiniYaml yaml)
|
||||
{
|
||||
var clients = new List<GameClient>();
|
||||
var clientsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Clients");
|
||||
var clientsNode = yaml.NodeWithKeyOrDefault("Clients");
|
||||
if (clientsNode != null)
|
||||
{
|
||||
var regex = new Regex(@"Client@\d+");
|
||||
@@ -159,7 +159,7 @@ namespace OpenRA.Network
|
||||
// Games advertised using the old API used a single Mods field
|
||||
if (Mod == null || Version == null)
|
||||
{
|
||||
var modsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Mods");
|
||||
var modsNode = yaml.NodeWithKeyOrDefault("Mods");
|
||||
if (modsNode != null)
|
||||
{
|
||||
var modVersion = modsNode.Value.Value.Split('@');
|
||||
@@ -169,9 +169,8 @@ namespace OpenRA.Network
|
||||
}
|
||||
|
||||
// Games advertised using the old API calculated the play time locally
|
||||
if (State == 2 && PlayTime < 0)
|
||||
if (DateTime.TryParse(Started, out var startTime))
|
||||
PlayTime = (int)(DateTime.UtcNow - startTime).TotalSeconds;
|
||||
if (State == 2 && PlayTime < 0 && DateTime.TryParse(Started, out var startTime))
|
||||
PlayTime = (int)(DateTime.UtcNow - startTime).TotalSeconds;
|
||||
|
||||
var externalKey = ExternalMod.MakeKey(Mod, Version);
|
||||
if (Game.ExternalMods.TryGetValue(externalKey, out var external) && external.Version == Version)
|
||||
@@ -183,13 +182,13 @@ namespace OpenRA.Network
|
||||
if (external != null && external.Version == Version)
|
||||
{
|
||||
// Use external mod registration to populate the section header
|
||||
ModTitle = external.Title;
|
||||
ModTitle = external.Id;
|
||||
}
|
||||
else if (Game.Mods.TryGetValue(Mod, out var mod))
|
||||
{
|
||||
// Use internal mod data to populate the section header, but
|
||||
// on-connect switching must use the external mod plumbing.
|
||||
ModTitle = mod.Metadata.Title;
|
||||
ModTitle = mod.Metadata.TitleTranslated;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -200,7 +199,7 @@ namespace OpenRA.Network
|
||||
.FirstOrDefault(m => m.Id == Mod);
|
||||
|
||||
if (guessMod != null)
|
||||
ModTitle = guessMod.Title;
|
||||
ModTitle = guessMod.Id;
|
||||
else
|
||||
ModTitle = $"Unknown mod: {Mod}";
|
||||
}
|
||||
@@ -217,13 +216,13 @@ namespace OpenRA.Network
|
||||
Name = server.Settings.Name;
|
||||
|
||||
// IP address will be replaced with a real value by the master server / receiving LAN client
|
||||
Address = "0.0.0.0:" + server.Settings.ListenPort.ToString();
|
||||
Address = "0.0.0.0:" + server.Settings.ListenPort.ToStringInvariant();
|
||||
State = (int)server.State;
|
||||
MaxPlayers = server.LobbyInfo.Slots.Count(s => !s.Value.Closed) - server.LobbyInfo.Clients.Count(c1 => c1.Bot != null);
|
||||
Map = server.Map.Uid;
|
||||
Mod = manifest.Id;
|
||||
Version = manifest.Metadata.Version;
|
||||
ModTitle = manifest.Metadata.Title;
|
||||
ModTitle = manifest.Metadata.TitleTranslated;
|
||||
ModWebsite = manifest.Metadata.Website;
|
||||
ModIcon32 = manifest.Metadata.WebIcon32;
|
||||
Protected = !string.IsNullOrEmpty(server.Settings.Password);
|
||||
@@ -234,7 +233,7 @@ namespace OpenRA.Network
|
||||
|
||||
public string ToPOSTData(bool lanGame)
|
||||
{
|
||||
var root = new List<MiniYamlNode>() { new MiniYamlNode("Protocol", ProtocolVersion.ToString()) };
|
||||
var root = new List<MiniYamlNode>() { new("Protocol", ProtocolVersion.ToStringInvariant()) };
|
||||
foreach (var field in SerializeFields)
|
||||
root.Add(FieldSaver.SaveField(this, field));
|
||||
|
||||
@@ -243,18 +242,16 @@ namespace OpenRA.Network
|
||||
// Add fields that are normally generated by the master server
|
||||
// LAN games overload the Id with a GUID string (rather than an ID) to allow deduplication
|
||||
root.Add(new MiniYamlNode("Id", Platform.SessionGUID.ToString()));
|
||||
root.Add(new MiniYamlNode("Players", Clients.Count(c => !c.IsBot && !c.IsSpectator).ToString()));
|
||||
root.Add(new MiniYamlNode("Spectators", Clients.Count(c => c.IsSpectator).ToString()));
|
||||
root.Add(new MiniYamlNode("Bots", Clients.Count(c => c.IsBot).ToString()));
|
||||
root.Add(new MiniYamlNode("Players", Clients.Count(c => !c.IsBot && !c.IsSpectator).ToStringInvariant()));
|
||||
root.Add(new MiniYamlNode("Spectators", Clients.Count(c => c.IsSpectator).ToStringInvariant()));
|
||||
root.Add(new MiniYamlNode("Bots", Clients.Count(c => c.IsBot).ToStringInvariant()));
|
||||
|
||||
// Included for backwards compatibility with older clients that don't support separated Mod/Version.
|
||||
root.Add(new MiniYamlNode("Mods", Mod + "@" + Version));
|
||||
}
|
||||
|
||||
var clientsNode = new MiniYaml("");
|
||||
var i = 0;
|
||||
foreach (var c in Clients)
|
||||
clientsNode.Nodes.Add(new MiniYamlNode("Client@" + i++.ToString(), FieldSaver.Save(c)));
|
||||
var clientsNode = new MiniYaml("", Clients.Select((c, i) =>
|
||||
new MiniYamlNode("Client@" + i, FieldSaver.Save(c))));
|
||||
|
||||
root.Add(new MiniYamlNode("Clients", clientsNode));
|
||||
return new MiniYaml("", root)
|
||||
|
||||
@@ -20,16 +20,16 @@ namespace OpenRA.Network
|
||||
public string Version;
|
||||
public string AuthToken;
|
||||
|
||||
public static HandshakeRequest Deserialize(string data)
|
||||
public static HandshakeRequest Deserialize(string data, string name)
|
||||
{
|
||||
var handshake = new HandshakeRequest();
|
||||
FieldLoader.Load(handshake, MiniYaml.FromString(data).First().Value);
|
||||
FieldLoader.Load(handshake, MiniYaml.FromString(data, name).First().Value);
|
||||
return handshake;
|
||||
}
|
||||
|
||||
public string Serialize()
|
||||
{
|
||||
var data = new List<MiniYamlNode> { new MiniYamlNode("Handshake", FieldSaver.Save(this)) };
|
||||
var data = new List<MiniYamlNode> { new("Handshake", FieldSaver.Save(this)) };
|
||||
return data.WriteToString();
|
||||
}
|
||||
}
|
||||
@@ -51,14 +51,14 @@ namespace OpenRA.Network
|
||||
[FieldLoader.Ignore]
|
||||
public Session.Client Client;
|
||||
|
||||
public static HandshakeResponse Deserialize(string data)
|
||||
public static HandshakeResponse Deserialize(string data, string name)
|
||||
{
|
||||
var handshake = new HandshakeResponse
|
||||
{
|
||||
Client = new Session.Client()
|
||||
};
|
||||
|
||||
var ys = MiniYaml.FromString(data);
|
||||
var ys = MiniYaml.FromString(data, name);
|
||||
foreach (var y in ys)
|
||||
{
|
||||
switch (y.Key)
|
||||
@@ -79,9 +79,9 @@ namespace OpenRA.Network
|
||||
{
|
||||
var data = new List<MiniYamlNode>
|
||||
{
|
||||
new MiniYamlNode("Handshake", null,
|
||||
new("Handshake", null,
|
||||
new[] { "Mod", "Version", "Password", "Fingerprint", "AuthSignature", "OrdersProtocol" }.Select(p => FieldSaver.SaveField(this, p)).ToList()),
|
||||
new MiniYamlNode("Client", FieldSaver.Save(Client))
|
||||
new("Client", FieldSaver.Save(Client))
|
||||
};
|
||||
|
||||
return data.WriteToString();
|
||||
|
||||
@@ -71,8 +71,8 @@ namespace OpenRA.Network
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("Port forwarding failed: {0}", e.Message);
|
||||
Log.Write("nat", e.StackTrace);
|
||||
Log.Write("nat", "Port forwarding failed.");
|
||||
Log.Write("nat", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -90,8 +90,8 @@ namespace OpenRA.Network
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine("Port removal failed: {0}", e.Message);
|
||||
Log.Write("nat", e.StackTrace);
|
||||
Log.Write("nat", "Port removal failed.");
|
||||
Log.Write("nat", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,8 @@ namespace OpenRA
|
||||
readonly Target target;
|
||||
readonly Target visualFeedbackTarget;
|
||||
|
||||
Order(string orderString, Actor subject, in Target target, string targetString, bool queued, Actor[] extraActors, CPos extraLocation, uint extraData, Actor[] groupedActors = null)
|
||||
Order(string orderString, Actor subject, in Target target, string targetString, bool queued,
|
||||
Actor[] extraActors, CPos extraLocation, uint extraData, Actor[] groupedActors = null)
|
||||
{
|
||||
OrderString = orderString ?? "";
|
||||
Subject = subject;
|
||||
@@ -156,7 +157,18 @@ namespace OpenRA
|
||||
else
|
||||
{
|
||||
var pos = new WPos(r.ReadInt32(), r.ReadInt32(), r.ReadInt32());
|
||||
target = Target.FromPos(pos);
|
||||
|
||||
var numberOfTerrainPositions = r.ReadInt16();
|
||||
if (numberOfTerrainPositions == -1)
|
||||
target = Target.FromPos(pos);
|
||||
else
|
||||
{
|
||||
var terrainPositions = new WPos[numberOfTerrainPositions];
|
||||
for (var i = 0; i < numberOfTerrainPositions; i++)
|
||||
terrainPositions[i] = new WPos(r.ReadInt32(), r.ReadInt32(), r.ReadInt32());
|
||||
|
||||
target = Target.FromSerializedTerrainPosition(pos, terrainPositions);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -259,7 +271,15 @@ namespace OpenRA
|
||||
|
||||
public static Order FromGroupedOrder(Order grouped, Actor subject)
|
||||
{
|
||||
return new Order(grouped.OrderString, subject, grouped.Target, grouped.TargetString, grouped.Queued, grouped.ExtraActors, grouped.ExtraLocation, grouped.ExtraData);
|
||||
return new Order(
|
||||
grouped.OrderString,
|
||||
subject,
|
||||
grouped.Target,
|
||||
grouped.TargetString,
|
||||
grouped.Queued,
|
||||
grouped.ExtraActors,
|
||||
grouped.ExtraLocation,
|
||||
grouped.ExtraData);
|
||||
}
|
||||
|
||||
public static Order Command(string text)
|
||||
@@ -388,6 +408,21 @@ namespace OpenRA
|
||||
w.Write(targetState.Pos.X);
|
||||
w.Write(targetState.Pos.Y);
|
||||
w.Write(targetState.Pos.Z);
|
||||
|
||||
// Don't send extra data over the network that will be restored by the Target ctor
|
||||
var terrainPositions = targetState.TerrainPositions.Length;
|
||||
if (terrainPositions == 1 && targetState.TerrainPositions[0] == targetState.Pos)
|
||||
w.Write((short)-1);
|
||||
else
|
||||
{
|
||||
w.Write((short)terrainPositions);
|
||||
foreach (var position in targetState.TerrainPositions)
|
||||
{
|
||||
w.Write(position.X);
|
||||
w.Write(position.Y);
|
||||
w.Write(position.Z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -430,7 +465,7 @@ namespace OpenRA
|
||||
public override string ToString()
|
||||
{
|
||||
return $"OrderString: \"{OrderString}\" \n\t Type: \"{Type}\". \n\t Subject: \"{Subject}\". \n\t Target: \"{Target}\"." +
|
||||
$"\n\t TargetString: \"{TargetString}\".\n\t IsImmediate: {IsImmediate}.\n\t Player(PlayerName): {Player?.PlayerName}\n";
|
||||
$"\n\t TargetString: \"{TargetString}\".\n\t IsImmediate: {IsImmediate}.\n\t Player(PlayerName): {Player?.ResolvedPlayerName}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace OpenRA.Network
|
||||
// the Order objects directly on the local client.
|
||||
data = new MemoryStream();
|
||||
foreach (var o in orders)
|
||||
data.WriteArray(o.Serialize());
|
||||
data.Write(o.Serialize());
|
||||
}
|
||||
|
||||
public OrderPacket(MemoryStream data)
|
||||
@@ -55,7 +55,7 @@ namespace OpenRA.Network
|
||||
public byte[] Serialize(int frame)
|
||||
{
|
||||
var ms = new MemoryStream((int)data.Length + 4);
|
||||
ms.WriteArray(BitConverter.GetBytes(frame));
|
||||
ms.Write(frame);
|
||||
|
||||
data.Position = 0;
|
||||
data.CopyTo(ms);
|
||||
@@ -83,19 +83,19 @@ namespace OpenRA.Network
|
||||
public static byte[] SerializeSync((int Frame, int SyncHash, ulong DefeatState) data)
|
||||
{
|
||||
var ms = new MemoryStream(4 + Order.SyncHashOrderLength);
|
||||
ms.WriteArray(BitConverter.GetBytes(data.Frame));
|
||||
ms.Write(data.Frame);
|
||||
ms.WriteByte((byte)OrderType.SyncHash);
|
||||
ms.WriteArray(BitConverter.GetBytes(data.SyncHash));
|
||||
ms.WriteArray(BitConverter.GetBytes(data.DefeatState));
|
||||
ms.Write(data.SyncHash);
|
||||
ms.Write(data.DefeatState);
|
||||
return ms.GetBuffer();
|
||||
}
|
||||
|
||||
public static byte[] SerializePingResponse(long timestamp, byte queueLength)
|
||||
{
|
||||
var ms = new MemoryStream(14);
|
||||
ms.WriteArray(BitConverter.GetBytes(0));
|
||||
ms.Write(0);
|
||||
ms.WriteByte((byte)OrderType.Ping);
|
||||
ms.WriteArray(BitConverter.GetBytes(timestamp));
|
||||
ms.Write(timestamp);
|
||||
ms.WriteByte(queueLength);
|
||||
return ms.GetBuffer();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace OpenRA.Network
|
||||
{
|
||||
const OrderPacket ClientDisconnected = null;
|
||||
|
||||
[FluentReference("frame")]
|
||||
const string DesyncCompareLogs = "notification-desync-compare-logs";
|
||||
|
||||
readonly SyncReport syncReport;
|
||||
readonly Dictionary<int, Queue<(int Frame, OrderPacket Orders)>> pendingOrders = new();
|
||||
readonly Dictionary<int, (int SyncHash, ulong DefeatState)> syncForFrame = new();
|
||||
@@ -36,6 +39,9 @@ namespace OpenRA.Network
|
||||
public string ServerError = null;
|
||||
public bool AuthenticationFailed = false;
|
||||
|
||||
// The default null means "no map restriction" while an empty set means "all maps restricted"
|
||||
public HashSet<string> ServerMapPool = null;
|
||||
|
||||
public int NetFrameNumber { get; private set; }
|
||||
public int LocalFrameNumber;
|
||||
|
||||
@@ -70,7 +76,7 @@ namespace OpenRA.Network
|
||||
public int Client;
|
||||
public Order Order;
|
||||
|
||||
public override string ToString()
|
||||
public override readonly string ToString()
|
||||
{
|
||||
return $"ClientId: {Client} {Order}";
|
||||
}
|
||||
@@ -85,7 +91,7 @@ namespace OpenRA.Network
|
||||
World.OutOfSync();
|
||||
IsOutOfSync = true;
|
||||
|
||||
TextNotificationsManager.AddSystemLine($"Out of sync in frame {frame}.\nCompare syncreport.log with other players.");
|
||||
TextNotificationsManager.AddSystemLine(DesyncCompareLogs, "frame", frame);
|
||||
}
|
||||
|
||||
public void StartGame()
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace OpenRA.Network
|
||||
if (o.OrderString == "StartGame")
|
||||
IsValid = true;
|
||||
else if (o.OrderString == "SyncInfo" && !IsValid)
|
||||
LobbyInfo = Session.Deserialize(o.TargetString);
|
||||
LobbyInfo = Session.Deserialize(o.TargetString, o.OrderString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace OpenRA.Network
|
||||
}
|
||||
}
|
||||
|
||||
file.WriteArray(initialContent);
|
||||
file.Write(initialContent);
|
||||
writer = new BinaryWriter(file);
|
||||
}
|
||||
|
||||
@@ -92,8 +92,8 @@ namespace OpenRA.Network
|
||||
public void ReceiveFrame(int clientID, int frame, byte[] data)
|
||||
{
|
||||
var ms = new MemoryStream(4 + data.Length);
|
||||
ms.WriteArray(BitConverter.GetBytes(frame));
|
||||
ms.WriteArray(data);
|
||||
ms.Write(frame);
|
||||
ms.Write(data);
|
||||
Receive(clientID, ms.GetBuffer());
|
||||
}
|
||||
|
||||
|
||||
@@ -41,13 +41,13 @@ namespace OpenRA.Network
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Session Deserialize(string data)
|
||||
public static Session Deserialize(string data, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
var session = new Session();
|
||||
|
||||
var nodes = MiniYaml.FromString(data);
|
||||
var nodes = MiniYaml.FromString(data, name);
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var strings = node.Key.Split('@');
|
||||
@@ -227,7 +227,7 @@ namespace OpenRA.Network
|
||||
{
|
||||
var gs = FieldLoader.Load<Global>(data);
|
||||
|
||||
var optionsNode = data.Nodes.FirstOrDefault(n => n.Key == "Options");
|
||||
var optionsNode = data.NodeWithKeyOrDefault("Options");
|
||||
if (optionsNode != null)
|
||||
foreach (var n in optionsNode.Value.Nodes)
|
||||
gs.LobbyOptions[n.Key] = FieldLoader.Load<LobbyOptionState>(n.Value);
|
||||
@@ -238,8 +238,9 @@ namespace OpenRA.Network
|
||||
public MiniYamlNode Serialize()
|
||||
{
|
||||
var data = new MiniYamlNode("GlobalSettings", FieldSaver.Save(this));
|
||||
var options = LobbyOptions.Select(kv => new MiniYamlNode(kv.Key, FieldSaver.Save(kv.Value))).ToList();
|
||||
data.Value.Nodes.Add(new MiniYamlNode("Options", new MiniYaml(null, options)));
|
||||
var options = LobbyOptions.Select(kv => new MiniYamlNode(kv.Key, FieldSaver.Save(kv.Value)));
|
||||
data = data.WithValue(data.Value.WithNodesAppended(
|
||||
new[] { new MiniYamlNode("Options", new MiniYaml(null, options)) }));
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -264,7 +265,7 @@ namespace OpenRA.Network
|
||||
{
|
||||
var sessionData = new List<MiniYamlNode>()
|
||||
{
|
||||
new MiniYamlNode("DisabledSpawnPoints", FieldSaver.FormatValue(DisabledSpawnPoints))
|
||||
new("DisabledSpawnPoints", FieldSaver.FormatValue(DisabledSpawnPoints))
|
||||
};
|
||||
|
||||
foreach (var client in Clients)
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace OpenRA.Network
|
||||
Log.Write("sync", $"Player: {Game.Settings.Player.Name} ({Platform.CurrentPlatform} {Environment.OSVersion} {Platform.RuntimeVersion})");
|
||||
if (Game.IsHost)
|
||||
Log.Write("sync", "Player is host.");
|
||||
Log.Write("sync", $"Game ID: {orderManager.LobbyInfo.GlobalSettings.GameUid} (Mod: {mod.Title} at Version {mod.Version})");
|
||||
Log.Write("sync", $"Game ID: {orderManager.LobbyInfo.GlobalSettings.GameUid} (Mod: {mod.TitleTranslated} at Version {mod.Version})");
|
||||
Log.Write("sync", $"Sync for net frame {r.Frame} -------------");
|
||||
Log.Write("sync", $"SharedRandom: {r.SyncedRandom} (#{r.TotalCount})");
|
||||
Log.Write("sync", "Synced Traits:");
|
||||
@@ -201,8 +201,12 @@ namespace OpenRA.Network
|
||||
public TypeInfo(Type type)
|
||||
{
|
||||
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
var fields = type.GetFields(Flags).Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>());
|
||||
var properties = type.GetProperties(Flags).Where(pi => pi.HasAttribute<SyncAttribute>());
|
||||
var fields = type.GetFields(Flags)
|
||||
.Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>())
|
||||
.ToList();
|
||||
var properties = type.GetProperties(Flags)
|
||||
.Where(pi => pi.HasAttribute<SyncAttribute>())
|
||||
.ToList();
|
||||
|
||||
foreach (var prop in properties)
|
||||
if (!prop.CanRead || prop.GetIndexParameters().Length > 0)
|
||||
@@ -300,7 +304,7 @@ namespace OpenRA.Network
|
||||
|
||||
public object this[int index]
|
||||
{
|
||||
get
|
||||
readonly get
|
||||
{
|
||||
if (item2OrSentinel == Sentinel)
|
||||
return ((object[])item1OrArray)[index];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user