Compare commits
544 Commits
log_chat_s
...
log_chat_s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1c5b19697 | ||
|
|
7e801c6fd7 | ||
|
|
93619f2766 | ||
|
|
7c44f94755 | ||
|
|
5c51f458f2 | ||
|
|
413aa00ce6 | ||
|
|
0a9b493c14 | ||
|
|
3a86c035b3 | ||
|
|
0a933ba09c | ||
|
|
6b04dbc210 | ||
|
|
c651f733a7 | ||
|
|
4a04f8df83 | ||
|
|
096ad0c413 | ||
|
|
74c4bcc072 | ||
|
|
2516c5a798 | ||
|
|
ef5541cce4 | ||
|
|
efe7bce08a | ||
|
|
a6f5002a84 | ||
|
|
05250933e7 | ||
|
|
f827421b37 | ||
|
|
ac162936ca | ||
|
|
2164700a7a | ||
|
|
d6af55bc04 | ||
|
|
f8c20a0f8d | ||
|
|
d6e2257158 | ||
|
|
a0a0d832f9 | ||
|
|
c063e16c51 | ||
|
|
dd14d72ed3 | ||
|
|
6743904372 | ||
|
|
a20ab8a33a | ||
|
|
85e16032c2 | ||
|
|
924c79b0ad | ||
|
|
ed1ebd536b | ||
|
|
156122f9e3 | ||
|
|
a6c58c0f68 | ||
|
|
ea07cb2cb1 | ||
|
|
bea698b2f1 | ||
|
|
9528f19845 | ||
|
|
d0ca1af555 | ||
|
|
78873b8e00 | ||
|
|
e48a77b67a | ||
|
|
95e698f44e | ||
|
|
a542767720 | ||
|
|
c3f902ebcf | ||
|
|
34e1e05dd8 | ||
|
|
df20709158 | ||
|
|
2cf2d7eed7 | ||
|
|
1300ecc18f | ||
|
|
dcd4c41d81 | ||
|
|
032f20b622 | ||
|
|
4c2f52a465 | ||
|
|
f8eefe310c | ||
|
|
c47ebfbb52 | ||
|
|
2c0a4f5873 | ||
|
|
81147bc3f3 | ||
|
|
efa673d999 | ||
|
|
9e1f7cad55 | ||
|
|
9fccc35046 | ||
|
|
afc6532aa8 | ||
|
|
8869533d07 | ||
|
|
b198eec753 | ||
|
|
f590e806a1 | ||
|
|
3f53abc4c4 | ||
|
|
5de04b0db9 | ||
|
|
0c1875db07 | ||
|
|
638c0d7db1 | ||
|
|
6e98d7f73e | ||
|
|
ec7e5021ae | ||
|
|
b2f32f8907 | ||
|
|
ed3f809982 | ||
|
|
e5cca8d0b4 | ||
|
|
f173b6f604 | ||
|
|
e495f4eca0 | ||
|
|
c07af73eae | ||
|
|
eb09870a10 | ||
|
|
0f22e3d4e4 | ||
|
|
af199a3a37 | ||
|
|
330f438a18 | ||
|
|
c7e67529b5 | ||
|
|
de952e6caf | ||
|
|
2246922118 | ||
|
|
fdb4ce4e0f | ||
|
|
0d479e7c18 | ||
|
|
4436f07fbf | ||
|
|
27fbd3ddeb | ||
|
|
6ae03038cf | ||
|
|
17210c1088 | ||
|
|
827f4682a5 | ||
|
|
a546ca2f92 | ||
|
|
3ea5b08848 | ||
|
|
4aec67c0c6 | ||
|
|
bca1dd599d | ||
|
|
d69deba56f | ||
|
|
81fec36fbb | ||
|
|
f8b9dfc73e | ||
|
|
d4fd66adf9 | ||
|
|
ec75dcd9e9 | ||
|
|
2e16ad47e5 | ||
|
|
7e9d87a808 | ||
|
|
98f7fe0db7 | ||
|
|
575f6624a2 | ||
|
|
962e7e911d | ||
|
|
c588c3ef8d | ||
|
|
70624fa44f | ||
|
|
06e3e013be | ||
|
|
06e4a2e010 | ||
|
|
bcfaa44d66 | ||
|
|
1539741e71 | ||
|
|
3ad38ba503 | ||
|
|
7cef83c51c | ||
|
|
493cf3f1eb | ||
|
|
3c7389f582 | ||
|
|
374166e9d3 | ||
|
|
31607bd0cf | ||
|
|
2b6977d53f | ||
|
|
a04cfaec78 | ||
|
|
13313b4270 | ||
|
|
7175e9062d | ||
|
|
94177848a8 | ||
|
|
9e548fd0f0 | ||
|
|
f4eb5739d6 | ||
|
|
637d8689f5 | ||
|
|
a3ee6645c1 | ||
|
|
649e7e8c28 | ||
|
|
797c71e500 | ||
|
|
e53f2b6044 | ||
|
|
2b2e2d6fe3 | ||
|
|
027e38ee69 | ||
|
|
9e926b0f53 | ||
|
|
f9bce6252c | ||
|
|
dd1528bd4a | ||
|
|
79567f3f8a | ||
|
|
8b8651dcf7 | ||
|
|
b429ca7879 | ||
|
|
1ad1d18abf | ||
|
|
93602c7c95 | ||
|
|
62e692063a | ||
|
|
9478548bdb | ||
|
|
8dc75eec34 | ||
|
|
3e7e4df2ed | ||
|
|
7e8f4a3479 | ||
|
|
86a4fb1d6f | ||
|
|
5ff7c9f269 | ||
|
|
b340c42ab1 | ||
|
|
ec7461f09f | ||
|
|
ba632fbf8b | ||
|
|
89a4823a83 | ||
|
|
bd53aa734e | ||
|
|
cde5208c54 | ||
|
|
e652f95be9 | ||
|
|
00cd438190 | ||
|
|
91ddfd6fc1 | ||
|
|
915ad36ddc | ||
|
|
56e6a9e6dc | ||
|
|
bc1a901f54 | ||
|
|
7d0340ad41 | ||
|
|
42ac32dace | ||
|
|
24c19f64c9 | ||
|
|
af326bfeb8 | ||
|
|
3745377387 | ||
|
|
0ed00a5766 | ||
|
|
6f6d039650 | ||
|
|
2359dce21b | ||
|
|
fd5e05ca6b | ||
|
|
3177de7b2c | ||
|
|
eb550bbbbb | ||
|
|
7004b552c9 | ||
|
|
3f19c81392 | ||
|
|
a62b085f1d | ||
|
|
a514f3a388 | ||
|
|
ebae15a9fe | ||
|
|
10db26fa0b | ||
|
|
47105e42cd | ||
|
|
2cb1e5f8c5 | ||
|
|
79288c6ee3 | ||
|
|
93dc94c1a0 | ||
|
|
79765004fc | ||
|
|
08d07eeb06 | ||
|
|
4cd1b4e208 | ||
|
|
bb2a44705f | ||
|
|
70243850b1 | ||
|
|
dff7cd3bbf | ||
|
|
24a09a94fe | ||
|
|
8144c5684e | ||
|
|
315b26e85c | ||
|
|
ea775ab824 | ||
|
|
1c693a2c4b | ||
|
|
41123d0520 | ||
|
|
d26b9a10ed | ||
|
|
58088ecabe | ||
|
|
a2a7b30727 | ||
|
|
3dcbdaa56e | ||
|
|
0fb42b39d9 | ||
|
|
dc2fa932ea | ||
|
|
d08d03274b | ||
|
|
b6495113c4 | ||
|
|
de78f4130b | ||
|
|
a16486fbfc | ||
|
|
9b94796a32 | ||
|
|
76b8a949e1 | ||
|
|
b0514bdb87 | ||
|
|
961702d818 | ||
|
|
7a2e25af86 | ||
|
|
f71684a120 | ||
|
|
361ec67ad9 | ||
|
|
48a5b5b9bc | ||
|
|
2804d103ae | ||
|
|
59ecb8d8a3 | ||
|
|
148cd911d5 | ||
|
|
2c86411eb4 | ||
|
|
2d755b010c | ||
|
|
619c5a13a1 | ||
|
|
f7e1b4259c | ||
|
|
82f35bbe1c | ||
|
|
b4c767ce48 | ||
|
|
fb5aae827a | ||
|
|
831cce2f9c | ||
|
|
994eddd46f | ||
|
|
85637d580e | ||
|
|
992305a1c0 | ||
|
|
0da4766300 | ||
|
|
fe2c227270 | ||
|
|
955634a520 | ||
|
|
83bbb357d1 | ||
|
|
bd590dfcf4 | ||
|
|
d4a08e5b58 | ||
|
|
9da921756d | ||
|
|
cccd40ca13 | ||
|
|
b5b44c048d | ||
|
|
7d4a590240 | ||
|
|
082f369fe3 | ||
|
|
1a8a3eca4d | ||
|
|
270c37764e | ||
|
|
a8b13d012b | ||
|
|
0230b79269 | ||
|
|
e126c59e94 | ||
|
|
d555baa0c2 | ||
|
|
3f73e7a1fb | ||
|
|
4823c3365f | ||
|
|
7d27df7053 | ||
|
|
4c9ae3678b | ||
|
|
2cf6ac6383 | ||
|
|
7cd7b32042 | ||
|
|
c3b22c9102 | ||
|
|
1124c6e6c2 | ||
|
|
28b02f662f | ||
|
|
792231fbb9 | ||
|
|
f8c8c36307 | ||
|
|
885274e3d7 | ||
|
|
8a0197c88b | ||
|
|
b2215c6dd1 | ||
|
|
42181d77a5 | ||
|
|
f8921226b8 | ||
|
|
c2c56c6374 | ||
|
|
f325e9c7b9 | ||
|
|
c19a7d338b | ||
|
|
7206a33d0d | ||
|
|
c27d6dd1b1 | ||
|
|
28a8f3a01f | ||
|
|
32750700a4 | ||
|
|
be971c4a34 | ||
|
|
d2780382bc | ||
|
|
a66feafa3f | ||
|
|
f0ec8105b3 | ||
|
|
15824ef70f | ||
|
|
c2ca0a1545 | ||
|
|
5aa7ee43db | ||
|
|
7779d0c27c | ||
|
|
288d839456 | ||
|
|
cc9fa4b3ef | ||
|
|
bb0a39ba6c | ||
|
|
c56d2c1bbe | ||
|
|
8ce9e01c1c | ||
|
|
3f0126eef4 | ||
|
|
5f51dec9f9 | ||
|
|
0c5e998e31 | ||
|
|
07d3ab2613 | ||
|
|
de49ad6a79 | ||
|
|
88043e297b | ||
|
|
f416e123f0 | ||
|
|
ec206dc359 | ||
|
|
25be2149c1 | ||
|
|
44c6747dec | ||
|
|
5d8e35088f | ||
|
|
e61db3faea | ||
|
|
0fac8d0d31 | ||
|
|
53f959e4bf | ||
|
|
a4966bc299 | ||
|
|
fa5a347d2e | ||
|
|
3ca951930e | ||
|
|
5f407211ac | ||
|
|
484784bd4d | ||
|
|
77baa7a35f | ||
|
|
3a5358ad7c | ||
|
|
74ead504ec | ||
|
|
8833e95f21 | ||
|
|
d657a72107 | ||
|
|
abc3a79330 | ||
|
|
25571df2b6 | ||
|
|
e4f289921c | ||
|
|
444e849241 | ||
|
|
2e3a30c9ca | ||
|
|
18870cbb03 | ||
|
|
fcae6a26d1 | ||
|
|
aa05081636 | ||
|
|
368ac277d0 | ||
|
|
631cbb1421 | ||
|
|
6fe039f62e | ||
|
|
f5ecb9ad99 | ||
|
|
84378969c3 | ||
|
|
4c04d4fa30 | ||
|
|
abec71c42d | ||
|
|
03c24da611 | ||
|
|
4be3add30a | ||
|
|
c27511dd66 | ||
|
|
12cb5997f0 | ||
|
|
ad19506836 | ||
|
|
0b13ee3e49 | ||
|
|
c92a6be4af | ||
|
|
561466d822 | ||
|
|
c6ed381085 | ||
|
|
aae2ec109b | ||
|
|
9c8470d18e | ||
|
|
c4332c808b | ||
|
|
1cdc823bca | ||
|
|
5c7188be81 | ||
|
|
2a0e787ae7 | ||
|
|
7538943f59 | ||
|
|
99253702c5 | ||
|
|
5c09d5d828 | ||
|
|
daf36769d6 | ||
|
|
ba444de014 | ||
|
|
6a67906823 | ||
|
|
681edb1879 | ||
|
|
b7d929de5a | ||
|
|
6ffd786d5b | ||
|
|
979483b63c | ||
|
|
6be947bbf0 | ||
|
|
5292919917 | ||
|
|
497b5e30ad | ||
|
|
4f1b17b832 | ||
|
|
ad08045e5f | ||
|
|
d3d949176d | ||
|
|
89d906a613 | ||
|
|
8f3ca30840 | ||
|
|
c5e4545ecd | ||
|
|
894d08adf0 | ||
|
|
92b3dde789 | ||
|
|
7fb88b974e | ||
|
|
798593bede | ||
|
|
3e06749006 | ||
|
|
9ffa7be606 | ||
|
|
0313d04970 | ||
|
|
ad13658076 | ||
|
|
0b110a0423 | ||
|
|
34a92f3406 | ||
|
|
b792e3eecf | ||
|
|
e27aa0d379 | ||
|
|
2b1121b970 | ||
|
|
5537a194fa | ||
|
|
56c615d890 | ||
|
|
e8d710dee4 | ||
|
|
4a4217c3b3 | ||
|
|
965a490b70 | ||
|
|
5b7a714ac2 | ||
|
|
90013bc6d3 | ||
|
|
246a1b9fec | ||
|
|
e5ffed2a4f | ||
|
|
c41a1302b6 | ||
|
|
bad58b5821 | ||
|
|
4c551cef00 | ||
|
|
d3b7688a7d | ||
|
|
9c78c45faf | ||
|
|
ce41ec3fc3 | ||
|
|
07ef98a578 | ||
|
|
c541a0e0dc | ||
|
|
f1573d4b0c | ||
|
|
ede12610a8 | ||
|
|
2fbbff2860 | ||
|
|
cd9001e7b9 | ||
|
|
87cff1f6f1 | ||
|
|
39aec39364 | ||
|
|
a2096b6768 | ||
|
|
cf11c6633e | ||
|
|
31154e3d2d | ||
|
|
e10cdc6690 | ||
|
|
f09959ecba | ||
|
|
b2acc653a0 | ||
|
|
78f660124c | ||
|
|
8f46247dc9 | ||
|
|
5676d7be40 | ||
|
|
d459d3883d | ||
|
|
af0dee3382 | ||
|
|
a4317fec57 | ||
|
|
3c2945c1bc | ||
|
|
d136ccc350 | ||
|
|
76cbd5582d | ||
|
|
d26d9e0b9c | ||
|
|
c187d21b95 | ||
|
|
36660b89e9 | ||
|
|
3de6a5fd5a | ||
|
|
737890d395 | ||
|
|
c04041e328 | ||
|
|
c028d8ad29 | ||
|
|
597e8e1573 | ||
|
|
0b03d7ea31 | ||
|
|
ee24d0539a | ||
|
|
25188e849e | ||
|
|
0f7b24a8c0 | ||
|
|
fc8103098e | ||
|
|
79454d8fd2 | ||
|
|
0740991c12 | ||
|
|
117a47f81f | ||
|
|
299c9b5506 | ||
|
|
30300d18bf | ||
|
|
665bebc460 | ||
|
|
385999dce9 | ||
|
|
13991f12c4 | ||
|
|
b623c5aa72 | ||
|
|
37b8912a91 | ||
|
|
e7d49723ea | ||
|
|
e2cb83436c | ||
|
|
2b28da97f9 | ||
|
|
5da2f1ba8a | ||
|
|
3111d06fcf | ||
|
|
6340379e3d | ||
|
|
e78ab81b26 | ||
|
|
e8391de36a | ||
|
|
ad3caf7e00 | ||
|
|
7e669013d9 | ||
|
|
d9425e4119 | ||
|
|
c4756b78ae | ||
|
|
b40b67242f | ||
|
|
37e5b0d74c | ||
|
|
cf3e213e7d | ||
|
|
f679bb1e2e | ||
|
|
9c6ee3ad02 | ||
|
|
a584bd8598 | ||
|
|
efce78179f | ||
|
|
cdef6413e7 | ||
|
|
9913ecb42f | ||
|
|
1707dbd92b | ||
|
|
fceb8a479d | ||
|
|
e186dc428e | ||
|
|
5c0b82e65b | ||
|
|
d379258afb | ||
|
|
e4ec2dfb4a | ||
|
|
e06d74b417 | ||
|
|
ab3b6a0e43 | ||
|
|
e022744b0a | ||
|
|
23b7b56c28 | ||
|
|
7ee79a0e64 | ||
|
|
8f59ddff5d | ||
|
|
db90cfb259 | ||
|
|
c892666b8f | ||
|
|
541cf3b720 | ||
|
|
0338258b45 | ||
|
|
ce3ad6fbb3 | ||
|
|
1b2c119b58 | ||
|
|
a324af1afd | ||
|
|
715d882456 | ||
|
|
39a7e477c6 | ||
|
|
85999d7a40 | ||
|
|
6ac33befe5 | ||
|
|
cfde11556f | ||
|
|
71ae0aedfb | ||
|
|
6193da6899 | ||
|
|
459bbf0025 | ||
|
|
96de59f75a | ||
|
|
ebf90970b2 | ||
|
|
747f41a8c3 | ||
|
|
639dc7c288 | ||
|
|
6ee274be6b | ||
|
|
017e645557 | ||
|
|
bcbf88719f | ||
|
|
81aa068b15 | ||
|
|
2a3ef8f910 | ||
|
|
0d30b19b1b | ||
|
|
98fe3c98ab | ||
|
|
cc632660eb | ||
|
|
1f65fd234b | ||
|
|
052c74e2d7 | ||
|
|
611ac8ce54 | ||
|
|
c6d5a1a182 | ||
|
|
09259be796 | ||
|
|
409efe7f4a | ||
|
|
0ea4bb10a1 | ||
|
|
56109b0ca1 | ||
|
|
1fda679b9f | ||
|
|
440987c0a4 | ||
|
|
cdbcfb9e20 | ||
|
|
19585ec7ac | ||
|
|
ddfdfcfee3 | ||
|
|
49fd39b4b6 | ||
|
|
55787fb7b7 | ||
|
|
6a8e5c7c83 | ||
|
|
037326024b | ||
|
|
04e9cef38e | ||
|
|
2126f3c5a2 | ||
|
|
bdc50899de | ||
|
|
75d4fa2509 | ||
|
|
9d77ca2bf8 | ||
|
|
5f9e0ffd43 | ||
|
|
dfa922de91 | ||
|
|
c66f63805a | ||
|
|
f2e582ce83 | ||
|
|
e903baf680 | ||
|
|
94f2ce4f56 | ||
|
|
dc4c596fa0 | ||
|
|
b9adbf8089 | ||
|
|
427e56d718 | ||
|
|
01819714a2 | ||
|
|
aa121dcb2f | ||
|
|
9c568aba33 | ||
|
|
22d3b5e5a1 | ||
|
|
04b61a20ea | ||
|
|
417f787294 | ||
|
|
0536c58b78 | ||
|
|
711055a3bc | ||
|
|
0efbefc62a | ||
|
|
c7cc9a68fd | ||
|
|
5450572e0a | ||
|
|
8b4478260e | ||
|
|
d9cfb2c278 | ||
|
|
1904012604 | ||
|
|
2c221fdc68 | ||
|
|
955bf96a84 | ||
|
|
f210b39bbc | ||
|
|
59c0c908cc | ||
|
|
640aa6545b | ||
|
|
269542c4b0 | ||
|
|
f820adab51 | ||
|
|
48d5b3c8d9 | ||
|
|
81875a1f3f | ||
|
|
0bd139b061 | ||
|
|
9719c1a3b0 | ||
|
|
0051d6fd63 | ||
|
|
2a1eeae7ca | ||
|
|
43325fdee7 | ||
|
|
087aebe058 | ||
|
|
18450aaff0 | ||
|
|
1af1ebd9e3 | ||
|
|
f76009d095 | ||
|
|
79875056bc |
161
.editorconfig
161
.editorconfig
@@ -63,7 +63,7 @@ dotnet_diagnostic.IDE0161.severity = warning
|
|||||||
|
|
||||||
# IDE0200 Remove unnecessary lambda expression
|
# IDE0200 Remove unnecessary lambda expression
|
||||||
#csharp_style_prefer_method_group_conversion = true
|
#csharp_style_prefer_method_group_conversion = true
|
||||||
dotnet_diagnostic.IDE0200.severity = silent # Requires C# 11
|
dotnet_diagnostic.IDE0200.severity = warning
|
||||||
|
|
||||||
# IDE0210 Convert to top-level statements/IDE0211 Convert to 'Program.Main' style program
|
# IDE0210 Convert to top-level statements/IDE0211 Convert to 'Program.Main' style program
|
||||||
csharp_style_prefer_top_level_statements = false
|
csharp_style_prefer_top_level_statements = false
|
||||||
@@ -72,7 +72,7 @@ dotnet_diagnostic.IDE0211.severity = warning
|
|||||||
|
|
||||||
# IDE0290 Use primary constructor
|
# IDE0290 Use primary constructor
|
||||||
#csharp_style_prefer_primary_constructors = true
|
#csharp_style_prefer_primary_constructors = true
|
||||||
dotnet_diagnostic.IDE0200.severity = silent # Requires C# 12
|
dotnet_diagnostic.IDE0290.severity = silent
|
||||||
|
|
||||||
# IDE0330 Prefer 'System.Threading.Lock'
|
# IDE0330 Prefer 'System.Threading.Lock'
|
||||||
#csharp_prefer_system_threading_lock = true
|
#csharp_prefer_system_threading_lock = true
|
||||||
@@ -229,6 +229,10 @@ dotnet_diagnostic.IDE0100.severity = warning
|
|||||||
# No options
|
# No options
|
||||||
dotnet_diagnostic.IDE0120.severity = warning
|
dotnet_diagnostic.IDE0120.severity = warning
|
||||||
|
|
||||||
|
# IDE0121 Simplify LINQ type check and cast
|
||||||
|
# No options
|
||||||
|
dotnet_diagnostic.IDE0121.severity = warning
|
||||||
|
|
||||||
# IDE0130 Namespace does not match folder structure
|
# IDE0130 Namespace does not match folder structure
|
||||||
#dotnet_style_namespace_match_folder = true
|
#dotnet_style_namespace_match_folder = true
|
||||||
# This rule doesn't appear to work (never reports violations)
|
# This rule doesn't appear to work (never reports violations)
|
||||||
@@ -292,7 +296,7 @@ dotnet_diagnostic.IDE0220.severity = warning
|
|||||||
|
|
||||||
# IDE0230 Use UTF-8 string literal
|
# IDE0230 Use UTF-8 string literal
|
||||||
#csharp_style_prefer_utf8_string_literals = true
|
#csharp_style_prefer_utf8_string_literals = true
|
||||||
dotnet_diagnostic.IDE0230.severity = silent # Requires C# 11
|
dotnet_diagnostic.IDE0230.severity = warning
|
||||||
|
|
||||||
# IDE0240 Nullable directive is redundant
|
# IDE0240 Nullable directive is redundant
|
||||||
# No options
|
# No options
|
||||||
@@ -308,27 +312,43 @@ dotnet_diagnostic.IDE0241.severity = warning
|
|||||||
|
|
||||||
# IDE0300 Use collection expression for array
|
# IDE0300 Use collection expression for array
|
||||||
# From above, uses dotnet_style_prefer_collection_expression
|
# From above, uses dotnet_style_prefer_collection_expression
|
||||||
dotnet_diagnostic.IDE0300.severity = silent # Requires C# 12
|
dotnet_diagnostic.IDE0300.severity = warning
|
||||||
|
|
||||||
# IDE0301 Use collection expression for empty
|
# IDE0301 Use collection expression for empty
|
||||||
# From above, uses dotnet_style_prefer_collection_expression
|
# From above, uses dotnet_style_prefer_collection_expression
|
||||||
dotnet_diagnostic.IDE0301.severity = silent # Requires C# 12
|
dotnet_diagnostic.IDE0301.severity = warning
|
||||||
|
|
||||||
# IDE0302 Use collection expression for stackalloc
|
# IDE0302 Use collection expression for stackalloc
|
||||||
# From above, uses dotnet_style_prefer_collection_expression
|
# From above, uses dotnet_style_prefer_collection_expression
|
||||||
dotnet_diagnostic.IDE0302.severity = silent # Requires C# 12
|
dotnet_diagnostic.IDE0302.severity = warning
|
||||||
|
|
||||||
# IDE0303 Use collection expression for 'Create()'
|
# IDE0303 Use collection expression for 'Create()'
|
||||||
# From above, uses dotnet_style_prefer_collection_expression
|
# From above, uses dotnet_style_prefer_collection_expression
|
||||||
dotnet_diagnostic.IDE0303.severity = silent # Requires C# 12
|
dotnet_diagnostic.IDE0303.severity = warning
|
||||||
|
|
||||||
# IDE0304 Use collection expression for builder
|
# IDE0304 Use collection expression for builder
|
||||||
# From above, uses dotnet_style_prefer_collection_expression
|
# From above, uses dotnet_style_prefer_collection_expression
|
||||||
dotnet_diagnostic.IDE0304.severity = silent # Requires C# 12
|
dotnet_diagnostic.IDE0304.severity = warning
|
||||||
|
|
||||||
# IDE0305 Use collection expression for fluent
|
# IDE0305 Use collection expression for fluent
|
||||||
# From above, uses dotnet_style_prefer_collection_expression
|
# From above, uses dotnet_style_prefer_collection_expression
|
||||||
dotnet_diagnostic.IDE0305.severity = silent # Requires C# 12
|
dotnet_diagnostic.IDE0305.severity = silent
|
||||||
|
|
||||||
|
# IDE0306 Use collection expression for new
|
||||||
|
# From above, uses dotnet_style_prefer_collection_expression
|
||||||
|
dotnet_diagnostic.IDE0306.severity = warning
|
||||||
|
|
||||||
|
# IDE0340 Use unbound generic type
|
||||||
|
#csharp_style_prefer_unbound_generic_type_in_nameof = true
|
||||||
|
dotnet_diagnostic.IDE0340.severity = silent # Requires C# 14
|
||||||
|
|
||||||
|
# IDE0350 Use implicitly typed lambda
|
||||||
|
#csharp_style_prefer_implicitly_typed_lambda_expression = true
|
||||||
|
dotnet_diagnostic.IDE0350.severity = warning
|
||||||
|
|
||||||
|
# IDE0360 Simplify property accessor
|
||||||
|
#csharp_style_prefer_simple_property_accessors = true
|
||||||
|
dotnet_diagnostic.IDE0360.severity = silent # Requires C# 13
|
||||||
|
|
||||||
## Field preferences
|
## Field preferences
|
||||||
|
|
||||||
@@ -336,7 +356,7 @@ dotnet_diagnostic.IDE0305.severity = silent # Requires C# 12
|
|||||||
#dotnet_style_readonly_field = true
|
#dotnet_style_readonly_field = true
|
||||||
dotnet_diagnostic.IDE0044.severity = warning
|
dotnet_diagnostic.IDE0044.severity = warning
|
||||||
|
|
||||||
## Language keyword vs. framework types preferences
|
## Language keyword vs framework types preferences
|
||||||
|
|
||||||
# IDE0049 Use language keywords instead of framework type names for type references
|
# IDE0049 Use language keywords instead of framework type names for type references
|
||||||
#dotnet_style_predefined_type_for_locals_parameters_members = true
|
#dotnet_style_predefined_type_for_locals_parameters_members = true
|
||||||
@@ -373,6 +393,10 @@ dotnet_diagnostic.IDE0251.severity = warning
|
|||||||
#csharp_prefer_static_anonymous_function = true
|
#csharp_prefer_static_anonymous_function = true
|
||||||
dotnet_diagnostic.IDE0320.severity = warning
|
dotnet_diagnostic.IDE0320.severity = warning
|
||||||
|
|
||||||
|
# IDE0380 Remove unnecessary 'unsafe' modifier
|
||||||
|
# No options
|
||||||
|
dotnet_diagnostic.IDE0380.severity = warning
|
||||||
|
|
||||||
## Null-checking preferences
|
## Null-checking preferences
|
||||||
|
|
||||||
# IDE1005 Use conditional delegate call
|
# IDE1005 Use conditional delegate call
|
||||||
@@ -387,7 +411,7 @@ dotnet_diagnostic.IDE0060.severity = warning
|
|||||||
|
|
||||||
# IDE0280 Use 'nameof'
|
# IDE0280 Use 'nameof'
|
||||||
# No options
|
# No options
|
||||||
dotnet_diagnostic.IDE0280.severity = silent # Requires C# 11
|
dotnet_diagnostic.IDE0280.severity = warning
|
||||||
|
|
||||||
## Parentheses preferences
|
## Parentheses preferences
|
||||||
|
|
||||||
@@ -426,7 +450,7 @@ dotnet_diagnostic.IDE0083.severity = warning
|
|||||||
|
|
||||||
# IDE0170 Simplify property pattern
|
# IDE0170 Simplify property pattern
|
||||||
#csharp_style_prefer_extended_property_pattern = true
|
#csharp_style_prefer_extended_property_pattern = true
|
||||||
dotnet_diagnostic.IDE0170.severity = silent # Requires C# 10
|
dotnet_diagnostic.IDE0170.severity = warning
|
||||||
|
|
||||||
## Suppression preferences
|
## Suppression preferences
|
||||||
|
|
||||||
@@ -434,6 +458,10 @@ dotnet_diagnostic.IDE0170.severity = silent # Requires C# 10
|
|||||||
#dotnet_remove_unnecessary_suppression_exclusions = none
|
#dotnet_remove_unnecessary_suppression_exclusions = none
|
||||||
dotnet_diagnostic.IDE0079.severity = warning
|
dotnet_diagnostic.IDE0079.severity = warning
|
||||||
|
|
||||||
|
# IDE0370 Remove unnecessary suppression (null-forgiving operator)
|
||||||
|
# No options
|
||||||
|
dotnet_diagnostic.IDE0370.severity = warning
|
||||||
|
|
||||||
## 'this' and 'Me' preferences
|
## 'this' and 'Me' preferences
|
||||||
|
|
||||||
# IDE0003/IDE0009 Remove 'this' or 'Me' qualification/Add 'this' or 'Me' qualification
|
# IDE0003/IDE0009 Remove 'this' or 'Me' qualification/Add 'this' or 'Me' qualification
|
||||||
@@ -748,7 +776,7 @@ dotnet_diagnostic.CA1305.severity = warning
|
|||||||
dotnet_diagnostic.CA1310.severity = warning
|
dotnet_diagnostic.CA1310.severity = warning
|
||||||
|
|
||||||
# Specify a culture or use an invariant version.
|
# Specify a culture or use an invariant version.
|
||||||
dotnet_diagnostic.CA1311.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA1311.severity = warning
|
||||||
|
|
||||||
### Portability and Interoperability Rules
|
### Portability and Interoperability Rules
|
||||||
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/interoperability-warnings
|
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/interoperability-warnings
|
||||||
@@ -763,19 +791,25 @@ dotnet_diagnostic.CA1417.severity = warning
|
|||||||
dotnet_diagnostic.CA1507.severity = warning
|
dotnet_diagnostic.CA1507.severity = warning
|
||||||
|
|
||||||
# Use ArgumentNullException throw helper.
|
# Use ArgumentNullException throw helper.
|
||||||
dotnet_diagnostic.CA1510.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA1510.severity = warning
|
||||||
|
|
||||||
# Use ArgumentException throw helper.
|
# Use ArgumentException throw helper.
|
||||||
dotnet_diagnostic.CA1511.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA1511.severity = warning
|
||||||
|
|
||||||
# Use ArgumentOutOfRangeException throw helper.
|
# Use ArgumentOutOfRangeException throw helper.
|
||||||
dotnet_diagnostic.CA1512.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
|
dotnet_diagnostic.CA1512.severity = warning
|
||||||
|
|
||||||
# Use ObjectDisposedException throw helper.
|
# Use ObjectDisposedException throw helper.
|
||||||
dotnet_diagnostic.CA1513.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA1513.severity = warning
|
||||||
|
|
||||||
# Avoid redundant length argument.
|
# Avoid redundant length argument.
|
||||||
dotnet_diagnostic.CA1514.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
|
dotnet_diagnostic.CA1514.severity = warning
|
||||||
|
|
||||||
|
# Consider making public types internal.
|
||||||
|
dotnet_diagnostic.CA1515.severity = warning
|
||||||
|
|
||||||
|
# Use cross-platform intrinsics.
|
||||||
|
dotnet_diagnostic.CA1516.severity = warning
|
||||||
|
|
||||||
### Naming Rules
|
### Naming Rules
|
||||||
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/naming-warnings
|
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/naming-warnings
|
||||||
@@ -868,8 +902,8 @@ dotnet_diagnostic.CA1843.severity = warning
|
|||||||
# Provide memory-based overrides of async methods when subclassing 'Stream'.
|
# Provide memory-based overrides of async methods when subclassing 'Stream'.
|
||||||
dotnet_diagnostic.CA1844.severity = warning
|
dotnet_diagnostic.CA1844.severity = warning
|
||||||
|
|
||||||
# Use span-based 'string.Concat'. (Not available on mono)
|
# Use span-based 'string.Concat'.
|
||||||
dotnet_diagnostic.CA1845.severity = none
|
dotnet_diagnostic.CA1845.severity = warning
|
||||||
|
|
||||||
# Prefer AsSpan over Substring.
|
# Prefer AsSpan over Substring.
|
||||||
dotnet_diagnostic.CA1846.severity = warning
|
dotnet_diagnostic.CA1846.severity = warning
|
||||||
@@ -878,34 +912,34 @@ dotnet_diagnostic.CA1846.severity = warning
|
|||||||
dotnet_diagnostic.CA1847.severity = warning
|
dotnet_diagnostic.CA1847.severity = warning
|
||||||
|
|
||||||
# Call async methods when in an async method.
|
# Call async methods when in an async method.
|
||||||
dotnet_diagnostic.CA1849.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA1849.severity = warning
|
||||||
|
|
||||||
# Prefer static HashData method over ComputeHash. (Not available on mono)
|
# Prefer static HashData method over ComputeHash.
|
||||||
dotnet_diagnostic.CA1850.severity = none # TODO: Change to warning once using .NET 7 or later AND once supported by mono.
|
dotnet_diagnostic.CA1850.severity = warning
|
||||||
|
|
||||||
# Possible multiple enumerations of IEnumerable collection.
|
# Possible multiple enumerations of IEnumerable collection.
|
||||||
#dotnet_code_quality.CA1851.enumeration_methods =
|
#dotnet_code_quality.CA1851.enumeration_methods =
|
||||||
dotnet_code_quality.CA1851.linq_chain_methods = M:OpenRA.Traits.IRenderModifier.Modify*
|
dotnet_code_quality.CA1851.linq_chain_methods = M:OpenRA.Traits.IRenderModifier.Modify*
|
||||||
dotnet_code_quality.CA1851.assume_method_enumerates_parameters = true
|
dotnet_code_quality.CA1851.assume_method_enumerates_parameters = true
|
||||||
dotnet_diagnostic.CA1851.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA1851.severity = warning
|
||||||
|
|
||||||
# Seal internal types.
|
# Seal internal types.
|
||||||
dotnet_diagnostic.CA1852.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA1852.severity = warning
|
||||||
|
|
||||||
# Unnecessary call to 'Dictionary.ContainsKey(key)'.
|
# Unnecessary call to 'Dictionary.ContainsKey(key)'.
|
||||||
dotnet_diagnostic.CA1853.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA1853.severity = warning
|
||||||
|
|
||||||
# Prefer the IDictionary.TryGetValue(TKey, out TValue) method.
|
# Prefer the IDictionary.TryGetValue(TKey, out TValue) method.
|
||||||
dotnet_diagnostic.CA1854.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA1854.severity = warning
|
||||||
|
|
||||||
# Use Span<T>.Clear() instead of Span<T>.Fill().
|
# Use Span<T>.Clear() instead of Span<T>.Fill().
|
||||||
dotnet_diagnostic.CA1855.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA1855.severity = warning
|
||||||
|
|
||||||
# Incorrect usage of ConstantExpected attribute.
|
# Incorrect usage of ConstantExpected attribute.
|
||||||
dotnet_diagnostic.CA1856.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA1856.severity = warning
|
||||||
|
|
||||||
# The parameter expects a constant for optimal performance.
|
# The parameter expects a constant for optimal performance.
|
||||||
dotnet_diagnostic.CA1857.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA1857.severity = warning
|
||||||
|
|
||||||
# Use StartsWith instead of IndexOf.
|
# Use StartsWith instead of IndexOf.
|
||||||
dotnet_diagnostic.CA1858.severity = warning
|
dotnet_diagnostic.CA1858.severity = warning
|
||||||
@@ -917,24 +951,24 @@ dotnet_diagnostic.CA1860.severity = warning
|
|||||||
dotnet_diagnostic.CA1862.severity = warning
|
dotnet_diagnostic.CA1862.severity = warning
|
||||||
|
|
||||||
# Use 'CompositeFormat'.
|
# Use 'CompositeFormat'.
|
||||||
dotnet_diagnostic.CA1863.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA1863.severity = warning
|
||||||
|
|
||||||
# Prefer the 'IDictionary.TryAdd(TKey, TValue)' method.
|
# Prefer the 'IDictionary.TryAdd(TKey, TValue)' method.
|
||||||
dotnet_diagnostic.CA1864.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
|
dotnet_diagnostic.CA1864.severity = warning
|
||||||
|
|
||||||
# Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char.
|
# Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char.
|
||||||
dotnet_diagnostic.CA1865.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
|
dotnet_diagnostic.CA1865.severity = warning
|
||||||
dotnet_diagnostic.CA1866.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
|
dotnet_diagnostic.CA1866.severity = warning
|
||||||
dotnet_diagnostic.CA1867.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
|
dotnet_diagnostic.CA1867.severity = warning
|
||||||
|
|
||||||
# Unnecessary call to 'Contains' for sets.
|
# Unnecessary call to 'Contains' for sets.
|
||||||
dotnet_diagnostic.CA1868.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
|
dotnet_diagnostic.CA1868.severity = warning
|
||||||
|
|
||||||
# Cache and reuse 'JsonSerializerOptions' instances.
|
# Cache and reuse 'JsonSerializerOptions' instances.
|
||||||
dotnet_diagnostic.CA1869.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
|
dotnet_diagnostic.CA1869.severity = warning
|
||||||
|
|
||||||
# Use a cached 'SearchValues' instance.
|
# Use a cached 'SearchValues' instance.
|
||||||
dotnet_diagnostic.CA1870.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
|
dotnet_diagnostic.CA1870.severity = warning
|
||||||
|
|
||||||
# Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull'.
|
# Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull'.
|
||||||
dotnet_diagnostic.CA1871.severity = warning
|
dotnet_diagnostic.CA1871.severity = warning
|
||||||
@@ -942,6 +976,18 @@ dotnet_diagnostic.CA1871.severity = warning
|
|||||||
# Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'.
|
# Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'.
|
||||||
dotnet_diagnostic.CA1872.severity = warning
|
dotnet_diagnostic.CA1872.severity = warning
|
||||||
|
|
||||||
|
# Avoid potentially expensive logging.
|
||||||
|
dotnet_diagnostic.CA1873.severity = warning
|
||||||
|
|
||||||
|
# Use 'Regex.IsMatch'.
|
||||||
|
dotnet_diagnostic.CA1874.severity = warning
|
||||||
|
|
||||||
|
# Use Use 'Regex.Count'.
|
||||||
|
dotnet_diagnostic.CA1875.severity = warning
|
||||||
|
|
||||||
|
# Use 'Path.Combine' or 'Path.Join' overloads.
|
||||||
|
dotnet_diagnostic.CA1877.severity = warning
|
||||||
|
|
||||||
### Reliability Rules
|
### Reliability Rules
|
||||||
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/reliability-warnings
|
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/reliability-warnings
|
||||||
|
|
||||||
@@ -964,7 +1010,7 @@ dotnet_diagnostic.CA2016.severity = warning
|
|||||||
dotnet_diagnostic.CA2018.severity = warning
|
dotnet_diagnostic.CA2018.severity = warning
|
||||||
|
|
||||||
# ThreadStatic fields should not use inline initialization.
|
# ThreadStatic fields should not use inline initialization.
|
||||||
dotnet_diagnostic.CA2019.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA2019.severity = warning
|
||||||
|
|
||||||
# Don't call Enumerable.Cast<T> or Enumerable.OfType<T> with incompatible types.
|
# Don't call Enumerable.Cast<T> or Enumerable.OfType<T> with incompatible types.
|
||||||
dotnet_diagnostic.CA2021.severity = warning
|
dotnet_diagnostic.CA2021.severity = warning
|
||||||
@@ -972,6 +1018,18 @@ dotnet_diagnostic.CA2021.severity = warning
|
|||||||
# Avoid inexact read with Stream.Read.
|
# Avoid inexact read with Stream.Read.
|
||||||
dotnet_diagnostic.CA2022.severity = warning
|
dotnet_diagnostic.CA2022.severity = warning
|
||||||
|
|
||||||
|
# Invalid braces in message template.
|
||||||
|
dotnet_diagnostic.CA2023.severity = warning
|
||||||
|
|
||||||
|
# Do not use StreamReader.EndOfStream in async methods.
|
||||||
|
dotnet_diagnostic.CA2024.severity = warning
|
||||||
|
|
||||||
|
# Do not pass 'IDisposable' instances into unawaited tasks.
|
||||||
|
dotnet_diagnostic.CA2025.severity = warning
|
||||||
|
|
||||||
|
# Prefer JsonElement.Parse over JsonDocument.Parse().RootElement.
|
||||||
|
dotnet_diagnostic.CA2026.severity = warning
|
||||||
|
|
||||||
### Security Rules
|
### Security Rules
|
||||||
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/security-warnings
|
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/security-warnings
|
||||||
|
|
||||||
@@ -1041,10 +1099,10 @@ dotnet_diagnostic.CA2250.severity = warning
|
|||||||
dotnet_diagnostic.CA2251.severity = warning
|
dotnet_diagnostic.CA2251.severity = warning
|
||||||
|
|
||||||
# Ensure ThreadStatic is only used with static fields.
|
# Ensure ThreadStatic is only used with static fields.
|
||||||
dotnet_diagnostic.CA2259.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
|
dotnet_diagnostic.CA2259.severity = warning
|
||||||
|
|
||||||
# Prefer generic overload when type is known.
|
# Prefer generic overload when type is known.
|
||||||
dotnet_diagnostic.CA2263.severity = none # TODO: Change to warning once mono is dropped.
|
dotnet_diagnostic.CA2263.severity = warning
|
||||||
|
|
||||||
# Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull'.
|
# Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull'.
|
||||||
dotnet_diagnostic.CA2264.severity = warning
|
dotnet_diagnostic.CA2264.severity = warning
|
||||||
@@ -1260,3 +1318,24 @@ dotnet_diagnostic.RCS1239.severity = warning
|
|||||||
|
|
||||||
# Use element access.
|
# Use element access.
|
||||||
dotnet_diagnostic.RCS1246.severity = warning
|
dotnet_diagnostic.RCS1246.severity = warning
|
||||||
|
|
||||||
|
# Use enum field explicitly.
|
||||||
|
dotnet_diagnostic.RCS1257.severity = warning
|
||||||
|
|
||||||
|
# Unnecessary enum flag.
|
||||||
|
dotnet_diagnostic.RCS1258.severity = warning
|
||||||
|
|
||||||
|
# Resource can be disposed asynchronously.
|
||||||
|
dotnet_diagnostic.RCS1261.severity = warning
|
||||||
|
|
||||||
|
# Unnecessary raw string literal.
|
||||||
|
dotnet_diagnostic.RCS1262.severity = warning
|
||||||
|
|
||||||
|
# Remove redundant catch block.
|
||||||
|
dotnet_diagnostic.RCS1265.severity = warning
|
||||||
|
|
||||||
|
# Use raw string literal.
|
||||||
|
dotnet_diagnostic.RCS1266.severity = warning
|
||||||
|
|
||||||
|
# Use string interpolation instead of 'string.Concat'.
|
||||||
|
dotnet_diagnostic.RCS1267.severity = warning
|
||||||
|
|||||||
1
.github/ISSUE_TEMPLATE/crash-report.md
vendored
1
.github/ISSUE_TEMPLATE/crash-report.md
vendored
@@ -16,7 +16,6 @@ assignees: ''
|
|||||||
- OpenRA Version: ENGINE VERSION
|
- OpenRA Version: ENGINE VERSION
|
||||||
- OpenRA Mod: GAME MOD
|
- OpenRA Mod: GAME MOD
|
||||||
- Source: Official download package OR self-compiled OR third-party package
|
- Source: Official download package OR self-compiled OR third-party package
|
||||||
- For self-compiled or third-party packages: Mono version
|
|
||||||
|
|
||||||
|
|
||||||
## Exception log
|
## Exception log
|
||||||
|
|||||||
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
47
.github/workflows/ci.yml
vendored
47
.github/workflows/ci.yml
vendored
@@ -2,25 +2,30 @@ name: Continuous Integration
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- '*.md'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ bleed, 'prep-*' ]
|
branches: [ bleed, 'prep-*' ]
|
||||||
|
paths-ignore:
|
||||||
|
- '*.md'
|
||||||
|
- '*.py'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # to fetch code (actions/checkout)
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
linux:
|
linux:
|
||||||
name: Linux (.NET 6.0)
|
name: Linux (.NET 8.0)
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Repository
|
- name: Clone Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install .NET 6.0
|
- name: Install .NET 8.0
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: '6.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
- name: Check Code
|
- name: Check Code
|
||||||
run: |
|
run: |
|
||||||
@@ -33,36 +38,18 @@ jobs:
|
|||||||
make check-scripts
|
make check-scripts
|
||||||
make TREAT_WARNINGS_AS_ERRORS=true test
|
make TREAT_WARNINGS_AS_ERRORS=true test
|
||||||
|
|
||||||
linux-mono:
|
|
||||||
name: Linux (mono)
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Clone Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Check Code
|
|
||||||
run: |
|
|
||||||
mono --version
|
|
||||||
make RUNTIME=mono check
|
|
||||||
|
|
||||||
- name: Check Mods
|
|
||||||
run: |
|
|
||||||
# check-scripts does not depend on .net/mono, so is not needed here
|
|
||||||
make RUNTIME=mono TREAT_WARNINGS_AS_ERRORS=true test
|
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
name: Windows (.NET 6.0)
|
name: Windows (.NET 8.0)
|
||||||
runs-on: windows-2019
|
runs-on: windows-2022
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Repository
|
- name: Clone Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install .NET 6.0
|
- name: Install .NET 8.0
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: '6.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
- name: Check Code
|
- name: Check Code
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@@ -74,7 +61,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check Mods
|
- name: Check Mods
|
||||||
run: |
|
run: |
|
||||||
choco install lua --version 5.1.5.52
|
choco install lua --version 5.1.5.52 --no-progress
|
||||||
$ENV:Path = $ENV:Path + ";C:\Program Files (x86)\Lua\5.1\"
|
$ENV:Path = $ENV:Path + ";C:\Program Files (x86)\Lua\5.1\"
|
||||||
$ENV:TREAT_WARNINGS_AS_ERRORS = "true"
|
$ENV:TREAT_WARNINGS_AS_ERRORS = "true"
|
||||||
.\make.ps1 check-scripts
|
.\make.ps1 check-scripts
|
||||||
|
|||||||
20
.github/workflows/documentation.yml
vendored
20
.github/workflows/documentation.yml
vendored
@@ -52,21 +52,21 @@ jobs:
|
|||||||
echo ${{ needs.prepare.outputs.version_type }}
|
echo ${{ needs.prepare.outputs.version_type }}
|
||||||
|
|
||||||
- name: Clone Repository
|
- name: Clone Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.prepare.outputs.git_tag }}
|
ref: ${{ needs.prepare.outputs.git_tag }}
|
||||||
|
|
||||||
- name: Install .NET 6
|
- name: Install .NET 8
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: '6.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
- name: Prepare Environment
|
- name: Prepare Environment
|
||||||
run: |
|
run: |
|
||||||
make all
|
make all
|
||||||
|
|
||||||
- name: Clone Wiki
|
- name: Clone Wiki
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: openra/openra.wiki
|
repository: openra/openra.wiki
|
||||||
token: ${{ secrets.DOCS_TOKEN }}
|
token: ${{ secrets.DOCS_TOKEN }}
|
||||||
@@ -107,14 +107,14 @@ jobs:
|
|||||||
echo ${{ needs.prepare.outputs.version_type }}
|
echo ${{ needs.prepare.outputs.version_type }}
|
||||||
|
|
||||||
- name: Clone Repository
|
- name: Clone Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.prepare.outputs.git_tag }}
|
ref: ${{ needs.prepare.outputs.git_tag }}
|
||||||
|
|
||||||
- name: Install .NET 6
|
- name: Install .NET 8
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: '6.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
- name: Prepare Environment
|
- name: Prepare Environment
|
||||||
run: |
|
run: |
|
||||||
@@ -122,7 +122,7 @@ jobs:
|
|||||||
|
|
||||||
# version_type is release/playtest/bleed - the name of the target branch.
|
# version_type is release/playtest/bleed - the name of the target branch.
|
||||||
- name: Clone docs.openra.net
|
- name: Clone docs.openra.net
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: openra/docs
|
repository: openra/docs
|
||||||
token: ${{ secrets.DOCS_TOKEN }}
|
token: ${{ secrets.DOCS_TOKEN }}
|
||||||
|
|||||||
85
.github/workflows/itch.yml
vendored
85
.github/workflows/itch.yml
vendored
@@ -15,73 +15,56 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
if: github.repository == 'openra/openra'
|
if: github.repository == 'openra/openra'
|
||||||
steps:
|
steps:
|
||||||
- name: Download Packages
|
- name: Download Butler
|
||||||
|
env:
|
||||||
|
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||||
run: |
|
run: |
|
||||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}-x64.exe"
|
wget -cq -O butler-linux-amd64.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
|
||||||
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"
|
unzip butler-linux-amd64.zip
|
||||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}.dmg"
|
rm butler-linux-amd64.zip
|
||||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Dune-2000-x86_64.AppImage"
|
chmod +x butler
|
||||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Red-Alert-x86_64.AppImage"
|
./butler -V
|
||||||
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Tiberian-Dawn-x86_64.AppImage"
|
./butler login
|
||||||
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
|
|
||||||
|
|
||||||
- name: Publish Windows Installer
|
- name: Publish Windows Installer
|
||||||
uses: josephbmanley/butler-publish-itchio-action@master
|
|
||||||
env:
|
env:
|
||||||
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
|
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||||
CHANNEL: win
|
run: |
|
||||||
ITCH_GAME: openra
|
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}-x64.exe"
|
||||||
ITCH_USER: openra
|
./butler push "OpenRA-${{ github.event.inputs.tag }}-x64.exe" "openra/openra:win" --userversion ${{ github.event.inputs.tag }}
|
||||||
VERSION: ${{ github.event.inputs.tag }}
|
|
||||||
PACKAGE: OpenRA-${{ github.event.inputs.tag }}-x64.exe
|
|
||||||
|
|
||||||
- name: Publish Windows Itch Bundle
|
- name: Publish Windows Itch Bundle
|
||||||
uses: josephbmanley/butler-publish-itchio-action@master
|
|
||||||
env:
|
env:
|
||||||
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
|
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||||
CHANNEL: itch
|
run: |
|
||||||
ITCH_GAME: openra
|
wget -q "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.event.inputs.tag }}/packaging/.itch.toml"
|
||||||
ITCH_USER: openra
|
zip -u "OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip" .itch.toml
|
||||||
VERSION: ${{ github.event.inputs.tag }}
|
./butler push "OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip" "openra/openra:itch" --userversion ${{ github.event.inputs.tag }}
|
||||||
PACKAGE: OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip
|
|
||||||
|
|
||||||
- name: Publish macOS Package
|
- name: Publish macOS Package
|
||||||
uses: josephbmanley/butler-publish-itchio-action@master
|
|
||||||
env:
|
env:
|
||||||
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
|
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||||
CHANNEL: macos
|
run: |
|
||||||
ITCH_GAME: openra
|
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}.dmg"
|
||||||
ITCH_USER: openra
|
./butler push "OpenRA-${{ github.event.inputs.tag }}.dmg" "openra/openra:macos" --userversion ${{ github.event.inputs.tag }}
|
||||||
VERSION: ${{ github.event.inputs.tag }}
|
|
||||||
PACKAGE: OpenRA-${{ github.event.inputs.tag }}.dmg
|
|
||||||
|
|
||||||
- name: Publish RA AppImage
|
- name: Publish RA AppImage
|
||||||
uses: josephbmanley/butler-publish-itchio-action@master
|
|
||||||
env:
|
env:
|
||||||
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
|
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||||
CHANNEL: linux-ra
|
run: |
|
||||||
ITCH_GAME: openra
|
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Red-Alert-x86_64.AppImage"
|
||||||
ITCH_USER: openra
|
./butler push "OpenRA-Red-Alert-x86_64.AppImage" "openra/openra:linux-ra" --userversion ${{ github.event.inputs.tag }}
|
||||||
VERSION: ${{ github.event.inputs.tag }}
|
|
||||||
PACKAGE: OpenRA-Red-Alert-x86_64.AppImage
|
|
||||||
|
|
||||||
- name: Publish TD AppImage
|
- name: Publish TD AppImage
|
||||||
uses: josephbmanley/butler-publish-itchio-action@master
|
|
||||||
env:
|
env:
|
||||||
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
|
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||||
CHANNEL: linux-cnc
|
run: |
|
||||||
ITCH_GAME: openra
|
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Tiberian-Dawn-x86_64.AppImage"
|
||||||
ITCH_USER: openra
|
./butler push "OpenRA-Tiberian-Dawn-x86_64.AppImage" "openra/openra:linux-cnc" --userversion ${{ github.event.inputs.tag }}
|
||||||
VERSION: ${{ github.event.inputs.tag }}
|
|
||||||
PACKAGE: OpenRA-Tiberian-Dawn-x86_64.AppImage
|
|
||||||
|
|
||||||
- name: Publish D2k AppImage
|
- name: Publish D2k AppImage
|
||||||
uses: josephbmanley/butler-publish-itchio-action@master
|
|
||||||
env:
|
env:
|
||||||
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
|
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
|
||||||
CHANNEL: linux-d2k
|
run: |
|
||||||
ITCH_GAME: openra
|
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Dune-2000-x86_64.AppImage"
|
||||||
ITCH_USER: openra
|
./butler push "OpenRA-Dune-2000-x86_64.AppImage" "openra/openra:linux-d2k" --userversion ${{ github.event.inputs.tag }}
|
||||||
VERSION: ${{ github.event.inputs.tag }}
|
|
||||||
PACKAGE: OpenRA-Dune-2000-x86_64.AppImage
|
|
||||||
|
|||||||
92
.github/workflows/packaging.yml
vendored
92
.github/workflows/packaging.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Repository
|
- name: Clone Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Prepare Environment
|
- name: Prepare Environment
|
||||||
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
|
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
|
||||||
@@ -40,12 +40,12 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Repository
|
- name: Clone Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install .NET 6.0
|
- name: Install .NET 8.0
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: '6.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
- name: Prepare Environment
|
- name: Prepare Environment
|
||||||
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
|
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
|
||||||
@@ -57,25 +57,23 @@ jobs:
|
|||||||
./packaging/linux/buildpackage.sh "${GIT_TAG}" "${PWD}/build/linux"
|
./packaging/linux/buildpackage.sh "${GIT_TAG}" "${PWD}/build/linux"
|
||||||
|
|
||||||
- name: Upload Packages
|
- name: Upload Packages
|
||||||
uses: svenstaro/upload-release-action@v2
|
env:
|
||||||
with:
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
shell: bash
|
||||||
tag: ${{ github.ref }}
|
run: |
|
||||||
overwrite: true
|
gh release upload ${{ github.ref_name }} build/linux/*
|
||||||
file_glob: true
|
|
||||||
file: build/linux/*
|
|
||||||
|
|
||||||
macos:
|
macos:
|
||||||
name: macOS Disk Image
|
name: macOS Disk Image
|
||||||
runs-on: macos-13
|
runs-on: macos-14
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Repository
|
- name: Clone Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install .NET 6.0
|
- name: Install .NET 8.0
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: '6.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
- name: Prepare Environment
|
- name: Prepare Environment
|
||||||
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
|
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
|
||||||
@@ -92,25 +90,23 @@ jobs:
|
|||||||
./packaging/macos/buildpackage.sh "${GIT_TAG}" "${PWD}/build/macos"
|
./packaging/macos/buildpackage.sh "${GIT_TAG}" "${PWD}/build/macos"
|
||||||
|
|
||||||
- name: Upload Package
|
- name: Upload Package
|
||||||
uses: svenstaro/upload-release-action@v2
|
env:
|
||||||
with:
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
shell: bash
|
||||||
tag: ${{ github.ref }}
|
run: |
|
||||||
overwrite: true
|
gh release upload ${{ github.ref_name }} build/macos/*
|
||||||
file_glob: true
|
|
||||||
file: build/macos/*
|
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
name: Windows Installers
|
name: Windows Installers
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Repository
|
- name: Clone Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install .NET 6.0
|
- name: Install .NET 8.0
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: '6.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
- name: Prepare Environment
|
- name: Prepare Environment
|
||||||
run: |
|
run: |
|
||||||
@@ -123,11 +119,37 @@ jobs:
|
|||||||
mkdir -p build/windows
|
mkdir -p build/windows
|
||||||
./packaging/windows/buildpackage.sh "${GIT_TAG}" "${PWD}/build/windows"
|
./packaging/windows/buildpackage.sh "${GIT_TAG}" "${PWD}/build/windows"
|
||||||
|
|
||||||
- name: Upload Packages
|
- name: Upload Installer
|
||||||
uses: svenstaro/upload-release-action@v2
|
id: unsigned-artifact
|
||||||
|
if: github.repository == 'openra/openra'
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
path: build/windows/*.exe
|
||||||
tag: ${{ github.ref }}
|
|
||||||
overwrite: true
|
- name: Microsoft Authenticode
|
||||||
file_glob: true
|
id: signpath
|
||||||
file: build/windows/*
|
if: github.repository == 'openra/openra'
|
||||||
|
uses: signpath/github-action-submit-signing-request@v2
|
||||||
|
with:
|
||||||
|
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||||
|
organization-id: '${{ secrets.SIGNPATH_ORGANISATION_ID }}'
|
||||||
|
project-slug: 'OpenRA'
|
||||||
|
signing-policy-slug: 'release-signing'
|
||||||
|
github-artifact-id: '${{ steps.unsigned-artifact.outputs.artifact-id }}'
|
||||||
|
wait-for-completion: true
|
||||||
|
output-artifact-directory: 'build/windows/signed/'
|
||||||
|
parameters: |
|
||||||
|
version: ${{ toJSON(github.ref_name) }}
|
||||||
|
|
||||||
|
- name: Upload Packages
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
gh release upload "${{ github.ref_name }}" build/windows/*.zip
|
||||||
|
|
||||||
|
if [ "${GITHUB_REPOSITORY}" = "openra/openra" ]; then
|
||||||
|
gh release upload "${{ github.ref_name }}" build/windows/signed/*.exe
|
||||||
|
else
|
||||||
|
gh release upload "${{ github.ref_name }}" build/windows/*.exe
|
||||||
|
fi
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -25,10 +25,6 @@ IP2LOCATION-LITE-DB1.IPV6.BIN.ZIP
|
|||||||
\#*
|
\#*
|
||||||
.*.sw?
|
.*.sw?
|
||||||
|
|
||||||
# Monodevelop
|
|
||||||
*.pidb
|
|
||||||
*.userprefs
|
|
||||||
|
|
||||||
# Mac OS X
|
# Mac OS X
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
|||||||
3
AUTHORS
3
AUTHORS
@@ -36,6 +36,7 @@ Also thanks to:
|
|||||||
* Andreas Beck (baxtor)
|
* Andreas Beck (baxtor)
|
||||||
* Ang Soon Li (asl97)
|
* Ang Soon Li (asl97)
|
||||||
* Arik Lirette (Angusm3)
|
* Arik Lirette (Angusm3)
|
||||||
|
* Ashley Newson
|
||||||
* Barnaby Smith (mvi)
|
* Barnaby Smith (mvi)
|
||||||
* Bellator
|
* Bellator
|
||||||
* Bernd Stellwag (burned42)
|
* Bernd Stellwag (burned42)
|
||||||
@@ -54,6 +55,7 @@ Also thanks to:
|
|||||||
* Constantin Helmig (CH4Code)
|
* Constantin Helmig (CH4Code)
|
||||||
* D2k Sardaukar
|
* D2k Sardaukar
|
||||||
* D'Arcy Rush (r34ch)
|
* D'Arcy Rush (r34ch)
|
||||||
|
* Dan Stoian (stoiandan)
|
||||||
* Daniel Derejvanik (Harisson)
|
* Daniel Derejvanik (Harisson)
|
||||||
* Danny Keary (Dan9550)
|
* Danny Keary (Dan9550)
|
||||||
* David Jiménez (Rydra)
|
* David Jiménez (Rydra)
|
||||||
@@ -141,6 +143,7 @@ Also thanks to:
|
|||||||
* Riderr3
|
* Riderr3
|
||||||
* riiga
|
* riiga
|
||||||
* Rikhardur Bjarni Einarsson (WolfGaming)
|
* Rikhardur Bjarni Einarsson (WolfGaming)
|
||||||
|
* Robert Nordan (robpvn)
|
||||||
* Sascha Biedermann (bidifx)
|
* Sascha Biedermann (bidifx)
|
||||||
* Sean Hunt (coppro)
|
* Sean Hunt (coppro)
|
||||||
* Shawn Collins (UberWaffe)
|
* Shawn Collins (UberWaffe)
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ Don't forget to add yourself to [AUTHORS](https://github.com/OpenRA/OpenRA/blob/
|
|||||||
|
|
||||||
Please propose a [CHANGELOG](https://github.com/OpenRA/OpenRA/wiki/CHANGELOG) entry in the pull-request comments.
|
Please propose a [CHANGELOG](https://github.com/OpenRA/OpenRA/wiki/CHANGELOG) entry in the pull-request comments.
|
||||||
|
|
||||||
While your pull-request is in review it will be helpful if you join [IRC](irc://chat.freenode.net/openra) to discuss the changes.
|
While your pull-request is in review it will be helpful if you join [Discord](https://discord.openra.net) or [IRC](ircs://irc.libera.chat:6697/openra) to discuss the changes.
|
||||||
|
|
||||||
See also the in-depth guide on [contributing](https://github.com/OpenRA/OpenRA/wiki/Contributing) to the OpenRA project.
|
See also the in-depth guide on [contributing](https://github.com/OpenRA/OpenRA/wiki/Contributing) to the OpenRA project.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<Optimize>true</Optimize>
|
<Optimize>true</Optimize>
|
||||||
<LangVersion>9</LangVersion>
|
<LangVersion>12</LangVersion>
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<DebugSymbols>true</DebugSymbols>
|
||||||
<EngineRootPath Condition="'$(EngineRootPath)' == ''">..</EngineRootPath>
|
<EngineRootPath Condition="'$(EngineRootPath)' == ''">..</EngineRootPath>
|
||||||
<OutputPath>$(EngineRootPath)/bin</OutputPath>
|
<OutputPath>$(EngineRootPath)/bin</OutputPath>
|
||||||
@@ -18,8 +18,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework Condition="'$(MSBuildRuntimeType)'!='Mono'">net6.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<TargetFramework Condition="'$(MSBuildRuntimeType)'=='Mono'">netstandard2.1</TargetFramework>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@@ -53,9 +52,8 @@
|
|||||||
|
|
||||||
<!-- StyleCop/Roslynator -->
|
<!-- StyleCop/Roslynator -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="All" />
|
||||||
<!-- Roslynator analyzers fail to run under Mono (AD0001) -->
|
<PackageReference Include="Roslynator.Analyzers" Version="4.13.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="Roslynator.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
|
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.13.0" PrivateAssets="All" />
|
||||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
apt-get update; \
|
||||||
|
apt-get -y upgrade; \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
make \
|
||||||
|
python3 \
|
||||||
|
unzip \
|
||||||
|
mono-complete
|
||||||
|
|
||||||
|
|
||||||
|
RUN useradd -d /home/openra -m -s /sbin/nologin openra
|
||||||
|
|
||||||
|
WORKDIR /home/openra
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN chown -R openra:openra .
|
||||||
|
|
||||||
|
USER openra
|
||||||
|
|
||||||
|
RUN make
|
||||||
|
|
||||||
|
EXPOSE 1234
|
||||||
|
|
||||||
|
ENTRYPOINT ["./launch-dedicated.sh"]
|
||||||
12
INSTALL.md
12
INSTALL.md
@@ -8,7 +8,7 @@ Windows
|
|||||||
|
|
||||||
Compiling OpenRA requires the following dependencies:
|
Compiling OpenRA requires the following dependencies:
|
||||||
* [Windows PowerShell >= 4.0](https://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)
|
* [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) (or via Visual Studio)
|
||||||
|
|
||||||
To compile OpenRA, open the `OpenRA.sln` solution in the main folder, build it from the command-line with `dotnet` or use the Makefile analogue command `make all` scripted in PowerShell syntax.
|
To compile OpenRA, open the `OpenRA.sln` solution in the main folder, build it from the command-line with `dotnet` or use the Makefile analogue command `make all` scripted in PowerShell syntax.
|
||||||
|
|
||||||
@@ -17,11 +17,9 @@ Run the game with `launch-game.cmd`. It can be handed arguments that specify the
|
|||||||
Linux
|
Linux
|
||||||
=====
|
=====
|
||||||
|
|
||||||
.NET 6 or Mono (version 6.12 or later) is required to compile OpenRA. We recommend using .NET 6 when possible, as Mono is poorly packaged by most Linux distributions (e.g. missing the required `msbuild` toolchain), and has been deprecated as a standalone project.
|
.NET 8 is required to compile OpenRA. The [.NET 8 download page](https://dotnet.microsoft.com/download/dotnet/8.0) provides repositories for various package managers and binary releases for several architectures.
|
||||||
|
|
||||||
The [.NET 6 download page](https://dotnet.microsoft.com/download/dotnet/6.0) provides repositories for various package managers and binary releases for several architectures. If you prefer to use Mono, we suggest adding the [upstream repository](https://www.mono-project.com/download/stable/#download-lin) for your distro to obtain the latest version and the `msbuild` toolchain.
|
To compile OpenRA, run `make` from the command line. After this one can run the game with `./launch-game.sh`. It is also possible to specify the mod you wish to run from the command line, e.g. with `./launch-game.sh Game.Mod=ts` if you wish to try the experimental Tiberian Sun mod.
|
||||||
|
|
||||||
To compile OpenRA, run `make` from the command line (or `make RUNTIME=mono` if using Mono). After this one can run the game with `./launch-game.sh`. It is also possible to specify the mod you wish to run from the command line, e.g. with `./launch-game.sh Game.Mod=ts` if you wish to try the experimental Tiberian Sun mod.
|
|
||||||
|
|
||||||
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`.
|
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`.
|
||||||
|
|
||||||
@@ -78,6 +76,6 @@ Type `sudo make install` for system-wide installation. Run `sudo make install-li
|
|||||||
macOS
|
macOS
|
||||||
=====
|
=====
|
||||||
|
|
||||||
[.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0) or [Mono](https://www.mono-project.com/download/stable/#download-mac) (version 6.12 or later) is required to compile OpenRA. We recommend using .NET 6 unless you are running a very old version of macOS (10.9 through 10.14).
|
[.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) is required to compile OpenRA.
|
||||||
|
|
||||||
To compile OpenRA, run `make` from the command line (or `make RUNTIME=mono` if using Mono). Run with `./launch-game.sh`.
|
To compile OpenRA, run `make` from the command line. Run with `./launch-game.sh`.
|
||||||
|
|||||||
43
Makefile
43
Makefile
@@ -3,20 +3,17 @@
|
|||||||
# to compile, run:
|
# to compile, run:
|
||||||
# make
|
# make
|
||||||
#
|
#
|
||||||
# to compile using Mono (version 6.12 or greater) instead of .NET 6, run:
|
|
||||||
# make RUNTIME=mono
|
|
||||||
#
|
|
||||||
# to compile using system libraries for native dependencies, run:
|
# to compile using system libraries for native dependencies, run:
|
||||||
# make [RUNTIME=net6] TARGETPLATFORM=unix-generic
|
# make TARGETPLATFORM=unix-generic
|
||||||
#
|
#
|
||||||
# to check the official mods for erroneous yaml files, run:
|
# to check the official mods for erroneous yaml files, run:
|
||||||
# make [RUNTIME=net6] test
|
# make test
|
||||||
#
|
#
|
||||||
# to check the engine and official mod dlls for code style violations, run:
|
# to check the engine and official mod dlls for code style violations, run:
|
||||||
# make [RUNTIME=net6] check
|
# make check
|
||||||
#
|
#
|
||||||
# to compile and install Red Alert, Tiberian Dawn, and Dune 2000, run:
|
# to compile and install Red Alert, Tiberian Dawn, and Dune 2000, run:
|
||||||
# make [RUNTIME=net6] [prefix=/foo] [bindir=/bar/bin] install
|
# make [prefix=/foo] [bindir=/bar/bin] install
|
||||||
#
|
#
|
||||||
# to compile and install Red Alert, Tiberian Dawn, and Dune 2000
|
# to compile and install Red Alert, Tiberian Dawn, and Dune 2000
|
||||||
# using system libraries for native dependencies, run:
|
# using system libraries for native dependencies, run:
|
||||||
@@ -48,15 +45,12 @@ gameinstalldir ?= $(libdir)/openra
|
|||||||
|
|
||||||
# Toolchain
|
# Toolchain
|
||||||
CWD = $(shell pwd)
|
CWD = $(shell pwd)
|
||||||
MSBUILD = msbuild -verbosity:m -nologo
|
|
||||||
DOTNET = dotnet
|
DOTNET = dotnet
|
||||||
MONO = mono
|
|
||||||
RM = rm
|
RM = rm
|
||||||
RM_R = $(RM) -r
|
RM_R = $(RM) -r
|
||||||
RM_F = $(RM) -f
|
RM_F = $(RM) -f
|
||||||
RM_RF = $(RM) -rf
|
RM_RF = $(RM) -rf
|
||||||
|
|
||||||
RUNTIME ?= net6
|
|
||||||
CONFIGURATION ?= Release
|
CONFIGURATION ?= Release
|
||||||
DOTNET_RID = $(shell ${DOTNET} --info | grep RID: | cut -w -f3)
|
DOTNET_RID = $(shell ${DOTNET} --info | grep RID: | cut -w -f3)
|
||||||
ARCH_X64 = $(shell echo ${DOTNET_RID} | grep x64)
|
ARCH_X64 = $(shell echo ${DOTNET_RID} | grep x64)
|
||||||
@@ -91,18 +85,12 @@ endif
|
|||||||
#
|
#
|
||||||
all:
|
all:
|
||||||
@echo "Compiling in ${CONFIGURATION} mode..."
|
@echo "Compiling in ${CONFIGURATION} mode..."
|
||||||
ifeq ($(RUNTIME), mono)
|
|
||||||
@command -v $(firstword $(MSBUILD)) >/dev/null || (echo "OpenRA requires the '$(MSBUILD)' tool provided by Mono >= 6.12."; exit 1)
|
|
||||||
@$(MSBUILD) -t:Build -restore -p:Configuration=${CONFIGURATION} -p:TargetPlatform=$(TARGETPLATFORM)
|
|
||||||
else
|
|
||||||
@$(DOTNET) build -c ${CONFIGURATION} -nologo -p:TargetPlatform=$(TARGETPLATFORM)
|
@$(DOTNET) build -c ${CONFIGURATION} -nologo -p:TargetPlatform=$(TARGETPLATFORM)
|
||||||
endif
|
|
||||||
ifeq ($(TARGETPLATFORM), unix-generic)
|
ifeq ($(TARGETPLATFORM), unix-generic)
|
||||||
@./configure-system-libraries.sh
|
@./configure-system-libraries.sh
|
||||||
endif
|
endif
|
||||||
@./fetch-geoip.sh
|
@./fetch-geoip.sh
|
||||||
|
|
||||||
# dotnet clean and msbuild -t:Clean leave files that cause problems when switching between mono/dotnet
|
|
||||||
# Deleting the intermediate / output directories ensures the build directory is actually clean
|
# Deleting the intermediate / output directories ensures the build directory is actually clean
|
||||||
clean:
|
clean:
|
||||||
@-$(RM_RF) ./bin ./*/obj
|
@-$(RM_RF) ./bin ./*/obj
|
||||||
@@ -111,12 +99,8 @@ clean:
|
|||||||
check:
|
check:
|
||||||
@echo
|
@echo
|
||||||
@echo "Compiling in Debug mode..."
|
@echo "Compiling in Debug mode..."
|
||||||
ifeq ($(RUNTIME), mono)
|
|
||||||
@$(MSBUILD) -t:clean\;build -restore -p:Configuration=Debug -warnaserror -p:TargetPlatform=$(TARGETPLATFORM)
|
|
||||||
else
|
|
||||||
@$(DOTNET) clean -c Debug --nologo --verbosity minimal
|
@$(DOTNET) clean -c Debug --nologo --verbosity minimal
|
||||||
@$(DOTNET) build -c Debug -nologo -warnaserror -p:TargetPlatform=$(TARGETPLATFORM)
|
@$(DOTNET) build -c Debug -nologo -warnaserror -p:TargetPlatform=$(TARGETPLATFORM)
|
||||||
endif
|
|
||||||
ifeq ($(TARGETPLATFORM), unix-generic)
|
ifeq ($(TARGETPLATFORM), unix-generic)
|
||||||
@./configure-system-libraries.sh
|
@./configure-system-libraries.sh
|
||||||
endif
|
endif
|
||||||
@@ -135,15 +119,19 @@ check-scripts:
|
|||||||
test: all
|
test: all
|
||||||
@echo
|
@echo
|
||||||
@echo "Testing Tiberian Sun mod MiniYAML..."
|
@echo "Testing Tiberian Sun mod MiniYAML..."
|
||||||
|
@./utility.sh ts-content --check-yaml
|
||||||
@./utility.sh ts --check-yaml
|
@./utility.sh ts --check-yaml
|
||||||
@echo
|
@echo
|
||||||
@echo "Testing Dune 2000 mod MiniYAML..."
|
@echo "Testing Dune 2000 mod MiniYAML..."
|
||||||
|
@./utility.sh d2k-content --check-yaml
|
||||||
@./utility.sh d2k --check-yaml
|
@./utility.sh d2k --check-yaml
|
||||||
@echo
|
@echo
|
||||||
@echo "Testing Tiberian Dawn mod MiniYAML..."
|
@echo "Testing Tiberian Dawn mod MiniYAML..."
|
||||||
|
@./utility.sh cnc-content --check-yaml
|
||||||
@./utility.sh cnc --check-yaml
|
@./utility.sh cnc --check-yaml
|
||||||
@echo
|
@echo
|
||||||
@echo "Testing Red Alert mod MiniYAML..."
|
@echo "Testing Red Alert mod MiniYAML..."
|
||||||
|
@./utility.sh ra-content --check-yaml
|
||||||
@./utility.sh ra --check-yaml
|
@./utility.sh ra --check-yaml
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
@@ -161,7 +149,7 @@ endif
|
|||||||
@sh -c '. ./packaging/functions.sh; set_mod_version "$(VERSION)" mods/*/mod.yaml'
|
@sh -c '. ./packaging/functions.sh; set_mod_version "$(VERSION)" mods/*/mod.yaml'
|
||||||
|
|
||||||
install:
|
install:
|
||||||
@sh -c '. ./packaging/functions.sh; install_assemblies $(CWD) $(DESTDIR)$(gameinstalldir) $(TARGETPLATFORM) $(RUNTIME) True True True'
|
@sh -c '. ./packaging/functions.sh; install_assemblies $(CWD) $(DESTDIR)$(gameinstalldir) $(TARGETPLATFORM) True True True'
|
||||||
@sh -c '. ./packaging/functions.sh; install_data $(CWD) $(DESTDIR)$(gameinstalldir) cnc d2k ra'
|
@sh -c '. ./packaging/functions.sh; install_data $(CWD) $(DESTDIR)$(gameinstalldir) cnc d2k ra'
|
||||||
|
|
||||||
install-linux-shortcuts:
|
install-linux-shortcuts:
|
||||||
@@ -178,24 +166,21 @@ help:
|
|||||||
@echo 'to compile, run:'
|
@echo 'to compile, run:'
|
||||||
@echo ' make'
|
@echo ' make'
|
||||||
@echo
|
@echo
|
||||||
@echo 'to compile using Mono (version 6.12 or greater) instead of .NET 6, run:'
|
|
||||||
@echo ' make RUNTIME=mono'
|
|
||||||
@echo
|
|
||||||
@echo 'to compile using system libraries for native dependencies, run:'
|
@echo 'to compile using system libraries for native dependencies, run:'
|
||||||
@echo ' make [RUNTIME=net6] TARGETPLATFORM=unix-generic'
|
@echo ' make TARGETPLATFORM=unix-generic'
|
||||||
@echo
|
@echo
|
||||||
@echo 'to check the official mods for erroneous yaml files, run:'
|
@echo 'to check the official mods for erroneous yaml files, run:'
|
||||||
@echo ' make [RUNTIME=net6] [TREAT_WARNINGS_AS_ERRORS=false] test'
|
@echo ' make [TREAT_WARNINGS_AS_ERRORS=false] test'
|
||||||
@echo
|
@echo
|
||||||
@echo 'to check the engine and official mod dlls for code style violations, run:'
|
@echo 'to check the engine and official mod dlls for code style violations, run:'
|
||||||
@echo ' make [RUNTIME=net6] check'
|
@echo ' make check'
|
||||||
@echo
|
@echo
|
||||||
@echo 'to compile and install Red Alert, Tiberian Dawn, and Dune 2000 run:'
|
@echo 'to compile and install Red Alert, Tiberian Dawn, and Dune 2000 run:'
|
||||||
@echo ' make [RUNTIME=net6] [prefix=/foo] [TARGETPLATFORM=unix-generic] install'
|
@echo ' make [prefix=/foo] [TARGETPLATFORM=unix-generic] install'
|
||||||
@echo
|
@echo
|
||||||
@echo 'to compile and install Red Alert, Tiberian Dawn, and Dune 2000'
|
@echo 'to compile and install Red Alert, Tiberian Dawn, and Dune 2000'
|
||||||
@echo 'using system libraries for native dependencies, run:'
|
@echo 'using system libraries for native dependencies, run:'
|
||||||
@echo ' make [RUNTIME=net6] [prefix=/foo] [bindir=/bar/bin] TARGETPLATFORM=unix-generic install'
|
@echo ' make [prefix=/foo] [bindir=/bar/bin] TARGETPLATFORM=unix-generic install'
|
||||||
@echo
|
@echo
|
||||||
@echo 'to install FreeDesktop startup scripts, desktop files, icons, and MIME metadata'
|
@echo 'to install FreeDesktop startup scripts, desktop files, icons, and MIME metadata'
|
||||||
@echo ' make install-linux-shortcuts'
|
@echo ' make install-linux-shortcuts'
|
||||||
|
|||||||
@@ -89,21 +89,21 @@ namespace OpenRA
|
|||||||
sealed class ConditionState
|
sealed class ConditionState
|
||||||
{
|
{
|
||||||
/// <summary>Delegates that have registered to be notified when this condition changes.</summary>
|
/// <summary>Delegates that have registered to be notified when this condition changes.</summary>
|
||||||
public readonly List<VariableObserverNotifier> Notifiers = new();
|
public readonly List<VariableObserverNotifier> Notifiers = [];
|
||||||
|
|
||||||
/// <summary>Unique integers identifying granted instances of the condition.</summary>
|
/// <summary>Unique integers identifying granted instances of the condition.</summary>
|
||||||
public readonly HashSet<int> Tokens = new();
|
public readonly HashSet<int> Tokens = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly Dictionary<string, ConditionState> conditionStates = new();
|
readonly Dictionary<string, ConditionState> conditionStates = [];
|
||||||
|
|
||||||
/// <summary>Each granted condition receives a unique token that is used when revoking.</summary>
|
/// <summary>Each granted condition receives a unique token that is used when revoking.</summary>
|
||||||
readonly Dictionary<int, string> conditionTokens = new();
|
readonly Dictionary<int, string> conditionTokens = [];
|
||||||
|
|
||||||
int nextConditionToken = 1;
|
int nextConditionToken = 1;
|
||||||
|
|
||||||
/// <summary>Cache of condition -> enabled state for quick evaluation of token counter conditions.</summary>
|
/// <summary>Cache of condition -> enabled state for quick evaluation of token counter conditions.</summary>
|
||||||
readonly Dictionary<string, int> conditionCache = new();
|
readonly Dictionary<string, int> conditionCache = [];
|
||||||
|
|
||||||
/// <summary>Read-only version of conditionCache that is passed to IConditionConsumers.</summary>
|
/// <summary>Read-only version of conditionCache that is passed to IConditionConsumers.</summary>
|
||||||
readonly IReadOnlyDictionary<string, int> readOnlyConditionCache;
|
readonly IReadOnlyDictionary<string, int> readOnlyConditionCache;
|
||||||
@@ -123,6 +123,9 @@ namespace OpenRA
|
|||||||
readonly IEnumerable<WPos> enabledTargetableWorldPositions;
|
readonly IEnumerable<WPos> enabledTargetableWorldPositions;
|
||||||
bool created;
|
bool created;
|
||||||
|
|
||||||
|
IEnumerable<IRenderable> renderables;
|
||||||
|
WorldRenderer lastWorldRenderer;
|
||||||
|
|
||||||
internal Actor(World world, string name, TypeDictionary initDict)
|
internal Actor(World world, string name, TypeDictionary initDict)
|
||||||
{
|
{
|
||||||
var duplicateInit = initDict.WithInterface<ISingleInstanceInit>().GroupBy(i => i.GetType())
|
var duplicateInit = initDict.WithInterface<ISingleInstanceInit>().GroupBy(i => i.GetType())
|
||||||
@@ -287,11 +290,18 @@ namespace OpenRA
|
|||||||
|
|
||||||
public IEnumerable<IRenderable> Render(WorldRenderer wr)
|
public IEnumerable<IRenderable> Render(WorldRenderer wr)
|
||||||
{
|
{
|
||||||
|
if (lastWorldRenderer != wr)
|
||||||
|
{
|
||||||
|
// PERF: Cache the enumerable to reduce allocations.
|
||||||
|
lastWorldRenderer = wr;
|
||||||
|
renderables = Renderables(wr);
|
||||||
|
}
|
||||||
|
|
||||||
// PERF: Avoid LINQ.
|
// PERF: Avoid LINQ.
|
||||||
var renderables = Renderables(wr);
|
var modifiedRenderables = renderables;
|
||||||
foreach (var modifier in renderModifiers)
|
foreach (var modifier in renderModifiers)
|
||||||
renderables = modifier.ModifyRender(this, wr, renderables);
|
modifiedRenderables = modifier.ModifyRender(this, wr, modifiedRenderables);
|
||||||
return renderables;
|
return modifiedRenderables;
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<IRenderable> Renderables(WorldRenderer wr)
|
IEnumerable<IRenderable> Renderables(WorldRenderer wr)
|
||||||
@@ -504,7 +514,7 @@ namespace OpenRA
|
|||||||
if (!visibilityModifier.IsVisible(this, player))
|
if (!visibilityModifier.IsVisible(this, player))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return defaultVisibility.IsVisible(this, player);
|
return defaultVisibility?.IsVisible(this, player) ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BitSet<TargetableType> GetAllTargetTypes()
|
public BitSet<TargetableType> GetAllTargetTypes()
|
||||||
@@ -541,7 +551,7 @@ namespace OpenRA
|
|||||||
if (EnabledTargetablePositions.Any())
|
if (EnabledTargetablePositions.Any())
|
||||||
return enabledTargetableWorldPositions;
|
return enabledTargetableWorldPositions;
|
||||||
|
|
||||||
return new[] { CenterPosition };
|
return [CenterPosition];
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Conditions
|
#region Conditions
|
||||||
|
|||||||
@@ -78,17 +78,9 @@ namespace OpenRA
|
|||||||
return new MPos(X, Y);
|
return new MPos(X, Y);
|
||||||
|
|
||||||
// Convert from RectangularIsometric cell (x, y) position to rectangular map position (u, v)
|
// Convert from RectangularIsometric cell (x, y) position to rectangular map position (u, v)
|
||||||
// - The staggered rows make this fiddly (hint: draw a diagram!)
|
// The staggered rows make this fiddly.
|
||||||
// (a) Consider the relationships:
|
|
||||||
// - +1x (even -> odd) adds (0, 1) to (u, v)
|
|
||||||
// - +1x (odd -> even) adds (1, 1) to (u, v)
|
|
||||||
// - +1y (even -> odd) adds (-1, 1) to (u, v)
|
|
||||||
// - +1y (odd -> even) adds (0, 1) to (u, v)
|
|
||||||
// (b) Therefore:
|
|
||||||
// - ax + by adds (a - b)/2 to u (only even increments count)
|
|
||||||
// - ax + by adds a + b to v
|
|
||||||
var u = (X - Y) / 2;
|
|
||||||
var v = X + Y;
|
var v = X + Y;
|
||||||
|
var u = (v - (v & 1)) / 2 - Y;
|
||||||
return new MPos(u, v);
|
return new MPos(u, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ namespace OpenRA
|
|||||||
public override string ToString() { return X + "," + Y; }
|
public override string ToString() { return X + "," + Y; }
|
||||||
|
|
||||||
public static readonly CVec[] Directions =
|
public static readonly CVec[] Directions =
|
||||||
{
|
[
|
||||||
new(-1, -1),
|
new(-1, -1),
|
||||||
new(-1, 0),
|
new(-1, 0),
|
||||||
new(-1, 1),
|
new(-1, 1),
|
||||||
@@ -71,7 +71,7 @@ namespace OpenRA
|
|||||||
new(1, -1),
|
new(1, -1),
|
||||||
new(1, 0),
|
new(1, 0),
|
||||||
new(1, 1),
|
new(1, 1),
|
||||||
};
|
];
|
||||||
|
|
||||||
#region Scripting interface
|
#region Scripting interface
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace OpenRA
|
|||||||
public static class CryptoUtil
|
public static class CryptoUtil
|
||||||
{
|
{
|
||||||
// Fixed byte pattern for the OID header
|
// Fixed byte pattern for the OID header
|
||||||
static readonly byte[] OIDHeader = { 0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0 };
|
static readonly byte[] OIDHeader = [0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0];
|
||||||
|
|
||||||
static readonly char[] HexUpperAlphabet = "0123456789ABCDEF".ToArray();
|
static readonly char[] HexUpperAlphabet = "0123456789ABCDEF".ToArray();
|
||||||
static readonly char[] HexLowerAlphabet = "0123456789abcdef".ToArray();
|
static readonly char[] HexLowerAlphabet = "0123456789abcdef".ToArray();
|
||||||
@@ -210,8 +210,7 @@ namespace OpenRA
|
|||||||
using (var rsa = new RSACryptoServiceProvider())
|
using (var rsa = new RSACryptoServiceProvider())
|
||||||
{
|
{
|
||||||
rsa.ImportParameters(parameters);
|
rsa.ImportParameters(parameters);
|
||||||
using (var csp = SHA1.Create())
|
return Convert.ToBase64String(rsa.SignHash(SHA1.HashData(data), CryptoConfig.MapNameToOID("SHA1")));
|
||||||
return Convert.ToBase64String(rsa.SignHash(csp.ComputeHash(data), CryptoConfig.MapNameToOID("SHA1")));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@@ -236,8 +235,7 @@ namespace OpenRA
|
|||||||
using (var rsa = new RSACryptoServiceProvider())
|
using (var rsa = new RSACryptoServiceProvider())
|
||||||
{
|
{
|
||||||
rsa.ImportParameters(parameters);
|
rsa.ImportParameters(parameters);
|
||||||
using (var csp = SHA1.Create())
|
return rsa.VerifyHash(SHA1.HashData(data), CryptoConfig.MapNameToOID("SHA1"), Convert.FromBase64String(signature));
|
||||||
return rsa.VerifyHash(csp.ComputeHash(data), CryptoConfig.MapNameToOID("SHA1"), Convert.FromBase64String(signature));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@@ -252,14 +250,12 @@ namespace OpenRA
|
|||||||
|
|
||||||
public static string SHA1Hash(Stream data)
|
public static string SHA1Hash(Stream data)
|
||||||
{
|
{
|
||||||
using var csp = SHA1.Create();
|
return ToHex(SHA1.HashData(data), true);
|
||||||
return ToHex(csp.ComputeHash(data), true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string SHA1Hash(byte[] data)
|
public static string SHA1Hash(byte[] data)
|
||||||
{
|
{
|
||||||
using var csp = SHA1.Create();
|
return ToHex(SHA1.HashData(data), true);
|
||||||
return ToHex(csp.ComputeHash(data), true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string SHA1Hash(string data)
|
public static string SHA1Hash(string data)
|
||||||
|
|||||||
@@ -9,17 +9,14 @@
|
|||||||
*/
|
*/
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using OpenRA.Traits;
|
using System;
|
||||||
|
|
||||||
namespace OpenRA.Mods.Common.Traits
|
namespace OpenRA
|
||||||
{
|
{
|
||||||
[Desc("The actor is always considered visible for targeting and rendering purposes.")]
|
// Mirrors DescriptionAttribute from System.ComponentModel but we don't want to have to use that everywhere.
|
||||||
public class AlwaysVisibleInfo : TraitInfo<AlwaysVisible>, IDefaultVisibilityInfo { }
|
[AttributeUsage(AttributeTargets.All)]
|
||||||
public class AlwaysVisible : IDefaultVisibility
|
public sealed class DescAttribute(params string[] lines) : Attribute
|
||||||
{
|
{
|
||||||
public bool IsVisible(Actor self, Player byPlayer)
|
public readonly string[] Lines = lines;
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenRA.FileFormats;
|
using OpenRA.FileFormats;
|
||||||
@@ -28,7 +29,7 @@ namespace OpenRA
|
|||||||
public readonly string Id;
|
public readonly string Id;
|
||||||
public readonly string Version;
|
public readonly string Version;
|
||||||
public readonly string LaunchPath;
|
public readonly string LaunchPath;
|
||||||
public readonly string[] LaunchArgs;
|
public readonly ImmutableArray<string> LaunchArgs;
|
||||||
public Sprite Icon { get; internal set; }
|
public Sprite Icon { get; internal set; }
|
||||||
public Sprite Icon2x { get; internal set; }
|
public Sprite Icon2x { get; internal set; }
|
||||||
public Sprite Icon3x { get; internal set; }
|
public Sprite Icon3x { get; internal set; }
|
||||||
@@ -40,7 +41,7 @@ namespace OpenRA
|
|||||||
|
|
||||||
public class ExternalMods : IReadOnlyDictionary<string, ExternalMod>
|
public class ExternalMods : IReadOnlyDictionary<string, ExternalMod>
|
||||||
{
|
{
|
||||||
readonly Dictionary<string, ExternalMod> mods = new();
|
readonly Dictionary<string, ExternalMod> mods = [];
|
||||||
readonly SheetBuilder sheetBuilder;
|
readonly SheetBuilder sheetBuilder;
|
||||||
|
|
||||||
Sheet CreateSheet()
|
Sheet CreateSheet()
|
||||||
@@ -122,13 +123,13 @@ namespace OpenRA
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var key = ExternalMod.MakeKey(mod);
|
var key = ExternalMod.MakeKey(mod);
|
||||||
var yaml = new MiniYamlNode("Registration", new MiniYaml("", new[]
|
var yaml = new MiniYamlNode("Registration", new MiniYaml("",
|
||||||
{
|
[
|
||||||
new MiniYamlNode("Id", mod.Id),
|
new MiniYamlNode("Id", mod.Id),
|
||||||
new MiniYamlNode("Version", mod.Metadata.Version),
|
new MiniYamlNode("Version", mod.Metadata.Version),
|
||||||
new MiniYamlNode("LaunchPath", launchPath),
|
new MiniYamlNode("LaunchPath", launchPath),
|
||||||
new MiniYamlNode("LaunchArgs", new[] { "Game.Mod=" + mod.Id }.Concat(launchArgs).JoinWith(", "))
|
new MiniYamlNode("LaunchArgs", new[] { "Game.Mod=" + mod.Id }.Concat(launchArgs).JoinWith(", "))
|
||||||
}));
|
]));
|
||||||
|
|
||||||
var iconNodes = new List<MiniYamlNode>();
|
var iconNodes = new List<MiniYamlNode>();
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,11 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
using OpenRA.Support;
|
using OpenRA.Support;
|
||||||
@@ -77,7 +79,7 @@ namespace OpenRA
|
|||||||
return Math.Sign((v1.X - v0.X) * (p.Y - v0.Y) - (p.X - v0.X) * (v1.Y - v0.Y));
|
return Math.Sign((v1.X - v0.X) * (p.Y - v0.Y) - (p.X - v0.X) * (v1.Y - v0.Y));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool PolygonContains(this int2[] polygon, int2 p)
|
public static bool PolygonContains(this ImmutableArray<int2> polygon, int2 p)
|
||||||
{
|
{
|
||||||
var windingNumber = 0;
|
var windingNumber = 0;
|
||||||
|
|
||||||
@@ -119,17 +121,11 @@ namespace OpenRA
|
|||||||
|
|
||||||
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, V v)
|
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, V v)
|
||||||
{
|
{
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
// SAFETY: Dictionary cannot be modified whilst the ref is alive.
|
// SAFETY: Dictionary cannot be modified whilst the ref is alive.
|
||||||
ref var value = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(d, k, out var exists);
|
ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(d, k, out var exists);
|
||||||
if (!exists)
|
if (!exists)
|
||||||
value = v;
|
value = v;
|
||||||
return value;
|
return value;
|
||||||
#else
|
|
||||||
if (!d.TryGetValue(k, out var ret))
|
|
||||||
d.Add(k, ret = v);
|
|
||||||
return ret;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, Func<K, V> createFn)
|
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, Func<K, V> createFn)
|
||||||
@@ -418,11 +414,6 @@ namespace OpenRA
|
|||||||
return ts.Except(exclusions);
|
return ts.Except(exclusions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source)
|
|
||||||
{
|
|
||||||
return new HashSet<T>(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Dictionary<TKey, TSource> ToDictionaryWithConflictLog<TSource, TKey>(
|
public static Dictionary<TKey, TSource> ToDictionaryWithConflictLog<TSource, TKey>(
|
||||||
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
|
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
|
||||||
string debugName, Func<TKey, string> logKey, Func<TSource, string> logValue)
|
string debugName, Func<TKey, string> logKey, Func<TSource, string> logValue)
|
||||||
@@ -465,14 +456,14 @@ namespace OpenRA
|
|||||||
// Check for a key conflict:
|
// Check for a key conflict:
|
||||||
if (!output.TryAdd(key, element))
|
if (!output.TryAdd(key, element))
|
||||||
{
|
{
|
||||||
dupKeys ??= new Dictionary<TKey, List<string>>();
|
dupKeys ??= [];
|
||||||
if (!dupKeys.TryGetValue(key, out var dupKeyMessages))
|
if (!dupKeys.TryGetValue(key, out var dupKeyMessages))
|
||||||
{
|
{
|
||||||
// Log the initial conflicting value already inserted:
|
// Log the initial conflicting value already inserted:
|
||||||
dupKeyMessages = new List<string>
|
dupKeyMessages =
|
||||||
{
|
[
|
||||||
logValue(output[key])
|
logValue(output[key])
|
||||||
};
|
];
|
||||||
dupKeys.Add(key, dupKeyMessages);
|
dupKeys.Add(key, dupKeyMessages);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,7 +478,7 @@ namespace OpenRA
|
|||||||
var badKeysFormatted = new StringBuilder(
|
var badKeysFormatted = new StringBuilder(
|
||||||
$"{debugName}, duplicate values found for the following keys: ");
|
$"{debugName}, duplicate values found for the following keys: ");
|
||||||
foreach (var p in dupKeys)
|
foreach (var p in dupKeys)
|
||||||
badKeysFormatted.Append($"{logKey(p.Key)}: [{string.Join(",", p.Value)}]");
|
badKeysFormatted.Append(CultureInfo.InvariantCulture, $"{logKey(p.Key)}: [{string.Join(",", p.Value)}]");
|
||||||
throw new ArgumentException(badKeysFormatted.ToString());
|
throw new ArgumentException(badKeysFormatted.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,44 +501,16 @@ namespace OpenRA
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T[,] ResizeArray<T>(T[,] ts, T t, int width, int height)
|
|
||||||
{
|
|
||||||
var result = new T[width, height];
|
|
||||||
for (var i = 0; i < width; i++)
|
|
||||||
{
|
|
||||||
for (var j = 0; j < height; j++)
|
|
||||||
{
|
|
||||||
// Workaround for broken ternary operators in certain versions of mono
|
|
||||||
// (3.10 and certain versions of the 3.8 series): https://bugzilla.xamarin.com/show_bug.cgi?id=23319
|
|
||||||
if (i <= ts.GetUpperBound(0) && j <= ts.GetUpperBound(1))
|
|
||||||
result[i, j] = ts[i, j];
|
|
||||||
else
|
|
||||||
result[i, j] = t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int ToBits(this IEnumerable<bool> bits)
|
|
||||||
{
|
|
||||||
var i = 0;
|
|
||||||
var result = 0;
|
|
||||||
foreach (var b in bits)
|
|
||||||
if (b)
|
|
||||||
result |= 1 << i++;
|
|
||||||
else
|
|
||||||
i++;
|
|
||||||
if (i > 33)
|
|
||||||
throw new InvalidOperationException("ToBits only accepts up to 32 values.");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte ParseByteInvariant(string s)
|
public static byte ParseByteInvariant(string s)
|
||||||
{
|
{
|
||||||
return byte.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
return byte.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ushort ParseUshortInvariant(string s)
|
||||||
|
{
|
||||||
|
return ushort.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
||||||
|
}
|
||||||
|
|
||||||
public static short ParseInt16Invariant(string s)
|
public static short ParseInt16Invariant(string s)
|
||||||
{
|
{
|
||||||
return short.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
return short.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
||||||
@@ -558,6 +521,22 @@ namespace OpenRA
|
|||||||
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static float ParseFloatOrPercentInvariant(string s)
|
||||||
|
{
|
||||||
|
var f = float.Parse(s.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo);
|
||||||
|
return f * (s.Contains('%') ? 0.01f : 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParseByteInvariant(string s, out byte i)
|
||||||
|
{
|
||||||
|
return byte.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParseUshortInvariant(string s, out ushort i)
|
||||||
|
{
|
||||||
|
return ushort.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
||||||
|
}
|
||||||
|
|
||||||
public static bool TryParseInt32Invariant(string s, out int i)
|
public static bool TryParseInt32Invariant(string s, out int i)
|
||||||
{
|
{
|
||||||
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
||||||
@@ -568,6 +547,17 @@ namespace OpenRA
|
|||||||
return long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
return long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryParseFloatOrPercentInvariant(string s, out float f)
|
||||||
|
{
|
||||||
|
if (float.TryParse(s?.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out f))
|
||||||
|
{
|
||||||
|
f *= s.Contains('%') ? 0.01f : 1f;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public static string ToStringInvariant(this byte i)
|
public static string ToStringInvariant(this byte i)
|
||||||
{
|
{
|
||||||
return i.ToString(NumberFormatInfo.InvariantInfo);
|
return i.ToString(NumberFormatInfo.InvariantInfo);
|
||||||
@@ -583,6 +573,16 @@ namespace OpenRA
|
|||||||
return i.ToString(NumberFormatInfo.InvariantInfo);
|
return i.ToString(NumberFormatInfo.InvariantInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string ToStringInvariant(this uint i)
|
||||||
|
{
|
||||||
|
return i.ToString(NumberFormatInfo.InvariantInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToStringInvariant(this float f)
|
||||||
|
{
|
||||||
|
return f.ToString(NumberFormatInfo.InvariantInfo);
|
||||||
|
}
|
||||||
|
|
||||||
public static string ToStringInvariant(this int i, string format)
|
public static string ToStringInvariant(this int i, string format)
|
||||||
{
|
{
|
||||||
return i.ToString(format, NumberFormatInfo.InvariantInfo);
|
return i.ToString(format, NumberFormatInfo.InvariantInfo);
|
||||||
@@ -665,7 +665,7 @@ namespace OpenRA
|
|||||||
if (index == -1)
|
if (index == -1)
|
||||||
{
|
{
|
||||||
// The remaining string is an empty string
|
// The remaining string is an empty string
|
||||||
str = ReadOnlySpan<char>.Empty;
|
str = [];
|
||||||
Current = span;
|
Current = span;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -677,27 +677,4 @@ namespace OpenRA
|
|||||||
|
|
||||||
public ReadOnlySpan<char> Current { get; private set; }
|
public ReadOnlySpan<char> Current { get; private set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Enum<T>
|
|
||||||
{
|
|
||||||
public static T Parse(string s) { return (T)Enum.Parse(typeof(T), s); }
|
|
||||||
public static T[] GetValues() { return (T[])Enum.GetValues(typeof(T)); }
|
|
||||||
|
|
||||||
public static bool TryParse(string s, bool ignoreCase, out T value)
|
|
||||||
{
|
|
||||||
// The string may be a comma delimited list of values
|
|
||||||
var names = ignoreCase ? Enum.GetNames(typeof(T)).Select(x => x.ToLowerInvariant()) : Enum.GetNames(typeof(T));
|
|
||||||
var values = ignoreCase ? s.Split(',').Select(x => x.Trim().ToLowerInvariant()) : s.Split(',').Select(x => x.Trim());
|
|
||||||
|
|
||||||
if (values.Any(x => !names.Contains(x)))
|
|
||||||
{
|
|
||||||
value = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = (T)Enum.Parse(typeof(T), s, ignoreCase);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,14 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.Serialization;
|
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
using OpenRA.Support;
|
using OpenRA.Support;
|
||||||
|
|
||||||
@@ -24,9 +25,8 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
public static class FieldLoader
|
public static class FieldLoader
|
||||||
{
|
{
|
||||||
const char SplitComma = ',';
|
const char Comma = ',';
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class MissingFieldsException : YamlException
|
public class MissingFieldsException : YamlException
|
||||||
{
|
{
|
||||||
public readonly string[] Missing;
|
public readonly string[] Missing;
|
||||||
@@ -46,13 +46,6 @@ namespace OpenRA
|
|||||||
Header = missing.Length > 1 ? header : headerSingle ?? header;
|
Header = missing.Length > 1 ? header : headerSingle ?? header;
|
||||||
Missing = missing;
|
Missing = missing;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void GetObjectData(SerializationInfo info, StreamingContext context)
|
|
||||||
{
|
|
||||||
base.GetObjectData(info, context);
|
|
||||||
info.AddValue("Missing", Missing);
|
|
||||||
info.AddValue("Header", Header);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Func<string, Type, string, object> InvalidValueAction = (s, t, f) =>
|
public static Func<string, Type, string, object> InvalidValueAction = (s, t, f) =>
|
||||||
@@ -68,8 +61,8 @@ namespace OpenRA
|
|||||||
static readonly ConcurrentCache<string, IntegerExpression> IntegerExpressionCache =
|
static readonly ConcurrentCache<string, IntegerExpression> IntegerExpressionCache =
|
||||||
new(expression => new IntegerExpression(expression));
|
new(expression => new IntegerExpression(expression));
|
||||||
|
|
||||||
static readonly Dictionary<Type, Func<string, Type, string, MemberInfo, object>> TypeParsers =
|
static readonly FrozenDictionary<Type, Func<string, Type, string, object>> TypeParsers =
|
||||||
new()
|
new Dictionary<Type, Func<string, Type, string, object>>
|
||||||
{
|
{
|
||||||
{ typeof(int), ParseInt },
|
{ typeof(int), ParseInt },
|
||||||
{ typeof(ushort), ParseUShort },
|
{ typeof(ushort), ParseUShort },
|
||||||
@@ -92,7 +85,6 @@ namespace OpenRA
|
|||||||
{ typeof(CVec[]), ParseCVecArray },
|
{ typeof(CVec[]), ParseCVecArray },
|
||||||
{ typeof(BooleanExpression), ParseBooleanExpression },
|
{ typeof(BooleanExpression), ParseBooleanExpression },
|
||||||
{ typeof(IntegerExpression), ParseIntegerExpression },
|
{ typeof(IntegerExpression), ParseIntegerExpression },
|
||||||
{ typeof(Enum), ParseEnum },
|
|
||||||
{ typeof(bool), ParseBool },
|
{ typeof(bool), ParseBool },
|
||||||
{ typeof(int2[]), ParseInt2Array },
|
{ typeof(int2[]), ParseInt2Array },
|
||||||
{ typeof(Size), ParseSize },
|
{ typeof(Size), ParseSize },
|
||||||
@@ -101,23 +93,44 @@ namespace OpenRA
|
|||||||
{ typeof(float3), ParseFloat3 },
|
{ typeof(float3), ParseFloat3 },
|
||||||
{ typeof(Rectangle), ParseRectangle },
|
{ typeof(Rectangle), ParseRectangle },
|
||||||
{ typeof(DateTime), ParseDateTime }
|
{ typeof(DateTime), ParseDateTime }
|
||||||
};
|
}.ToFrozenDictionary();
|
||||||
|
|
||||||
static readonly Dictionary<Type, Func<string, Type, string, MiniYaml, MemberInfo, object>> GenericTypeParsers =
|
static readonly FrozenDictionary<Type, Func<string, Type, string, MiniYaml, object>> GenericTypeParsers =
|
||||||
new()
|
new Dictionary<Type, Func<string, Type, string, MiniYaml, object>>
|
||||||
{
|
{
|
||||||
{ typeof(HashSet<>), ParseHashSetOrList },
|
{ typeof(HashSet<>), ParseHashSetOrList },
|
||||||
{ typeof(List<>), ParseHashSetOrList },
|
{ typeof(List<>), ParseHashSetOrList },
|
||||||
{ typeof(Dictionary<,>), ParseDictionary },
|
{ typeof(Dictionary<,>), ParseDictionary },
|
||||||
|
{ typeof(ImmutableArray<>), ParseImmutableArray },
|
||||||
|
{ typeof(FrozenSet<>), ParseFrozenSet },
|
||||||
|
{ typeof(FrozenDictionary<,>), ParseFrozenDictionary },
|
||||||
{ typeof(BitSet<>), ParseBitSet },
|
{ typeof(BitSet<>), ParseBitSet },
|
||||||
{ typeof(Nullable<>), ParseNullable },
|
{ typeof(Nullable<>), ParseNullable },
|
||||||
};
|
}.ToFrozenDictionary();
|
||||||
|
|
||||||
static readonly object BoxedTrue = true;
|
static readonly object BoxedTrue = true;
|
||||||
static readonly object BoxedFalse = false;
|
static readonly object BoxedFalse = false;
|
||||||
static readonly object[] BoxedInts = Exts.MakeArray(33, i => (object)i);
|
static readonly object[] BoxedInts = Exts.MakeArray(33, i => (object)i);
|
||||||
|
|
||||||
static object ParseInt(string fieldName, Type fieldType, string value, MemberInfo field)
|
static readonly MethodInfo ToImmutableArray =
|
||||||
|
typeof(ImmutableArray)
|
||||||
|
.GetMethods()
|
||||||
|
.Single(m =>
|
||||||
|
m.Name == nameof(ImmutableArray.ToImmutableArray) &&
|
||||||
|
m.GetParameters()?.First().ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>));
|
||||||
|
|
||||||
|
static readonly MethodInfo ToFrozenSet =
|
||||||
|
typeof(FrozenSet)
|
||||||
|
.GetMethod(nameof(FrozenSet.ToFrozenSet));
|
||||||
|
|
||||||
|
static readonly MethodInfo ToFrozenDictionary =
|
||||||
|
typeof(FrozenDictionary)
|
||||||
|
.GetMethods()
|
||||||
|
.Single(m =>
|
||||||
|
m.Name == nameof(FrozenDictionary.ToFrozenDictionary) &&
|
||||||
|
m.GetParameters().Length == 2);
|
||||||
|
|
||||||
|
static object ParseInt(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (Exts.TryParseInt32Invariant(value, out var res))
|
if (Exts.TryParseInt32Invariant(value, out var res))
|
||||||
{
|
{
|
||||||
@@ -129,48 +142,48 @@ namespace OpenRA
|
|||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseUShort(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseUShort(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (ushort.TryParse(value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var res))
|
if (Exts.TryParseUshortInvariant(value, out var res))
|
||||||
return res;
|
return res;
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseLong(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseLong(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (long.TryParse(value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var res))
|
if (Exts.TryParseInt64Invariant(value, out var res))
|
||||||
return res;
|
return res;
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseFloat(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseFloat(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null && float.TryParse(value.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var res))
|
if (Exts.TryParseFloatOrPercentInvariant(value, out var res))
|
||||||
return res * (value.Contains('%') ? 0.01f : 1f);
|
return res;
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseDecimal(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseDecimal(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null && decimal.TryParse(value.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var res))
|
if (value != null && decimal.TryParse(value.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var res))
|
||||||
return res * (value.Contains('%') ? 0.01m : 1m);
|
return res * (value.Contains('%') ? 0.01m : 1m);
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseString(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseString(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseColor(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseColor(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null && Color.TryParse(value, out var color))
|
if (Color.TryParse(value, out var color))
|
||||||
return color;
|
return color;
|
||||||
|
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseHotkey(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseHotkey(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (Hotkey.TryParse(value, out var res))
|
if (Hotkey.TryParse(value, out var res))
|
||||||
return res;
|
return res;
|
||||||
@@ -178,12 +191,12 @@ namespace OpenRA
|
|||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseHotkeyReference(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseHotkeyReference(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
return Game.ModData.Hotkeys[value];
|
return Game.ModData.Hotkeys[value];
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseWDist(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseWDist(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (WDist.TryParse(value, out var res))
|
if (WDist.TryParse(value, out var res))
|
||||||
return res;
|
return res;
|
||||||
@@ -191,11 +204,11 @@ namespace OpenRA
|
|||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseWVec(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseWVec(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
if (parts.Length == 3
|
if (parts.Length == 3
|
||||||
&& WDist.TryParse(parts[0], out var rx)
|
&& WDist.TryParse(parts[0], out var rx)
|
||||||
&& WDist.TryParse(parts[1], out var ry)
|
&& WDist.TryParse(parts[1], out var ry)
|
||||||
@@ -206,11 +219,11 @@ namespace OpenRA
|
|||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseWVecArray(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseWVecArray(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
if (parts.Length % 3 != 0)
|
if (parts.Length % 3 != 0)
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
@@ -223,6 +236,8 @@ namespace OpenRA
|
|||||||
&& WDist.TryParse(parts[3 * i + 1], out var ry)
|
&& WDist.TryParse(parts[3 * i + 1], out var ry)
|
||||||
&& WDist.TryParse(parts[3 * i + 2], out var rz))
|
&& WDist.TryParse(parts[3 * i + 2], out var rz))
|
||||||
vecs[i] = new WVec(rx, ry, rz);
|
vecs[i] = new WVec(rx, ry, rz);
|
||||||
|
else
|
||||||
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return vecs;
|
return vecs;
|
||||||
@@ -231,11 +246,11 @@ namespace OpenRA
|
|||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseWPos(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseWPos(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
if (parts.Length == 3
|
if (parts.Length == 3
|
||||||
&& WDist.TryParse(parts[0], out var rx)
|
&& WDist.TryParse(parts[0], out var rx)
|
||||||
&& WDist.TryParse(parts[1], out var ry)
|
&& WDist.TryParse(parts[1], out var ry)
|
||||||
@@ -246,18 +261,18 @@ namespace OpenRA
|
|||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseWAngle(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseWAngle(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (Exts.TryParseInt32Invariant(value, out var res))
|
if (Exts.TryParseInt32Invariant(value, out var res))
|
||||||
return new WAngle(res);
|
return new WAngle(res);
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseWRot(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseWRot(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
if (parts.Length == 3
|
if (parts.Length == 3
|
||||||
&& Exts.TryParseInt32Invariant(parts[0], out var rr)
|
&& Exts.TryParseInt32Invariant(parts[0], out var rr)
|
||||||
&& Exts.TryParseInt32Invariant(parts[1], out var rp)
|
&& Exts.TryParseInt32Invariant(parts[1], out var rp)
|
||||||
@@ -268,27 +283,31 @@ namespace OpenRA
|
|||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseCPos(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseCPos(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
if (parts.Length == 3)
|
if (parts.Length == 3
|
||||||
return new CPos(
|
&& Exts.TryParseInt32Invariant(parts[0], out var x)
|
||||||
Exts.ParseInt32Invariant(parts[0]),
|
&& Exts.TryParseInt32Invariant(parts[1], out var y)
|
||||||
Exts.ParseInt32Invariant(parts[1]),
|
&& Exts.TryParseByteInvariant(parts[2], out var layer))
|
||||||
Exts.ParseByteInvariant(parts[2]));
|
return new CPos(x, y, layer);
|
||||||
return new CPos(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
|
|
||||||
|
if (parts.Length == 2
|
||||||
|
&& Exts.TryParseInt32Invariant(parts[0], out x)
|
||||||
|
&& Exts.TryParseInt32Invariant(parts[1], out y))
|
||||||
|
return new CPos(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseCPosArray(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseCPosArray(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
if (parts.Length % 2 != 0)
|
if (parts.Length % 2 != 0)
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
@@ -296,9 +315,11 @@ namespace OpenRA
|
|||||||
var vecs = new CPos[parts.Length / 2];
|
var vecs = new CPos[parts.Length / 2];
|
||||||
for (var i = 0; i < vecs.Length; i++)
|
for (var i = 0; i < vecs.Length; i++)
|
||||||
{
|
{
|
||||||
if (int.TryParse(parts[2 * i], out var rx)
|
if (Exts.TryParseInt32Invariant(parts[2 * i], out var rx)
|
||||||
&& int.TryParse(parts[2 * i + 1], out var ry))
|
&& Exts.TryParseInt32Invariant(parts[2 * i + 1], out var ry))
|
||||||
vecs[i] = new CPos(rx, ry);
|
vecs[i] = new CPos(rx, ry);
|
||||||
|
else
|
||||||
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return vecs;
|
return vecs;
|
||||||
@@ -307,22 +328,25 @@ namespace OpenRA
|
|||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseCVec(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseCVec(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
return new CVec(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
|
if (parts.Length == 2
|
||||||
|
&& Exts.TryParseInt32Invariant(parts[0], out var x)
|
||||||
|
&& Exts.TryParseInt32Invariant(parts[1], out var y))
|
||||||
|
return new CVec(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseCVecArray(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseCVecArray(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
if (parts.Length % 2 != 0)
|
if (parts.Length % 2 != 0)
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
@@ -330,9 +354,11 @@ namespace OpenRA
|
|||||||
var vecs = new CVec[parts.Length / 2];
|
var vecs = new CVec[parts.Length / 2];
|
||||||
for (var i = 0; i < vecs.Length; i++)
|
for (var i = 0; i < vecs.Length; i++)
|
||||||
{
|
{
|
||||||
if (int.TryParse(parts[2 * i], out var rx)
|
if (Exts.TryParseInt32Invariant(parts[2 * i], out var rx)
|
||||||
&& int.TryParse(parts[2 * i + 1], out var ry))
|
&& Exts.TryParseInt32Invariant(parts[2 * i + 1], out var ry))
|
||||||
vecs[i] = new CVec(rx, ry);
|
vecs[i] = new CVec(rx, ry);
|
||||||
|
else
|
||||||
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
return vecs;
|
return vecs;
|
||||||
@@ -341,7 +367,7 @@ namespace OpenRA
|
|||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseBooleanExpression(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseBooleanExpression(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
@@ -351,14 +377,14 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
catch (InvalidDataException e)
|
catch (InvalidDataException e)
|
||||||
{
|
{
|
||||||
throw new YamlException(e.Message);
|
throw new YamlException($"FieldLoader: Cannot parse `{value}` into `{fieldName}.{fieldType}`: {e.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseIntegerExpression(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseIntegerExpression(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
@@ -368,44 +394,50 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
catch (InvalidDataException e)
|
catch (InvalidDataException e)
|
||||||
{
|
{
|
||||||
throw new YamlException(e.Message);
|
throw new YamlException($"FieldLoader: Cannot parse `{value}` into `{fieldName}.{fieldType}`: {e.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseEnum(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseEnum(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
try
|
// Will allow numeric values that fit the underlying type of the enum, even if they aren't defined enumeration members.
|
||||||
|
if (Enum.TryParse(fieldType, value, true, out var enumValue))
|
||||||
{
|
{
|
||||||
return Enum.Parse(fieldType, value, true);
|
return enumValue;
|
||||||
}
|
|
||||||
catch (ArgumentException)
|
|
||||||
{
|
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseBool(string fieldName, Type fieldType, string value, MemberInfo field)
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static object ParseBool(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (bool.TryParse(value.ToLowerInvariant(), out var result))
|
if (bool.TryParse(value, out var result))
|
||||||
return result ? BoxedTrue : BoxedFalse;
|
return result ? BoxedTrue : BoxedFalse;
|
||||||
|
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseInt2Array(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseInt2Array(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
if (parts.Length % 2 != 0)
|
if (parts.Length % 2 != 0)
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
|
|
||||||
var ints = new int2[parts.Length / 2];
|
var ints = new int2[parts.Length / 2];
|
||||||
|
|
||||||
for (var i = 0; i < ints.Length; i++)
|
for (var i = 0; i < ints.Length; i++)
|
||||||
ints[i] = new int2(Exts.ParseInt32Invariant(parts[2 * i]), Exts.ParseInt32Invariant(parts[2 * i + 1]));
|
{
|
||||||
|
if (Exts.TryParseInt32Invariant(parts[2 * i], out var x)
|
||||||
|
&& Exts.TryParseInt32Invariant(parts[2 * i + 1], out var y))
|
||||||
|
ints[i] = new int2(x, y);
|
||||||
|
else
|
||||||
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
return ints;
|
return ints;
|
||||||
}
|
}
|
||||||
@@ -413,109 +445,130 @@ namespace OpenRA
|
|||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseSize(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseSize(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
return new Size(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
|
if (parts.Length == 2
|
||||||
|
&& Exts.TryParseInt32Invariant(parts[0], out var width)
|
||||||
|
&& Exts.TryParseInt32Invariant(parts[1], out var height))
|
||||||
|
return new Size(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseInt2(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseInt2(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
if (parts.Length != 2)
|
if (parts.Length == 2
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
&& Exts.TryParseInt32Invariant(parts[0], out var x)
|
||||||
|
&& Exts.TryParseInt32Invariant(parts[1], out var y))
|
||||||
return new int2(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
|
return new int2(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseFloat2(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseFloat2(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
float xx = 0;
|
if (parts.Length == 2
|
||||||
float yy = 0;
|
&& Exts.TryParseFloatOrPercentInvariant(parts[0], out var x)
|
||||||
if (float.TryParse(parts[0].Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var res))
|
&& Exts.TryParseFloatOrPercentInvariant(parts[1], out var y))
|
||||||
xx = res * (parts[0].Contains('%') ? 0.01f : 1f);
|
return new float2(x, y);
|
||||||
if (float.TryParse(parts[1].Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out res))
|
|
||||||
yy = res * (parts[1].Contains('%') ? 0.01f : 1f);
|
|
||||||
return new float2(xx, yy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseFloat3(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseFloat3(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
float.TryParse(parts[0], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var x);
|
if (parts.Length == 3
|
||||||
float.TryParse(parts[1], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var y);
|
&& Exts.TryParseFloatOrPercentInvariant(parts[0], out var x)
|
||||||
|
&& Exts.TryParseFloatOrPercentInvariant(parts[1], out var y)
|
||||||
|
&& Exts.TryParseFloatOrPercentInvariant(parts[2], out var z))
|
||||||
|
return new float3(x, y, z);
|
||||||
|
|
||||||
// z component is optional for compatibility with older float2 definitions
|
// z component is optional for compatibility with older float2 definitions
|
||||||
float z = 0;
|
if (parts.Length == 2
|
||||||
if (parts.Length > 2)
|
&& Exts.TryParseFloatOrPercentInvariant(parts[0], out x)
|
||||||
float.TryParse(parts[2], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out z);
|
&& Exts.TryParseFloatOrPercentInvariant(parts[1], out y))
|
||||||
|
return new float3(x, y, 0);
|
||||||
return new float3(x, y, z);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseRectangle(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseRectangle(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
return new Rectangle(
|
if (parts.Length == 4
|
||||||
Exts.ParseInt32Invariant(parts[0]),
|
&& Exts.TryParseInt32Invariant(parts[0], out var x)
|
||||||
Exts.ParseInt32Invariant(parts[1]),
|
&& Exts.TryParseInt32Invariant(parts[1], out var y)
|
||||||
Exts.ParseInt32Invariant(parts[2]),
|
&& Exts.TryParseInt32Invariant(parts[2], out var width)
|
||||||
Exts.ParseInt32Invariant(parts[3]));
|
&& Exts.TryParseInt32Invariant(parts[3], out var height))
|
||||||
|
return new Rectangle(x, y, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseDateTime(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object ParseDateTime(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
if (DateTime.TryParseExact(value, "yyyy-MM-dd HH-mm-ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dt))
|
if (DateTime.TryParseExact(value, "yyyy-MM-dd HH-mm-ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dt))
|
||||||
return dt;
|
return dt;
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
return InvalidValueAction(value, fieldType, fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseHashSetOrList(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
|
static object ParseArray(string fieldName, Type fieldType, string value)
|
||||||
|
{
|
||||||
|
var elementType = fieldType.GetElementType();
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
return typeof(Array)
|
||||||
|
.GetMethod(nameof(Array.Empty))
|
||||||
|
.MakeGenericMethod(elementType)
|
||||||
|
.Invoke(null, null);
|
||||||
|
|
||||||
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
|
var ret = Array.CreateInstance(elementType, parts.Length);
|
||||||
|
for (var i = 0; i < parts.Length; i++)
|
||||||
|
ret.SetValue(GetValue(fieldName, elementType, parts[i]), i);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static object ParseHashSetOrList(string fieldName, Type fieldType, string value, MiniYaml yaml)
|
||||||
{
|
{
|
||||||
if (value == null)
|
if (value == null)
|
||||||
return Activator.CreateInstance(fieldType);
|
return Activator.CreateInstance(fieldType);
|
||||||
|
|
||||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
var set = Activator.CreateInstance(fieldType, parts.Length);
|
var set = Activator.CreateInstance(fieldType, parts.Length);
|
||||||
var arguments = fieldType.GetGenericArguments();
|
var arguments = fieldType.GetGenericArguments();
|
||||||
var addMethod = fieldType.GetMethod(nameof(List<object>.Add), arguments);
|
var addMethod = fieldType.GetMethod(nameof(List<object>.Add), arguments);
|
||||||
var addArgs = new object[1];
|
var addArgs = new object[1];
|
||||||
for (var i = 0; i < parts.Length; i++)
|
for (var i = 0; i < parts.Length; i++)
|
||||||
{
|
{
|
||||||
addArgs[0] = GetValue(fieldName, arguments[0], parts[i].Trim(), field);
|
addArgs[0] = GetValue(fieldName, arguments[0], parts[i]);
|
||||||
addMethod.Invoke(set, addArgs);
|
addMethod.Invoke(set, addArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseDictionary(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
|
static object ParseDictionary(string fieldName, Type fieldType, string value, MiniYaml yaml)
|
||||||
{
|
{
|
||||||
if (yaml == null)
|
if (yaml == null)
|
||||||
return Activator.CreateInstance(fieldType);
|
return Activator.CreateInstance(fieldType);
|
||||||
@@ -526,34 +579,97 @@ namespace OpenRA
|
|||||||
var addArgs = new object[2];
|
var addArgs = new object[2];
|
||||||
foreach (var node in yaml.Nodes)
|
foreach (var node in yaml.Nodes)
|
||||||
{
|
{
|
||||||
addArgs[0] = GetValue(fieldName, arguments[0], node.Key, field);
|
addArgs[0] = GetValue(fieldName, arguments[0], node.Key);
|
||||||
addArgs[1] = GetValue(fieldName, arguments[1], node.Value, field);
|
addArgs[1] = GetValue(fieldName, arguments[1], node.Value);
|
||||||
addMethod.Invoke(dict, addArgs);
|
addMethod.Invoke(dict, addArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
return dict;
|
return dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
static object ParseBitSet(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
|
static object ParseImmutableArray(string fieldName, Type fieldType, string value, MiniYaml yaml)
|
||||||
|
{
|
||||||
|
var typeArgs = fieldType.GenericTypeArguments;
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
return typeof(ImmutableArray<>).MakeGenericType(typeArgs)
|
||||||
|
.GetField(nameof(ImmutableArray<object>.Empty))
|
||||||
|
.GetValue(null);
|
||||||
|
|
||||||
|
object array;
|
||||||
|
if (typeArgs[0] == typeof(WVec))
|
||||||
|
array = ParseWVecArray(fieldName, typeArgs[0].MakeArrayType(), value);
|
||||||
|
else if (typeArgs[0] == typeof(CPos))
|
||||||
|
array = ParseCPosArray(fieldName, typeArgs[0].MakeArrayType(), value);
|
||||||
|
else if (typeArgs[0] == typeof(CVec))
|
||||||
|
array = ParseCVecArray(fieldName, typeArgs[0].MakeArrayType(), value);
|
||||||
|
else if (typeArgs[0] == typeof(int2))
|
||||||
|
array = ParseInt2Array(fieldName, typeArgs[0].MakeArrayType(), value);
|
||||||
|
else
|
||||||
|
array = ParseArray(fieldName, typeArgs[0].MakeArrayType(), value);
|
||||||
|
|
||||||
|
var toImmutableArray = ToImmutableArray.MakeGenericMethod(typeArgs);
|
||||||
|
|
||||||
|
return toImmutableArray.Invoke(null, [array]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static object ParseFrozenSet(string fieldName, Type fieldType, string value, MiniYaml yaml)
|
||||||
|
{
|
||||||
|
var typeArgs = fieldType.GenericTypeArguments;
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
return typeof(FrozenSet<>).MakeGenericType(typeArgs)
|
||||||
|
.GetProperty(nameof(FrozenSet<object>.Empty))
|
||||||
|
.GetValue(null);
|
||||||
|
|
||||||
|
var set =
|
||||||
|
ParseHashSetOrList(fieldName, typeof(HashSet<>).MakeGenericType(typeArgs), value, yaml);
|
||||||
|
|
||||||
|
var toFrozenSet = ToFrozenSet.MakeGenericMethod(typeArgs);
|
||||||
|
|
||||||
|
return toFrozenSet.Invoke(null, [set, null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static object ParseFrozenDictionary(string fieldName, Type fieldType, string value, MiniYaml yaml)
|
||||||
|
{
|
||||||
|
var typeArgs = fieldType.GenericTypeArguments;
|
||||||
|
|
||||||
|
if (yaml == null)
|
||||||
|
return typeof(FrozenDictionary<,>).MakeGenericType(typeArgs)
|
||||||
|
.GetProperty(nameof(FrozenDictionary<object, object>.Empty))
|
||||||
|
.GetValue(null);
|
||||||
|
|
||||||
|
var dict =
|
||||||
|
ParseDictionary(fieldName, typeof(Dictionary<,>).MakeGenericType(typeArgs), value, yaml);
|
||||||
|
|
||||||
|
var toFrozenDict = ToFrozenDictionary.MakeGenericMethod(typeArgs);
|
||||||
|
|
||||||
|
return toFrozenDict.Invoke(null, [dict, null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static object ParseBitSet(string fieldName, Type fieldType, string value, MiniYaml yaml)
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
var ctor = fieldType.GetConstructor(new[] { typeof(string[]) });
|
var ctor = fieldType.GetConstructor([typeof(string[])]);
|
||||||
return ctor.Invoke(new object[] { parts.Select(p => p.Trim()).ToArray() });
|
return ctor.Invoke([parts]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var ctor = fieldType.GetConstructor([typeof(string[])]);
|
||||||
|
return ctor.Invoke([Array.Empty<string>()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return InvalidValueAction(value, fieldType, fieldName);
|
static object ParseNullable(string fieldName, Type fieldType, string value, MiniYaml yaml)
|
||||||
}
|
|
||||||
|
|
||||||
static object ParseNullable(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var innerType = fieldType.GetGenericArguments()[0];
|
var innerType = fieldType.GetGenericArguments()[0];
|
||||||
var innerValue = GetValue("Nullable<T>", innerType, value, field);
|
var innerValue = GetValue("Nullable<T>", innerType, value);
|
||||||
return fieldType.GetConstructor(new[] { innerType }).Invoke(new[] { innerValue });
|
return fieldType.GetConstructor([innerType]).Invoke([innerValue]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Load(object self, MiniYaml my)
|
public static void Load(object self, MiniYaml my)
|
||||||
@@ -574,7 +690,7 @@ namespace OpenRA
|
|||||||
val = fli.Loader(my);
|
val = fli.Loader(my);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
missing ??= new List<string>();
|
missing ??= [];
|
||||||
missing.Add(fli.YamlName);
|
missing.Add(fli.YamlName);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -585,7 +701,7 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
if (fli.Attribute.Required)
|
if (fli.Attribute.Required)
|
||||||
{
|
{
|
||||||
missing ??= new List<string>();
|
missing ??= [];
|
||||||
missing.Add(fli.YamlName);
|
missing.Add(fli.YamlName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,7 +723,7 @@ namespace OpenRA
|
|||||||
if (!md.TryGetValue(yamlName, out var yaml))
|
if (!md.TryGetValue(yamlName, out var yaml))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
ret = GetValue(field.Name, field.FieldType, yaml, field);
|
ret = GetValue(field.Name, field.FieldType, yaml);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,7 +734,7 @@ namespace OpenRA
|
|||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void LoadField(object target, string key, string value)
|
public static void LoadFieldOrProperty(object target, string key, string value)
|
||||||
{
|
{
|
||||||
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||||
|
|
||||||
@@ -627,18 +743,14 @@ namespace OpenRA
|
|||||||
var field = target.GetType().GetField(key, Flags);
|
var field = target.GetType().GetField(key, Flags);
|
||||||
if (field != null)
|
if (field != null)
|
||||||
{
|
{
|
||||||
var sa = field.GetCustomAttributes<SerializeAttribute>(false).DefaultIfEmpty(SerializeAttribute.Default).First();
|
field.SetValue(target, GetValue(field.Name, field.FieldType, value));
|
||||||
if (!sa.FromYamlKey)
|
|
||||||
field.SetValue(target, GetValue(field.Name, field.FieldType, value, field));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var prop = target.GetType().GetProperty(key, Flags);
|
var prop = target.GetType().GetProperty(key, Flags);
|
||||||
if (prop != null)
|
if (prop != null)
|
||||||
{
|
{
|
||||||
var sa = prop.GetCustomAttributes<SerializeAttribute>(false).DefaultIfEmpty(SerializeAttribute.Default).First();
|
prop.SetValue(target, GetValue(prop.Name, prop.PropertyType, value), null);
|
||||||
if (!sa.FromYamlKey)
|
|
||||||
prop.SetValue(target, GetValue(prop.Name, prop.PropertyType, value, prop), null);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,48 +762,34 @@ namespace OpenRA
|
|||||||
return (T)GetValue(field, typeof(T), value, null);
|
return (T)GetValue(field, typeof(T), value, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static object GetValue(string fieldName, Type fieldType, string value)
|
static object GetValue(string fieldName, Type fieldType, string value)
|
||||||
{
|
{
|
||||||
return GetValue(fieldName, fieldType, value, null);
|
return GetValue(fieldName, fieldType, value, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static object GetValue(string fieldName, Type fieldType, string value, MemberInfo field)
|
static object GetValue(string fieldName, Type fieldType, MiniYaml yaml)
|
||||||
{
|
{
|
||||||
return GetValue(fieldName, fieldType, value, null, field);
|
return GetValue(fieldName, fieldType, yaml.Value, yaml);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static object GetValue(string fieldName, Type fieldType, MiniYaml yaml, MemberInfo field)
|
static object GetValue(string fieldName, Type fieldType, string value, MiniYaml yaml)
|
||||||
{
|
|
||||||
return GetValue(fieldName, fieldType, yaml.Value, yaml, field);
|
|
||||||
}
|
|
||||||
|
|
||||||
static object GetValue(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
|
|
||||||
{
|
{
|
||||||
value = value?.Trim();
|
value = value?.Trim();
|
||||||
if (fieldType.IsGenericType)
|
if (fieldType.IsGenericType)
|
||||||
{
|
{
|
||||||
if (GenericTypeParsers.TryGetValue(fieldType.GetGenericTypeDefinition(), out var parseFuncGeneric))
|
if (GenericTypeParsers.TryGetValue(fieldType.GetGenericTypeDefinition(), out var parseFuncGeneric))
|
||||||
return parseFuncGeneric(fieldName, fieldType, value, yaml, field);
|
return parseFuncGeneric(fieldName, fieldType, value, yaml);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (TypeParsers.TryGetValue(fieldType, out var parseFunc))
|
if (TypeParsers.TryGetValue(fieldType, out var parseFunc))
|
||||||
return parseFunc(fieldName, fieldType, value, field);
|
return parseFunc(fieldName, fieldType, value);
|
||||||
|
|
||||||
if (fieldType.IsArray && fieldType.GetArrayRank() == 1)
|
if (fieldType.IsArray && fieldType.GetArrayRank() == 1)
|
||||||
{
|
return ParseArray(fieldName, fieldType, value);
|
||||||
if (value == null)
|
|
||||||
return Array.CreateInstance(fieldType.GetElementType(), 0);
|
|
||||||
|
|
||||||
var options = field != null && field.HasAttribute<AllowEmptyEntriesAttribute>() ?
|
if (fieldType.IsEnum)
|
||||||
StringSplitOptions.None : StringSplitOptions.RemoveEmptyEntries;
|
return ParseEnum(fieldName, fieldType, value);
|
||||||
var parts = value.Split(SplitComma, options);
|
|
||||||
|
|
||||||
var ret = Array.CreateInstance(fieldType.GetElementType(), parts.Length);
|
|
||||||
for (var i = 0; i < parts.Length; i++)
|
|
||||||
ret.SetValue(GetValue(fieldName, fieldType.GetElementType(), parts[i].Trim(), field), i);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var conv = TypeDescriptor.GetConverter(fieldType);
|
var conv = TypeDescriptor.GetConverter(fieldType);
|
||||||
@@ -715,21 +813,20 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
public readonly FieldInfo Field;
|
public readonly FieldInfo Field;
|
||||||
public readonly SerializeAttribute Attribute;
|
public readonly SerializeAttribute Attribute;
|
||||||
public readonly string YamlName;
|
|
||||||
public readonly Func<MiniYaml, object> Loader;
|
public readonly Func<MiniYaml, object> Loader;
|
||||||
|
public string YamlName => Field.Name;
|
||||||
|
|
||||||
internal FieldLoadInfo(FieldInfo field, SerializeAttribute attr, string yamlName, Func<MiniYaml, object> loader = null)
|
public FieldLoadInfo(FieldInfo field, SerializeAttribute attr, Func<MiniYaml, object> loader = null)
|
||||||
{
|
{
|
||||||
Field = field;
|
Field = field;
|
||||||
Attribute = attr;
|
Attribute = attr;
|
||||||
YamlName = yamlName;
|
|
||||||
Loader = loader;
|
Loader = loader;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<FieldLoadInfo> GetTypeLoadInfo(Type type, bool includePrivateByDefault = false)
|
public static IEnumerable<FieldLoadInfo> GetTypeLoadInfo(Type type)
|
||||||
{
|
{
|
||||||
return TypeLoadInfo[type].Where(fli => includePrivateByDefault || fli.Field.IsPublic || (fli.Attribute.Serialize && !fli.Attribute.IsDefault));
|
return TypeLoadInfo[type].Where(fli => fli.Field.IsPublic || (fli.Attribute.Serialize && !fli.Attribute.IsDefault));
|
||||||
}
|
}
|
||||||
|
|
||||||
static FieldLoadInfo[] BuildTypeLoadInfo(Type type)
|
static FieldLoadInfo[] BuildTypeLoadInfo(Type type)
|
||||||
@@ -744,13 +841,9 @@ namespace OpenRA
|
|||||||
if (!sa.Serialize)
|
if (!sa.Serialize)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var yamlName = string.IsNullOrEmpty(sa.YamlName) ? field.Name : sa.YamlName;
|
|
||||||
|
|
||||||
var loader = sa.GetLoader(type);
|
var loader = sa.GetLoader(type);
|
||||||
if (loader == null && sa.FromYamlKey)
|
|
||||||
loader = yaml => GetValue(yamlName, field.FieldType, yaml, field);
|
|
||||||
|
|
||||||
var fli = new FieldLoadInfo(field, sa, yamlName, loader);
|
var fli = new FieldLoadInfo(field, sa, loader);
|
||||||
ret.Add(fli);
|
ret.Add(fli);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -761,31 +854,21 @@ namespace OpenRA
|
|||||||
public sealed class IgnoreAttribute : SerializeAttribute
|
public sealed class IgnoreAttribute : SerializeAttribute
|
||||||
{
|
{
|
||||||
public IgnoreAttribute()
|
public IgnoreAttribute()
|
||||||
: base(false) { }
|
: base(serialize: false) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field)]
|
[AttributeUsage(AttributeTargets.Field)]
|
||||||
public sealed class RequireAttribute : SerializeAttribute
|
public sealed class RequireAttribute : SerializeAttribute
|
||||||
{
|
{
|
||||||
public RequireAttribute()
|
public RequireAttribute()
|
||||||
: base(true, true) { }
|
: base(serialize: true, required: true) { }
|
||||||
}
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field)]
|
|
||||||
public sealed class AllowEmptyEntriesAttribute : SerializeAttribute
|
|
||||||
{
|
|
||||||
public AllowEmptyEntriesAttribute()
|
|
||||||
: base(allowEmptyEntries: true) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field)]
|
[AttributeUsage(AttributeTargets.Field)]
|
||||||
public sealed class LoadUsingAttribute : SerializeAttribute
|
public sealed class LoadUsingAttribute : SerializeAttribute
|
||||||
{
|
{
|
||||||
public LoadUsingAttribute(string loader, bool required = false)
|
public LoadUsingAttribute(string loader, bool required = false)
|
||||||
{
|
: base(serialize: true, required, loader) { }
|
||||||
Loader = loader;
|
|
||||||
Required = required;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field)]
|
[AttributeUsage(AttributeTargets.Field)]
|
||||||
@@ -796,18 +879,14 @@ namespace OpenRA
|
|||||||
public bool IsDefault => this == Default;
|
public bool IsDefault => this == Default;
|
||||||
|
|
||||||
public readonly bool Serialize;
|
public readonly bool Serialize;
|
||||||
public string YamlName;
|
public readonly bool Required;
|
||||||
public string Loader;
|
public readonly string Loader;
|
||||||
public bool FromYamlKey;
|
|
||||||
public bool DictionaryFromYamlKey;
|
|
||||||
public bool Required;
|
|
||||||
public bool AllowEmptyEntries;
|
|
||||||
|
|
||||||
public SerializeAttribute(bool serialize = true, bool required = false, bool allowEmptyEntries = false)
|
protected SerializeAttribute(bool serialize = true, bool required = false, string loader = null)
|
||||||
{
|
{
|
||||||
Serialize = serialize;
|
Serialize = serialize;
|
||||||
Required = required;
|
Required = required;
|
||||||
AllowEmptyEntries = allowEmptyEntries;
|
Loader = loader;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Func<MiniYaml, object> GetLoader(Type type)
|
internal Func<MiniYaml, object> GetLoader(Type type)
|
||||||
@@ -827,32 +906,4 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Field)]
|
|
||||||
public sealed class FieldFromYamlKeyAttribute : FieldLoader.SerializeAttribute
|
|
||||||
{
|
|
||||||
public FieldFromYamlKeyAttribute()
|
|
||||||
{
|
|
||||||
FromYamlKey = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special-cases FieldFromYamlKeyAttribute for use with Dictionary<K,V>.
|
|
||||||
[AttributeUsage(AttributeTargets.Field)]
|
|
||||||
public sealed class DictionaryFromYamlKeyAttribute : FieldLoader.SerializeAttribute
|
|
||||||
{
|
|
||||||
public DictionaryFromYamlKeyAttribute()
|
|
||||||
{
|
|
||||||
FromYamlKey = true;
|
|
||||||
DictionaryFromYamlKey = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mirrors DescriptionAttribute from System.ComponentModel but we don't want to have to use that everywhere.
|
|
||||||
[AttributeUsage(AttributeTargets.All)]
|
|
||||||
public sealed class DescAttribute : Attribute
|
|
||||||
{
|
|
||||||
public readonly string[] Lines;
|
|
||||||
public DescAttribute(params string[] lines) { Lines = lines; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -22,39 +24,38 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
public static class FieldSaver
|
public static class FieldSaver
|
||||||
{
|
{
|
||||||
public static MiniYaml Save(object o, bool includePrivateByDefault = false)
|
public static MiniYaml Save(object o)
|
||||||
{
|
{
|
||||||
var nodes = new List<MiniYamlNode>();
|
var nodes = new List<MiniYamlNode>();
|
||||||
string root = null;
|
|
||||||
|
|
||||||
foreach (var info in FieldLoader.GetTypeLoadInfo(o.GetType(), includePrivateByDefault))
|
foreach (var fieldInfo in FieldLoader.GetTypeLoadInfo(o.GetType()))
|
||||||
{
|
{
|
||||||
if (info.Attribute.DictionaryFromYamlKey)
|
if (fieldInfo.Field.FieldType.IsGenericType && fieldInfo.Field.FieldType.IsAssignableTo(typeof(System.Collections.IDictionary)))
|
||||||
{
|
{
|
||||||
var dict = (System.Collections.IDictionary)info.Field.GetValue(o);
|
var dict = (System.Collections.IDictionary)fieldInfo.Field.GetValue(o);
|
||||||
|
var dictNodes = new List<MiniYamlNode>();
|
||||||
foreach (var kvp in dict)
|
foreach (var kvp in dict)
|
||||||
{
|
{
|
||||||
var key = ((System.Collections.DictionaryEntry)kvp).Key;
|
var key = ((System.Collections.DictionaryEntry)kvp).Key;
|
||||||
var value = ((System.Collections.DictionaryEntry)kvp).Value;
|
var value = ((System.Collections.DictionaryEntry)kvp).Value;
|
||||||
|
dictNodes.Add(new MiniYamlNode(FormatValue(key), FormatValue(value)));
|
||||||
|
}
|
||||||
|
|
||||||
nodes.Add(new MiniYamlNode(FormatValue(key), FormatValue(value)));
|
nodes.Add(new MiniYamlNode(fieldInfo.YamlName, "", dictNodes));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (info.Attribute.FromYamlKey)
|
|
||||||
root = FormatValue(o, info.Field);
|
|
||||||
else
|
else
|
||||||
nodes.Add(new MiniYamlNode(info.YamlName, FormatValue(o, info.Field)));
|
nodes.Add(new MiniYamlNode(fieldInfo.YamlName, FormatValue(o, fieldInfo.Field)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MiniYaml(root, nodes);
|
return new MiniYaml(null, nodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MiniYaml SaveDifferences(object o, object from, bool includePrivateByDefault = false)
|
public static MiniYaml SaveDifferences(object o, object from)
|
||||||
{
|
{
|
||||||
if (o.GetType() != from.GetType())
|
if (o.GetType() != from.GetType())
|
||||||
throw new InvalidOperationException("FieldLoader: can't diff objects of different types");
|
throw new InvalidOperationException("FieldSaver: can't diff objects of different types");
|
||||||
|
|
||||||
var fields = FieldLoader.GetTypeLoadInfo(o.GetType(), includePrivateByDefault)
|
var fields = FieldLoader.GetTypeLoadInfo(o.GetType())
|
||||||
.Where(info => FormatValue(o, info.Field) != FormatValue(from, info.Field));
|
.Where(info => FormatValue(o, info.Field) != FormatValue(from, info.Field));
|
||||||
|
|
||||||
return new MiniYaml(
|
return new MiniYaml(
|
||||||
@@ -79,11 +80,35 @@ namespace OpenRA
|
|||||||
if (t.IsArray && t.GetArrayRank() == 1)
|
if (t.IsArray && t.GetArrayRank() == 1)
|
||||||
return ((Array)v).Cast<object>().Select(FormatValue).JoinWith(", ");
|
return ((Array)v).Cast<object>().Select(FormatValue).JoinWith(", ");
|
||||||
|
|
||||||
if (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(HashSet<>) || t.GetGenericTypeDefinition() == typeof(List<>)))
|
if (t.IsGenericType &&
|
||||||
|
t.GetGenericTypeDefinition() == typeof(ImmutableArray<>))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return ((System.Collections.IEnumerable)v).Cast<object>().Select(FormatValue).JoinWith(", ");
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t.IsGenericType &&
|
||||||
|
(t.GetGenericTypeDefinition() == typeof(List<>) ||
|
||||||
|
t.GetGenericTypeDefinition() == typeof(HashSet<>) ||
|
||||||
|
t.GetGenericTypeDefinition()
|
||||||
|
.BaseTypes()
|
||||||
|
.Select(bt => bt.IsGenericType ? bt.GetGenericTypeDefinition() : null)
|
||||||
|
.Any(bt => bt == typeof(FrozenSet<>))))
|
||||||
return ((System.Collections.IEnumerable)v).Cast<object>().Select(FormatValue).JoinWith(", ");
|
return ((System.Collections.IEnumerable)v).Cast<object>().Select(FormatValue).JoinWith(", ");
|
||||||
|
|
||||||
// This is only for documentation generation
|
// This is only for documentation generation
|
||||||
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
if (t.IsGenericType &&
|
||||||
|
(t.GetGenericTypeDefinition() == typeof(Dictionary<,>) ||
|
||||||
|
t.GetGenericTypeDefinition()
|
||||||
|
.BaseTypes()
|
||||||
|
.Select(bt => bt.IsGenericType ? bt.GetGenericTypeDefinition() : null)
|
||||||
|
.Any(bt => bt == typeof(FrozenDictionary<,>))))
|
||||||
{
|
{
|
||||||
var result = new StringBuilder();
|
var result = new StringBuilder();
|
||||||
var dict = (System.Collections.IDictionary)v;
|
var dict = (System.Collections.IDictionary)v;
|
||||||
@@ -95,7 +120,7 @@ namespace OpenRA
|
|||||||
var formattedKey = FormatValue(key);
|
var formattedKey = FormatValue(key);
|
||||||
var formattedValue = FormatValue(value);
|
var formattedValue = FormatValue(value);
|
||||||
|
|
||||||
result.Append($"{formattedKey}: {formattedValue}{Environment.NewLine}");
|
result.Append(CultureInfo.InvariantCulture, $"{formattedKey}: {formattedValue}{Environment.NewLine}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.ToString();
|
return result.ToString();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace OpenRA.Mods.Cnc.FileFormats
|
namespace OpenRA.FileFormats
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Static class that uses a lookup table to calculates CRC32
|
/// Static class that uses a lookup table to calculates CRC32
|
||||||
@@ -22,8 +22,8 @@ namespace OpenRA.Mods.Cnc.FileFormats
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The CRC32 lookup table.
|
/// The CRC32 lookup table.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
static readonly uint[] LookUp = new uint[256]
|
static readonly uint[] LookUp =
|
||||||
{
|
[
|
||||||
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
|
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
|
||||||
0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
|
0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
|
||||||
0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
|
0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
|
||||||
@@ -88,7 +88,7 @@ namespace OpenRA.Mods.Cnc.FileFormats
|
|||||||
0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
|
0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
|
||||||
0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
|
0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
|
||||||
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
|
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
|
||||||
};
|
];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A CRC32 implementation that can be used on spans of bytes.
|
/// A CRC32 implementation that can be used on spans of bytes.
|
||||||
@@ -118,5 +118,16 @@ namespace OpenRA.Mods.Cnc.FileFormats
|
|||||||
{
|
{
|
||||||
return Calculate(data, 0xFFFFFFFF);
|
return Calculate(data, 0xFFFFFFFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chaining method: accepts the current CRC state
|
||||||
|
public static uint Update(uint crc, ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < data.Length; i++)
|
||||||
|
crc = (crc >> 8) ^ LookUp[(crc & 0xFF) ^ data[i]];
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard PNG start: 0xFFFFFFFF, Finalize: XOR 0xFFFFFFFF
|
||||||
|
public static uint Finish(uint crc) => crc ^ 0xFFFFFFFF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,29 +10,105 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using ICSharpCode.SharpZipLib.Checksum;
|
|
||||||
using ICSharpCode.SharpZipLib.Zip.Compression;
|
|
||||||
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
|
|
||||||
using OpenRA.Graphics;
|
using OpenRA.Graphics;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
|
|
||||||
namespace OpenRA.FileFormats
|
namespace OpenRA.FileFormats
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used to connect several IDAT chunks into a continuous stream.
|
||||||
|
/// </summary>
|
||||||
|
sealed class PngIdatStream : Stream
|
||||||
|
{
|
||||||
|
readonly Stream baseStream;
|
||||||
|
int remainingInChunk;
|
||||||
|
bool eof;
|
||||||
|
|
||||||
|
public PngIdatStream(Stream baseStream, int initialLen)
|
||||||
|
{
|
||||||
|
this.baseStream = baseStream;
|
||||||
|
remainingInChunk = initialLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Read(byte[] buffer, int offset, int count) => Read(buffer.AsSpan(offset, count));
|
||||||
|
|
||||||
|
public override int Read(Span<byte> buffer)
|
||||||
|
{
|
||||||
|
if (eof || buffer.Length == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var totalRead = 0;
|
||||||
|
Span<byte> header = stackalloc byte[8];
|
||||||
|
while (buffer.Length > 0)
|
||||||
|
{
|
||||||
|
if (remainingInChunk == 0)
|
||||||
|
{
|
||||||
|
// Skip CRC and read next chunk header.
|
||||||
|
var r = baseStream.Seek(4, SeekOrigin.Current) + baseStream.Read(header);
|
||||||
|
if (r < 12)
|
||||||
|
throw new EndOfStreamException("Invalid PNG file - no end chunk found.");
|
||||||
|
|
||||||
|
// The PNG spec states that IDAT chunks must be chained together.
|
||||||
|
if (BinaryPrimitives.ReadUInt32BigEndian(header[4..]) == Png.ChunkIDAT)
|
||||||
|
remainingInChunk = BinaryPrimitives.ReadInt32BigEndian(header[..4]);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This is not an IDAT chunk. Discontinue reading.
|
||||||
|
baseStream.Seek(-8, SeekOrigin.Current);
|
||||||
|
eof = true;
|
||||||
|
return totalRead;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var toRead = Math.Min(buffer.Length, remainingInChunk);
|
||||||
|
var read = baseStream.Read(buffer[..toRead]);
|
||||||
|
if (read == 0)
|
||||||
|
throw new EndOfStreamException("Unexpected end of stream in IDAT chunk.");
|
||||||
|
|
||||||
|
remainingInChunk -= read;
|
||||||
|
totalRead += read;
|
||||||
|
buffer = buffer[read..];
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanRead => true;
|
||||||
|
public override bool CanSeek => false;
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
public override long Length => throw new NotSupportedException();
|
||||||
|
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
|
||||||
|
public override void Flush() { }
|
||||||
|
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||||
|
public override void SetLength(long value) => throw new NotSupportedException();
|
||||||
|
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
public class Png
|
public class Png
|
||||||
{
|
{
|
||||||
static readonly byte[] Signature = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
|
public const uint ChunkIHDR = 0x49484452;
|
||||||
|
public const uint ChunkPLTE = 0x504C5445;
|
||||||
|
public const uint ChunkIDAT = 0x49444154;
|
||||||
|
public const uint ChunkIEND = 0x49454E44;
|
||||||
|
public const uint ChunkTRNS = 0x74524E53;
|
||||||
|
public const uint ChunkTEXT = 0x74455874;
|
||||||
|
|
||||||
|
static readonly byte[] Signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
||||||
|
|
||||||
public int Width { get; }
|
public int Width { get; }
|
||||||
public int Height { get; }
|
public int Height { get; }
|
||||||
public Color[] Palette { get; }
|
public Color[] Palette { get; }
|
||||||
public byte[] Data { get; }
|
public byte[] Data { get; }
|
||||||
public SpriteFrameType Type { get; }
|
public SpriteFrameType Type { get; }
|
||||||
public Dictionary<string, string> EmbeddedData = new();
|
public Dictionary<string, string> EmbeddedData = [];
|
||||||
|
|
||||||
public int PixelStride => Type == SpriteFrameType.Indexed8 ? 1 : Type == SpriteFrameType.Rgb24 ? 3 : 4;
|
public int PixelStride => Type == SpriteFrameType.Indexed8 ? 1 : Type == SpriteFrameType.Rgb24 ? 3 : 4;
|
||||||
|
|
||||||
@@ -43,43 +119,50 @@ namespace OpenRA.FileFormats
|
|||||||
|
|
||||||
s.Position += 8;
|
s.Position += 8;
|
||||||
var headerParsed = false;
|
var headerParsed = false;
|
||||||
var data = new List<byte>();
|
var dataParsed = false;
|
||||||
|
byte bitDepth = 8;
|
||||||
Type = SpriteFrameType.Rgba32;
|
Type = SpriteFrameType.Rgba32;
|
||||||
|
|
||||||
byte bitDepth = 8;
|
// Use a reusable 8-byte buffer for Length + Type.
|
||||||
|
Span<byte> chunkHeader = stackalloc byte[8];
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var length = IPAddress.NetworkToHostOrder(s.ReadInt32());
|
if (s.Read(chunkHeader) < 8)
|
||||||
var type = s.ReadASCII(4);
|
throw new EndOfStreamException("Invalid PNG file - no end chunk found.");
|
||||||
var content = s.ReadBytes(length);
|
|
||||||
s.ReadInt32(); // crc
|
|
||||||
|
|
||||||
if (!headerParsed && type != "IHDR")
|
var length = BinaryPrimitives.ReadInt32BigEndian(chunkHeader[..4]);
|
||||||
|
var type = BinaryPrimitives.ReadUInt32BigEndian(chunkHeader[4..]);
|
||||||
|
|
||||||
|
if (!headerParsed && type != ChunkIHDR)
|
||||||
throw new InvalidDataException("Invalid PNG file - header does not appear first.");
|
throw new InvalidDataException("Invalid PNG file - header does not appear first.");
|
||||||
|
|
||||||
using (var ms = new MemoryStream(content))
|
|
||||||
{
|
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case "IHDR":
|
case ChunkIHDR:
|
||||||
{
|
{
|
||||||
if (headerParsed)
|
if (headerParsed)
|
||||||
throw new InvalidDataException("Invalid PNG file - duplicate header.");
|
throw new InvalidDataException("Invalid PNG file - duplicate header.");
|
||||||
Width = IPAddress.NetworkToHostOrder(ms.ReadInt32());
|
|
||||||
Height = IPAddress.NetworkToHostOrder(ms.ReadInt32());
|
|
||||||
|
|
||||||
bitDepth = ms.ReadUInt8();
|
#pragma warning disable CA2014 // Do not use stackalloc in loops
|
||||||
var colorType = (PngColorType)ms.ReadUInt8();
|
Span<byte> buffer = stackalloc byte[13];
|
||||||
|
#pragma warning restore CA2014 // Do not use stackalloc in loops
|
||||||
|
s.ReadBytes(buffer);
|
||||||
|
|
||||||
|
Width = BinaryPrimitives.ReadInt32BigEndian(buffer[..4]);
|
||||||
|
Height = BinaryPrimitives.ReadInt32BigEndian(buffer[4..8]);
|
||||||
|
bitDepth = buffer[8];
|
||||||
|
var colorType = (PngColorType)buffer[9];
|
||||||
|
|
||||||
if (IsPaletted(bitDepth, colorType))
|
if (IsPaletted(bitDepth, colorType))
|
||||||
Type = SpriteFrameType.Indexed8;
|
Type = SpriteFrameType.Indexed8;
|
||||||
else if (colorType == PngColorType.Color)
|
else if (colorType == PngColorType.Color)
|
||||||
Type = SpriteFrameType.Rgb24;
|
Type = SpriteFrameType.Rgb24;
|
||||||
|
|
||||||
Data = new byte[Width * Height * PixelStride];
|
var compression = buffer[10];
|
||||||
|
|
||||||
var compression = ms.ReadUInt8();
|
// filter = buffer[11]
|
||||||
ms.ReadUInt8(); // filter
|
var interlace = buffer[12];
|
||||||
var interlace = ms.ReadUInt8();
|
|
||||||
|
|
||||||
if (compression != 0)
|
if (compression != 0)
|
||||||
throw new InvalidDataException("Compression method not supported");
|
throw new InvalidDataException("Compression method not supported");
|
||||||
@@ -87,73 +170,156 @@ namespace OpenRA.FileFormats
|
|||||||
if (interlace != 0)
|
if (interlace != 0)
|
||||||
throw new InvalidDataException("Interlacing not supported");
|
throw new InvalidDataException("Interlacing not supported");
|
||||||
|
|
||||||
|
Data = new byte[Width * Height * PixelStride];
|
||||||
|
|
||||||
|
// Skip CRC.
|
||||||
|
s.Seek(4, SeekOrigin.Current);
|
||||||
headerParsed = true;
|
headerParsed = true;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "PLTE":
|
case ChunkPLTE:
|
||||||
{
|
{
|
||||||
Palette = new Color[length / 3];
|
if (length % 3 != 0)
|
||||||
for (var i = 0; i < Palette.Length; i++)
|
throw new InvalidDataException("Invalid PLTE chunk length.");
|
||||||
|
|
||||||
|
var count = length / 3;
|
||||||
|
Palette = new Color[count];
|
||||||
|
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent(length);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var r = ms.ReadUInt8(); var g = ms.ReadUInt8(); var b = ms.ReadUInt8();
|
s.ReadBytes(buffer.AsSpan(0, length));
|
||||||
Palette[i] = Color.FromArgb(r, g, b);
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var offset = i * 3;
|
||||||
|
Palette[i] = Color.FromArgb(buffer[offset], buffer[offset + 1], buffer[offset + 2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip CRC.
|
||||||
|
s.Seek(4, SeekOrigin.Current);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "tRNS":
|
case ChunkTRNS:
|
||||||
{
|
{
|
||||||
if (Palette == null)
|
if (Palette == null)
|
||||||
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
|
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
|
||||||
|
|
||||||
for (var i = 0; i < length; i++)
|
var buffer = ArrayPool<byte>.Shared.Rent(length);
|
||||||
Palette[i] = Color.FromArgb(ms.ReadUInt8(), Palette[i]);
|
try
|
||||||
|
{
|
||||||
|
s.ReadBytes(buffer.AsSpan(0, length));
|
||||||
|
for (var i = 0; i < length && i < Palette.Length; i++)
|
||||||
|
Palette[i] = Color.FromArgb(buffer[i], Palette[i]);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip CRC.
|
||||||
|
s.Seek(4, SeekOrigin.Current);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "IDAT":
|
case ChunkIDAT:
|
||||||
{
|
if (dataParsed)
|
||||||
data.AddRange(content);
|
throw new InvalidDataException("Invalid PNG file - discontinuous IDAT chunks.");
|
||||||
|
|
||||||
|
dataParsed = true;
|
||||||
|
|
||||||
|
ProcessIDAT(s, length, bitDepth);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ChunkTEXT:
|
||||||
|
{
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent(length);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var span = buffer.AsSpan(0, length);
|
||||||
|
s.ReadBytes(span);
|
||||||
|
|
||||||
|
// Find Null Terminator for ASCIIZ (Keyword).
|
||||||
|
var nullIndex = span.IndexOf((byte)0);
|
||||||
|
if (nullIndex == -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var key = Encoding.ASCII.GetString(span[..nullIndex]);
|
||||||
|
var value = Encoding.ASCII.GetString(span[(nullIndex + 1)..]);
|
||||||
|
|
||||||
|
EmbeddedData[key] = value;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip CRC.
|
||||||
|
s.Seek(4, SeekOrigin.Current);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "tEXt":
|
case ChunkIEND:
|
||||||
{
|
{
|
||||||
var key = ms.ReadASCIIZ();
|
if (Type == SpriteFrameType.Indexed8 && Palette == null)
|
||||||
EmbeddedData.Add(key, ms.ReadASCII(length - key.Length - 1));
|
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
|
||||||
|
|
||||||
break;
|
// Skip CRC.
|
||||||
|
s.Seek(4, SeekOrigin.Current);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "IEND":
|
default:
|
||||||
{
|
{
|
||||||
using (var ns = new MemoryStream(data.ToArray()))
|
// Skip unknown chunk + CRC.
|
||||||
{
|
s.Seek(length + 4, SeekOrigin.Current);
|
||||||
using (var ds = new InflaterInputStream(ns))
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessIDAT(Stream s, int firstChunkLength, byte bitDepth)
|
||||||
{
|
{
|
||||||
var pxStride = PixelStride;
|
var pxStride = PixelStride;
|
||||||
var rowStride = Width * pxStride;
|
var rowStride = Width * pxStride;
|
||||||
var pixelsPerByte = 8 / bitDepth;
|
var pixelsPerByte = 8 / bitDepth;
|
||||||
var sourceRowStride = Exts.IntegerDivisionRoundingAwayFromZero(rowStride, pixelsPerByte);
|
var sourceRowStride = Exts.IntegerDivisionRoundingAwayFromZero(rowStride, pixelsPerByte);
|
||||||
|
|
||||||
Span<byte> prevLine = new byte[rowStride];
|
// Custom stream that stitches IDAT chunks together without copying.
|
||||||
|
using var idatStream = new PngIdatStream(s, firstChunkLength);
|
||||||
|
using var ds = new ZLibStream(idatStream, CompressionMode.Decompress);
|
||||||
|
|
||||||
|
// Rent buffer from pool to avoid allocation churn.
|
||||||
|
var prevLineBuffer = ArrayPool<byte>.Shared.Rent(rowStride);
|
||||||
|
var prevLine = prevLineBuffer.AsSpan(0, rowStride);
|
||||||
|
prevLine.Clear();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
for (var y = 0; y < Height; y++)
|
for (var y = 0; y < Height; y++)
|
||||||
{
|
{
|
||||||
var filter = (PngFilter)ds.ReadUInt8();
|
var filterByte = ds.ReadByte();
|
||||||
ds.ReadBytes(Data, y * rowStride, sourceRowStride);
|
if (filterByte == -1)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var filter = (PngFilter)filterByte;
|
||||||
var line = Data.AsSpan(y * rowStride, rowStride);
|
var line = Data.AsSpan(y * rowStride, rowStride);
|
||||||
|
|
||||||
|
ds.ReadBytes(line[..sourceRowStride]);
|
||||||
|
|
||||||
// If the source has a bit depth of 1, 2 or 4 it packs multiple pixels per byte.
|
// 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.
|
// Unpack to bit depth of 8, yielding 1 pixel per byte.
|
||||||
// This makes life easier for consumers of palleted data.
|
// This makes life easier for consumers of palleted data.
|
||||||
if (bitDepth < 8)
|
if (bitDepth < 8)
|
||||||
{
|
{
|
||||||
var mask = 0xFF >> (8 - bitDepth);
|
var mask = (1 << bitDepth) - 1;
|
||||||
for (var i = sourceRowStride - 1; i >= 0; i--)
|
for (var i = sourceRowStride - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
var packed = line[i];
|
var packed = line[i];
|
||||||
@@ -161,7 +327,7 @@ namespace OpenRA.FileFormats
|
|||||||
{
|
{
|
||||||
var dest = i * pixelsPerByte + j;
|
var dest = i * pixelsPerByte + j;
|
||||||
if (dest < line.Length) // Guard against last byte being only partially packed
|
if (dest < line.Length) // Guard against last byte being only partially packed
|
||||||
line[dest] = (byte)(packed >> (8 - (j + 1) * bitDepth) & mask);
|
line[dest] = (byte)((packed >> (8 - (j + 1) * bitDepth)) & mask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,23 +337,23 @@ namespace OpenRA.FileFormats
|
|||||||
case PngFilter.None:
|
case PngFilter.None:
|
||||||
break;
|
break;
|
||||||
case PngFilter.Sub:
|
case PngFilter.Sub:
|
||||||
for (var i = pxStride; i < rowStride; i++)
|
for (var i = pxStride; i < line.Length; i++)
|
||||||
line[i] += line[i - pxStride];
|
line[i] += line[i - pxStride];
|
||||||
break;
|
break;
|
||||||
case PngFilter.Up:
|
case PngFilter.Up:
|
||||||
for (var i = 0; i < rowStride; i++)
|
for (var i = 0; i < line.Length; i++)
|
||||||
line[i] += prevLine[i];
|
line[i] += prevLine[i];
|
||||||
break;
|
break;
|
||||||
case PngFilter.Average:
|
case PngFilter.Average:
|
||||||
for (var i = 0; i < pxStride; i++)
|
for (var i = 0; i < pxStride; i++)
|
||||||
line[i] += Average(0, prevLine[i]);
|
line[i] += Average(0, prevLine[i]);
|
||||||
for (var i = pxStride; i < rowStride; i++)
|
for (var i = pxStride; i < line.Length; i++)
|
||||||
line[i] += Average(line[i - pxStride], prevLine[i]);
|
line[i] += Average(line[i - pxStride], prevLine[i]);
|
||||||
break;
|
break;
|
||||||
case PngFilter.Paeth:
|
case PngFilter.Paeth:
|
||||||
for (var i = 0; i < pxStride; i++)
|
for (var i = 0; i < pxStride; i++)
|
||||||
line[i] += Paeth(0, prevLine[i], 0);
|
line[i] += Paeth(0, prevLine[i], 0);
|
||||||
for (var i = pxStride; i < rowStride; i++)
|
for (var i = pxStride; i < line.Length; i++)
|
||||||
line[i] += Paeth(line[i - pxStride], prevLine[i], prevLine[i - pxStride]);
|
line[i] += Paeth(line[i - pxStride], prevLine[i], prevLine[i - pxStride]);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -197,6 +363,15 @@ namespace OpenRA.FileFormats
|
|||||||
prevLine = line;
|
prevLine = line;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drain remaining Zlib footer bytes if necessary.
|
||||||
|
Span<byte> drain = stackalloc byte[16];
|
||||||
|
while (ds.Read(drain) > 0) { }
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(prevLineBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
static byte Average(byte a, byte b) => (byte)((a + b) / 2);
|
static byte Average(byte a, byte b) => (byte)((a + b) / 2);
|
||||||
|
|
||||||
static byte Paeth(byte a, byte b, byte c)
|
static byte Paeth(byte a, byte b, byte c)
|
||||||
@@ -210,17 +385,6 @@ namespace OpenRA.FileFormats
|
|||||||
(pb <= pc) ? b : c;
|
(pb <= pc) ? b : c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (Type == SpriteFrameType.Indexed8 && Palette == null)
|
|
||||||
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Png(byte[] data, SpriteFrameType type, int width, int height, Color[] palette = null,
|
public Png(byte[] data, SpriteFrameType type, int width, int height, Color[] palette = null,
|
||||||
Dictionary<string, string> embeddedData = null)
|
Dictionary<string, string> embeddedData = null)
|
||||||
@@ -256,14 +420,27 @@ namespace OpenRA.FileFormats
|
|||||||
// Convert to big endian
|
// Convert to big endian
|
||||||
Data = new byte[data.Length];
|
Data = new byte[data.Length];
|
||||||
var stride = PixelStride;
|
var stride = PixelStride;
|
||||||
for (var i = 0; i < width * height; i++)
|
var elements = width * height * stride;
|
||||||
{
|
var src = data.AsSpan();
|
||||||
Data[stride * i] = data[stride * i + 2];
|
var dst = Data.AsSpan();
|
||||||
Data[stride * i + 1] = data[stride * i + 1];
|
|
||||||
Data[stride * i + 2] = data[stride * i + 0];
|
|
||||||
|
|
||||||
if (type == SpriteFrameType.Bgra32)
|
if (type == SpriteFrameType.Bgra32)
|
||||||
Data[stride * i + 3] = data[stride * i + 3];
|
{
|
||||||
|
for (var i = 0; i < elements; i += 4)
|
||||||
|
{
|
||||||
|
dst[i + 0] = src[i + 2];
|
||||||
|
dst[i + 1] = src[i + 1];
|
||||||
|
dst[i + 2] = src[i + 0];
|
||||||
|
dst[i + 3] = src[i + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var i = 0; i < elements; i += 3)
|
||||||
|
{
|
||||||
|
dst[i + 0] = src[i + 2];
|
||||||
|
dst[i + 1] = src[i + 1];
|
||||||
|
dst[i + 2] = src[i + 0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -280,7 +457,8 @@ namespace OpenRA.FileFormats
|
|||||||
public static bool Verify(Stream s)
|
public static bool Verify(Stream s)
|
||||||
{
|
{
|
||||||
var pos = s.Position;
|
var pos = s.Position;
|
||||||
var isPng = Signature.Aggregate(true, (current, t) => current && s.ReadUInt8() == t);
|
Span<byte> sigBuffer = stackalloc byte[8];
|
||||||
|
var isPng = s.Read(sigBuffer) == 8 && sigBuffer.SequenceEqual(Signature);
|
||||||
s.Position = pos;
|
s.Position = pos;
|
||||||
return isPng;
|
return isPng;
|
||||||
}
|
}
|
||||||
@@ -303,24 +481,29 @@ namespace OpenRA.FileFormats
|
|||||||
throw new InvalidDataException("Unknown pixel format");
|
throw new InvalidDataException("Unknown pixel format");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void WritePngChunk(Stream output, string type, Stream input)
|
static void WritePngChunk(MemoryStream output, uint type, MemoryStream input)
|
||||||
{
|
{
|
||||||
input.Position = 0;
|
Span<byte> header = stackalloc byte[8];
|
||||||
|
BinaryPrimitives.WriteInt32BigEndian(header[..4], (int)input.Length);
|
||||||
|
BinaryPrimitives.WriteUInt32BigEndian(header[4..], type);
|
||||||
|
|
||||||
var typeBytes = Encoding.ASCII.GetBytes(type);
|
if (!input.TryGetBuffer(out var dataSegment))
|
||||||
output.Write(IPAddress.HostToNetworkOrder((int)input.Length));
|
dataSegment = new ArraySegment<byte>(input.ToArray());
|
||||||
output.Write(typeBytes);
|
|
||||||
|
|
||||||
var data = input.ReadAllBytes();
|
ReadOnlySpan<byte> data = dataSegment.AsSpan(0, (int)input.Length);
|
||||||
|
|
||||||
|
output.Write(header);
|
||||||
output.Write(data);
|
output.Write(data);
|
||||||
|
|
||||||
var crc32 = new Crc32();
|
var crc = 0xFFFFFFFF;
|
||||||
crc32.Update(typeBytes);
|
crc = CRC32.Update(crc, header[4..]);
|
||||||
crc32.Update(data);
|
crc = CRC32.Update(crc, data);
|
||||||
output.Write(IPAddress.NetworkToHostOrder((int)crc32.Value));
|
var finalCrc = CRC32.Finish(crc);
|
||||||
|
|
||||||
|
output.Write(IPAddress.NetworkToHostOrder((int)finalCrc));
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] Save()
|
public byte[] Save(CompressionLevel compression = CompressionLevel.SmallestSize)
|
||||||
{
|
{
|
||||||
using (var output = new MemoryStream())
|
using (var output = new MemoryStream())
|
||||||
{
|
{
|
||||||
@@ -339,7 +522,7 @@ namespace OpenRA.FileFormats
|
|||||||
header.WriteByte(0); // Filter
|
header.WriteByte(0); // Filter
|
||||||
header.WriteByte(0); // Interlacing
|
header.WriteByte(0); // Interlacing
|
||||||
|
|
||||||
WritePngChunk(output, "IHDR", header);
|
WritePngChunk(output, ChunkIHDR, header);
|
||||||
}
|
}
|
||||||
|
|
||||||
var alphaPalette = false;
|
var alphaPalette = false;
|
||||||
@@ -355,7 +538,7 @@ namespace OpenRA.FileFormats
|
|||||||
alphaPalette |= c.A > 0;
|
alphaPalette |= c.A > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
WritePngChunk(output, "PLTE", palette);
|
WritePngChunk(output, ChunkPLTE, palette);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,13 +549,13 @@ namespace OpenRA.FileFormats
|
|||||||
foreach (var c in Palette)
|
foreach (var c in Palette)
|
||||||
alpha.WriteByte(c.A);
|
alpha.WriteByte(c.A);
|
||||||
|
|
||||||
WritePngChunk(output, "tRNS", alpha);
|
WritePngChunk(output, ChunkTRNS, alpha);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var data = new MemoryStream())
|
using (var data = new MemoryStream())
|
||||||
{
|
{
|
||||||
using (var compressed = new DeflaterOutputStream(data, new Deflater(Deflater.BEST_COMPRESSION)))
|
using (var compressed = new ZLibStream(data, compression, true))
|
||||||
{
|
{
|
||||||
var rowStride = Width * PixelStride;
|
var rowStride = Width * PixelStride;
|
||||||
for (var y = 0; y < Height; y++)
|
for (var y = 0; y < Height; y++)
|
||||||
@@ -382,12 +565,9 @@ namespace OpenRA.FileFormats
|
|||||||
compressed.WriteByte(FilterType);
|
compressed.WriteByte(FilterType);
|
||||||
compressed.Write(Data, y * rowStride, rowStride);
|
compressed.Write(Data, y * rowStride, rowStride);
|
||||||
}
|
}
|
||||||
|
|
||||||
compressed.Flush();
|
|
||||||
compressed.Finish();
|
|
||||||
|
|
||||||
WritePngChunk(output, "IDAT", data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WritePngChunk(output, ChunkIDAT, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var kv in EmbeddedData)
|
foreach (var kv in EmbeddedData)
|
||||||
@@ -395,18 +575,18 @@ namespace OpenRA.FileFormats
|
|||||||
using (var text = new MemoryStream())
|
using (var text = new MemoryStream())
|
||||||
{
|
{
|
||||||
text.Write(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
|
text.Write(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
|
||||||
WritePngChunk(output, "tEXt", text);
|
WritePngChunk(output, ChunkTEXT, text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WritePngChunk(output, "IEND", new MemoryStream());
|
WritePngChunk(output, ChunkIEND, new MemoryStream());
|
||||||
return output.ToArray();
|
return output.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Save(string path)
|
public void Save(string path, CompressionLevel compression = CompressionLevel.SmallestSize)
|
||||||
{
|
{
|
||||||
File.WriteAllBytes(path, Save());
|
File.WriteAllBytes(path, Save(compression));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ namespace OpenRA.FileFormats
|
|||||||
|
|
||||||
public ReplayMetadata(GameInformation info)
|
public ReplayMetadata(GameInformation info)
|
||||||
{
|
{
|
||||||
if (info == null)
|
ArgumentNullException.ThrowIfNull(info);
|
||||||
throw new ArgumentNullException(nameof(info));
|
|
||||||
|
|
||||||
GameInfo = info;
|
GameInfo = info;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,16 +29,16 @@ namespace OpenRA.FileSystem
|
|||||||
public class FileSystem : IReadOnlyFileSystem
|
public class FileSystem : IReadOnlyFileSystem
|
||||||
{
|
{
|
||||||
public IEnumerable<IReadOnlyPackage> MountedPackages => mountedPackages.Keys;
|
public IEnumerable<IReadOnlyPackage> MountedPackages => mountedPackages.Keys;
|
||||||
readonly Dictionary<IReadOnlyPackage, int> mountedPackages = new();
|
readonly Dictionary<IReadOnlyPackage, int> mountedPackages = [];
|
||||||
readonly Dictionary<string, IReadOnlyPackage> explicitMounts = new();
|
readonly Dictionary<string, IReadOnlyPackage> explicitMounts = [];
|
||||||
readonly string modID;
|
readonly string modID;
|
||||||
|
|
||||||
// Mod packages that should not be disposed
|
// Mod packages that should not be disposed
|
||||||
readonly List<IReadOnlyPackage> modPackages = new();
|
readonly List<IReadOnlyPackage> modPackages = [];
|
||||||
readonly IReadOnlyDictionary<string, Manifest> installedMods;
|
readonly IReadOnlyDictionary<string, Manifest> installedMods;
|
||||||
readonly IPackageLoader[] packageLoaders;
|
readonly IPackageLoader[] packageLoaders;
|
||||||
|
|
||||||
Cache<string, List<IReadOnlyPackage>> fileIndex = new(_ => new List<IReadOnlyPackage>());
|
Cache<string, List<IReadOnlyPackage>> fileIndex = new(_ => []);
|
||||||
|
|
||||||
public FileSystem(string modID, IReadOnlyDictionary<string, Manifest> installedMods, IPackageLoader[] packageLoaders)
|
public FileSystem(string modID, IReadOnlyDictionary<string, Manifest> installedMods, IPackageLoader[] packageLoaders)
|
||||||
{
|
{
|
||||||
@@ -178,7 +178,7 @@ namespace OpenRA.FileSystem
|
|||||||
explicitMounts.Clear();
|
explicitMounts.Clear();
|
||||||
modPackages.Clear();
|
modPackages.Clear();
|
||||||
|
|
||||||
fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
|
fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => []);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void TrimExcess()
|
public void TrimExcess()
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace OpenRA.FileSystem
|
|||||||
return Directory.GetFiles(Name, "*", SearchOption.TopDirectoryOnly)
|
return Directory.GetFiles(Name, "*", SearchOption.TopDirectoryOnly)
|
||||||
.Concat(Directory.GetDirectories(Name))
|
.Concat(Directory.GetDirectories(Name))
|
||||||
.Select(Path.GetFileName)
|
.Select(Path.GetFileName)
|
||||||
.OrderBy(f => f);
|
.Order();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,11 +98,11 @@ namespace OpenRA.FileSystem
|
|||||||
{
|
{
|
||||||
readonly MemoryStream pkgStream = new();
|
readonly MemoryStream pkgStream = new();
|
||||||
|
|
||||||
public ReadWriteZipFile(string filename, bool create = false)
|
public ReadWriteZipFile(string filename = null, bool create = false)
|
||||||
{
|
{
|
||||||
// SharpZipLib breaks when asked to update archives loaded from outside streams or files
|
// SharpZipLib breaks when asked to update archives loaded from outside streams or files
|
||||||
// We can work around this by creating a clean in-memory-only file, cutting all outside references
|
// We can work around this by creating a clean in-memory-only file, cutting all outside references
|
||||||
if (!create)
|
if (!string.IsNullOrEmpty(filename) && !create)
|
||||||
{
|
{
|
||||||
using (var copy = new MemoryStream(File.ReadAllBytes(filename)))
|
using (var copy = new MemoryStream(File.ReadAllBytes(filename)))
|
||||||
{
|
{
|
||||||
@@ -120,8 +120,26 @@ namespace OpenRA.FileSystem
|
|||||||
entry.ExtraData = null;
|
entry.ExtraData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ReadWriteZipFile(byte[] data)
|
||||||
|
{
|
||||||
|
using (var copy = new MemoryStream(data))
|
||||||
|
{
|
||||||
|
pkgStream.Capacity = (int)copy.Length;
|
||||||
|
copy.CopyTo(pkgStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgStream.Position = 0;
|
||||||
|
pkg = new ZipFile(pkgStream);
|
||||||
|
Name = null;
|
||||||
|
|
||||||
|
// Remove subfields that can break ZIP updating.
|
||||||
|
foreach (ZipEntry entry in pkg)
|
||||||
|
entry.ExtraData = null;
|
||||||
|
}
|
||||||
|
|
||||||
void Commit()
|
void Commit()
|
||||||
{
|
{
|
||||||
|
if (!string.IsNullOrEmpty(Name))
|
||||||
File.WriteAllBytes(Name, pkgStream.ToArray());
|
File.WriteAllBytes(Name, pkgStream.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Linguini.Bundle;
|
using Linguini.Bundle;
|
||||||
@@ -48,23 +49,58 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public sealed class IncludeStaticFluentReferencesAttribute : Attribute
|
||||||
|
{
|
||||||
|
public readonly Type[] Types;
|
||||||
|
|
||||||
|
public IncludeStaticFluentReferencesAttribute(params Type[] types)
|
||||||
|
{
|
||||||
|
Types = types;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public sealed class IncludeChromeLogicArgsFluentReferencesAttribute : Attribute
|
||||||
|
{
|
||||||
|
public readonly string[] MethodNames;
|
||||||
|
|
||||||
|
public IncludeChromeLogicArgsFluentReferencesAttribute(params string[] methodNames)
|
||||||
|
{
|
||||||
|
MethodNames = methodNames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Field)]
|
||||||
|
public sealed class IncludeFluentReferencesAttribute : Attribute
|
||||||
|
{
|
||||||
|
public readonly LintDictionaryReference DictionaryReference;
|
||||||
|
|
||||||
|
public IncludeFluentReferencesAttribute() { }
|
||||||
|
|
||||||
|
public IncludeFluentReferencesAttribute(LintDictionaryReference dictionaryReference = LintDictionaryReference.None)
|
||||||
|
{
|
||||||
|
DictionaryReference = dictionaryReference;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class FluentBundle
|
public class FluentBundle
|
||||||
{
|
{
|
||||||
readonly Linguini.Bundle.FluentBundle bundle;
|
readonly Linguini.Bundle.FluentBundle bundle;
|
||||||
|
|
||||||
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem)
|
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem)
|
||||||
: this(culture, paths, fileSystem, error => Log.Write("debug", error.Message)) { }
|
: this(culture, paths, fileSystem, error => Log.Write("debug", error.Message)) { }
|
||||||
|
|
||||||
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, string text)
|
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem, string text)
|
||||||
: this(culture, paths, fileSystem, text, error => Log.Write("debug", error.Message)) { }
|
: this(culture, paths, fileSystem, text, error => Log.Write("debug", error.Message)) { }
|
||||||
|
|
||||||
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, Action<ParseError> onError)
|
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem, Action<ParseError> onError)
|
||||||
: this(culture, paths, fileSystem, null, onError) { }
|
: this(culture, paths, fileSystem, null, onError) { }
|
||||||
|
|
||||||
public FluentBundle(string culture, string text, Action<ParseError> onError)
|
public FluentBundle(string culture, string text, Action<ParseError> onError)
|
||||||
: this(culture, null, null, text, onError) { }
|
: this(culture, default, null, text, onError) { }
|
||||||
|
|
||||||
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, string text, Action<ParseError> onError)
|
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem, string text, Action<ParseError> onError)
|
||||||
{
|
{
|
||||||
bundle = LinguiniBuilder.Builder()
|
bundle = LinguiniBuilder.Builder()
|
||||||
.CultureInfo(new CultureInfo(culture))
|
.CultureInfo(new CultureInfo(culture))
|
||||||
@@ -111,8 +147,7 @@ namespace OpenRA
|
|||||||
|
|
||||||
public bool TryGetMessage(string key, out string value, object[] args = null)
|
public bool TryGetMessage(string key, out string value, object[] args = null)
|
||||||
{
|
{
|
||||||
if (key == null)
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
throw new ArgumentNullException(nameof(key));
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -129,7 +164,7 @@ namespace OpenRA
|
|||||||
throw new ArgumentException("Expected a comma separated list of name, value arguments " +
|
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));
|
"but the number of arguments is not a multiple of two", nameof(args));
|
||||||
|
|
||||||
fluentArgs = new Dictionary<string, IFluentType>();
|
fluentArgs = [];
|
||||||
for (var i = 0; i < args.Length; i += 2)
|
for (var i = 0; i < args.Length; i += 2)
|
||||||
{
|
{
|
||||||
var argKey = args[i] as string;
|
var argKey = args[i] as string;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using OpenRA.FileSystem;
|
using OpenRA.FileSystem;
|
||||||
|
|
||||||
@@ -22,16 +23,16 @@ namespace OpenRA
|
|||||||
static FluentBundle modFluentBundle;
|
static FluentBundle modFluentBundle;
|
||||||
static FluentBundle mapFluentBundle;
|
static FluentBundle mapFluentBundle;
|
||||||
|
|
||||||
public static void Initialize(ModData modData, IReadOnlyFileSystem fileSystem)
|
public static void Initialize(Manifest manifest, IReadOnlyFileSystem fileSystem)
|
||||||
{
|
{
|
||||||
lock (SyncObject)
|
lock (SyncObject)
|
||||||
{
|
{
|
||||||
modFluentBundle = new FluentBundle(modData.Manifest.FluentCulture, modData.Manifest.FluentMessages, fileSystem);
|
modFluentBundle = new FluentBundle(manifest.FluentCulture, manifest.FluentMessages, fileSystem);
|
||||||
if (fileSystem is Map map && map.FluentMessageDefinitions != null)
|
if (fileSystem is Map map && map.FluentMessageDefinitions != null)
|
||||||
{
|
{
|
||||||
var files = Array.Empty<string>();
|
var files = ImmutableArray<string>.Empty;
|
||||||
if (map.FluentMessageDefinitions.Value != null)
|
if (map.FluentMessageDefinitions.Value != null)
|
||||||
files = FieldLoader.GetValue<string[]>("value", map.FluentMessageDefinitions.Value);
|
files = FieldLoader.GetValue<ImmutableArray<string>>("value", map.FluentMessageDefinitions.Value);
|
||||||
|
|
||||||
string text = null;
|
string text = null;
|
||||||
if (map.FluentMessageDefinitions.Nodes.Length > 0)
|
if (map.FluentMessageDefinitions.Nodes.Length > 0)
|
||||||
@@ -44,7 +45,7 @@ namespace OpenRA
|
|||||||
text = builder.ToString();
|
text = builder.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
mapFluentBundle = new FluentBundle(modData.Manifest.FluentCulture, files, fileSystem, text);
|
mapFluentBundle = new FluentBundle(manifest.FluentCulture, files, fileSystem, text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
using System.Collections.Frozen;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace OpenRA
|
namespace OpenRA
|
||||||
@@ -23,15 +24,15 @@ namespace OpenRA
|
|||||||
public class Fonts : IGlobalModData
|
public class Fonts : IGlobalModData
|
||||||
{
|
{
|
||||||
[FieldLoader.LoadUsing(nameof(LoadFonts))]
|
[FieldLoader.LoadUsing(nameof(LoadFonts))]
|
||||||
public readonly Dictionary<string, FontData> FontList;
|
public readonly FrozenDictionary<string, FontData> FontList;
|
||||||
|
|
||||||
static object LoadFonts(MiniYaml y)
|
static object LoadFonts(MiniYaml y)
|
||||||
{
|
{
|
||||||
var ret = new Dictionary<string, FontData>();
|
var ret = new Dictionary<string, FontData>(y.Nodes.Length);
|
||||||
foreach (var node in y.Nodes)
|
foreach (var node in y.Nodes)
|
||||||
ret.Add(node.Key, FieldLoader.Load<FontData>(node.Value));
|
ret.Add(node.Key, FieldLoader.Load<FontData>(node.Value));
|
||||||
|
|
||||||
return ret;
|
return ret.ToFrozenDictionary();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -27,6 +28,7 @@ using OpenRA.Widgets;
|
|||||||
|
|
||||||
namespace OpenRA
|
namespace OpenRA
|
||||||
{
|
{
|
||||||
|
[IncludeStaticFluentReferences(typeof(Server.Server), typeof(Player), typeof(UnitOrders), typeof(OrderManager))]
|
||||||
public static class Game
|
public static class Game
|
||||||
{
|
{
|
||||||
[FluentReference("filename")]
|
[FluentReference("filename")]
|
||||||
@@ -166,6 +168,7 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
return ModData.WidgetLoader.LoadWidget(new WidgetArgs(args)
|
return ModData.WidgetLoader.LoadWidget(new WidgetArgs(args)
|
||||||
{
|
{
|
||||||
|
{ "modData", ModData },
|
||||||
{ "world", world },
|
{ "world", world },
|
||||||
{ "orderManager", OrderManager },
|
{ "orderManager", OrderManager },
|
||||||
{ "worldRenderer", worldRenderer },
|
{ "worldRenderer", worldRenderer },
|
||||||
@@ -181,7 +184,16 @@ namespace OpenRA
|
|||||||
|
|
||||||
public static event Action BeforeGameStart = () => { };
|
public static event Action BeforeGameStart = () => { };
|
||||||
public static event Action AfterGameStart = () => { };
|
public static event Action AfterGameStart = () => { };
|
||||||
internal static void StartGame(string mapUID, WorldType type)
|
internal static void StartGame(string uid, WorldType type)
|
||||||
|
{
|
||||||
|
var preview = ModData.MapCache[uid];
|
||||||
|
if (preview.Status != MapStatus.Available)
|
||||||
|
throw new InvalidDataException($"Invalid map uid: {uid}");
|
||||||
|
|
||||||
|
StartGame(preview.ToMap(), type);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void StartGame(Map map, WorldType type)
|
||||||
{
|
{
|
||||||
// Dispose of the old world before creating a new one.
|
// Dispose of the old world before creating a new one.
|
||||||
worldRenderer?.Dispose();
|
worldRenderer?.Dispose();
|
||||||
@@ -190,7 +202,23 @@ namespace OpenRA
|
|||||||
BeforeGameStart();
|
BeforeGameStart();
|
||||||
|
|
||||||
using (new PerfTimer("NewWorld"))
|
using (new PerfTimer("NewWorld"))
|
||||||
OrderManager.World = new World(mapUID, ModData, OrderManager, type);
|
{
|
||||||
|
ModData.PrepareMap(map);
|
||||||
|
|
||||||
|
// The depth buffer needs to be initialized with enough range to cover:
|
||||||
|
// - the height of the screen
|
||||||
|
// - the z-offset of tiles from MaxTerrainHeight below the bottom of the screen (pushed into view)
|
||||||
|
// - additional z-offset from actors on top of MaxTerrainHeight terrain
|
||||||
|
// - a small margin so that tiles rendered partially above the top edge of the screen aren't pushed behind the clip plane
|
||||||
|
// We need an offset of mapGrid.MaximumTerrainHeight * mapGrid.TileSize.Height / 2 to cover the terrain height
|
||||||
|
// and choose to use mapGrid.MaximumTerrainHeight * mapGrid.TileSize.Height / 4 for each of the actor and top-edge cases
|
||||||
|
var margin = 0;
|
||||||
|
if (map.Grid.EnableDepthBuffer)
|
||||||
|
margin = map.Rules.TerrainInfo.TileSize.Height * map.Grid.MaximumTerrainHeight;
|
||||||
|
|
||||||
|
Renderer.SetDepthMargin(margin);
|
||||||
|
OrderManager.World = new World(map, ModData, OrderManager, type);
|
||||||
|
}
|
||||||
|
|
||||||
OrderManager.World.GameOver += FinishBenchmark;
|
OrderManager.World.GameOver += FinishBenchmark;
|
||||||
|
|
||||||
@@ -344,7 +372,7 @@ namespace OpenRA
|
|||||||
var explicitModPaths = Array.Empty<string>();
|
var explicitModPaths = Array.Empty<string>();
|
||||||
if (modID != null && (File.Exists(modID) || Directory.Exists(modID)))
|
if (modID != null && (File.Exists(modID) || Directory.Exists(modID)))
|
||||||
{
|
{
|
||||||
explicitModPaths = new[] { modID };
|
explicitModPaths = [modID];
|
||||||
modID = Path.GetFileNameWithoutExtension(modID);
|
modID = Path.GetFileNameWithoutExtension(modID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,6 +387,48 @@ namespace OpenRA
|
|||||||
Log.AddChannel("nat", "nat.log");
|
Log.AddChannel("nat", "nat.log");
|
||||||
Log.AddChannel("client", "client.log");
|
Log.AddChannel("client", "client.log");
|
||||||
|
|
||||||
|
Nat.Initialize();
|
||||||
|
|
||||||
|
var modSearchArg = args.GetValue("Engine.ModSearchPaths", null);
|
||||||
|
var modSearchPaths = modSearchArg != null ?
|
||||||
|
FieldLoader.GetValue<ImmutableArray<string>>("Engine.ModsPath", modSearchArg) :
|
||||||
|
[Path.Combine(Platform.EngineDir, "mods")];
|
||||||
|
|
||||||
|
Mods = new InstalledMods(modSearchPaths, explicitModPaths);
|
||||||
|
Console.WriteLine("Internal mods:");
|
||||||
|
foreach (var mod in Mods)
|
||||||
|
Console.WriteLine($"\t{mod.Key} ({mod.Value.Metadata.Version})");
|
||||||
|
|
||||||
|
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
|
||||||
|
|
||||||
|
ExternalMods = new ExternalMods();
|
||||||
|
|
||||||
|
if (modID == null)
|
||||||
|
throw new InvalidOperationException("Game.Mod argument missing.");
|
||||||
|
|
||||||
|
if (Mods.TryGetValue(modID, out var manifest))
|
||||||
|
{
|
||||||
|
var launchPath = args.GetValue("Engine.LaunchPath", null);
|
||||||
|
var launchArgs = new List<string>();
|
||||||
|
|
||||||
|
// Sanitize input from platform-specific launchers
|
||||||
|
// Process.Start requires paths to not be quoted, even if they contain spaces
|
||||||
|
if (launchPath != null && launchPath[0] == '"' && launchPath[^1] == '"')
|
||||||
|
launchPath = launchPath[1..^1];
|
||||||
|
|
||||||
|
// Metadata registration requires an explicit launch path
|
||||||
|
if (launchPath != null)
|
||||||
|
ExternalMods.Register(Mods[modID], launchPath, launchArgs, ModRegistration.User);
|
||||||
|
|
||||||
|
ExternalMods.ClearInvalidRegistrations(ModRegistration.User);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw new InvalidOperationException($"Unknown or invalid mod '{modID}'.");
|
||||||
|
|
||||||
|
Console.WriteLine("External mods:");
|
||||||
|
foreach (var mod in ExternalMods)
|
||||||
|
Console.WriteLine($"\t{mod.Key} ({mod.Value.Version})");
|
||||||
|
|
||||||
var platforms = new[] { Settings.Game.Platform, "Default", null };
|
var platforms = new[] { Settings.Game.Platform, "Default", null };
|
||||||
foreach (var p in platforms)
|
foreach (var p in platforms)
|
||||||
{
|
{
|
||||||
@@ -369,7 +439,7 @@ namespace OpenRA
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var platform = CreatePlatform(p);
|
var platform = CreatePlatform(p);
|
||||||
Renderer = new Renderer(platform, Settings.Graphics);
|
Renderer = new Renderer(platform, Settings.Graphics, manifest.RendererConstants.VertexBatchSize);
|
||||||
Sound = new Sound(platform, Settings.Sound);
|
Sound = new Sound(platform, Settings.Sound);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -385,67 +455,23 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Nat.Initialize();
|
InitializeMod(manifest, args);
|
||||||
|
|
||||||
var modSearchArg = args.GetValue("Engine.ModSearchPaths", null);
|
|
||||||
var modSearchPaths = modSearchArg != null ?
|
|
||||||
FieldLoader.GetValue<string[]>("Engine.ModsPath", modSearchArg) :
|
|
||||||
new[] { Path.Combine(Platform.EngineDir, "mods") };
|
|
||||||
|
|
||||||
Mods = new InstalledMods(modSearchPaths, explicitModPaths);
|
|
||||||
Console.WriteLine("Internal mods:");
|
|
||||||
foreach (var mod in Mods)
|
|
||||||
Console.WriteLine($"\t{mod.Key} ({mod.Value.Metadata.Version})");
|
|
||||||
|
|
||||||
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
|
|
||||||
|
|
||||||
ExternalMods = new ExternalMods();
|
|
||||||
|
|
||||||
if (modID != null && Mods.TryGetValue(modID, out _))
|
|
||||||
{
|
|
||||||
var launchPath = args.GetValue("Engine.LaunchPath", null);
|
|
||||||
var launchArgs = new List<string>();
|
|
||||||
|
|
||||||
// Sanitize input from platform-specific launchers
|
|
||||||
// Process.Start requires paths to not be quoted, even if they contain spaces
|
|
||||||
if (launchPath != null && launchPath[0] == '"' && launchPath.Last() == '"')
|
|
||||||
launchPath = launchPath[1..^1];
|
|
||||||
|
|
||||||
// Metadata registration requires an explicit launch path
|
|
||||||
if (launchPath != null)
|
|
||||||
ExternalMods.Register(Mods[modID], launchPath, launchArgs, ModRegistration.User);
|
|
||||||
|
|
||||||
ExternalMods.ClearInvalidRegistrations(ModRegistration.User);
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine("External mods:");
|
|
||||||
foreach (var mod in ExternalMods)
|
|
||||||
Console.WriteLine($"\t{mod.Key} ({mod.Value.Version})");
|
|
||||||
|
|
||||||
InitializeMod(modID, args);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IPlatform CreatePlatform(string platformName)
|
public static IPlatform CreatePlatform(string platformName)
|
||||||
{
|
{
|
||||||
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + platformName + ".dll");
|
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + platformName + ".dll");
|
||||||
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
var loader = new AssemblyLoader(rendererPath);
|
var loader = new AssemblyLoader(rendererPath);
|
||||||
var platformType = loader.LoadDefaultAssembly().GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
|
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)
|
if (platformType == null)
|
||||||
throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation.");
|
throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation.");
|
||||||
|
|
||||||
return (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null);
|
return (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void InitializeMod(string mod, Arguments args)
|
public static void InitializeMod(Manifest manifest, Arguments args)
|
||||||
{
|
{
|
||||||
// Clear static state if we have switched mods
|
// Clear static state if we have switched mods
|
||||||
LobbyInfoChanged = () => { };
|
LobbyInfoChanged = () => { };
|
||||||
@@ -469,34 +495,25 @@ namespace OpenRA
|
|||||||
|
|
||||||
ModData = null;
|
ModData = null;
|
||||||
|
|
||||||
if (mod == null)
|
Console.WriteLine($"Loading mod: {manifest.Id}");
|
||||||
throw new InvalidOperationException("Game.Mod argument missing.");
|
|
||||||
|
|
||||||
if (!Mods.ContainsKey(mod))
|
|
||||||
throw new InvalidOperationException($"Unknown or invalid mod '{mod}'.");
|
|
||||||
|
|
||||||
Console.WriteLine($"Loading mod: {mod}");
|
|
||||||
|
|
||||||
Sound.StopVideo();
|
Sound.StopVideo();
|
||||||
|
|
||||||
ModData = new ModData(Mods[mod], Mods, true);
|
ModData = new ModData(manifest, Mods, true);
|
||||||
|
|
||||||
LocalPlayerProfile = new LocalPlayerProfile(Path.Combine(Platform.SupportDir, Settings.Game.AuthProfile), ModData.Manifest.Get<PlayerDatabase>());
|
LocalPlayerProfile = new LocalPlayerProfile(Path.Combine(Platform.SupportDir, Settings.Game.AuthProfile), ModData.GetOrCreate<PlayerDatabase>());
|
||||||
|
|
||||||
if (!ModData.LoadScreen.BeforeLoad())
|
if (!ModData.LoadScreen.BeforeLoad(ModData))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ModData.InitializeLoaders(ModData.DefaultFileSystem);
|
ModData.InitializeLoaders(ModData.DefaultFileSystem);
|
||||||
Renderer.InitializeFonts(ModData);
|
Renderer.InitializeFonts(ModData);
|
||||||
|
|
||||||
using (new PerfTimer("LoadMaps"))
|
using (new PerfTimer("LoadMaps"))
|
||||||
ModData.MapCache.LoadMaps();
|
ModData.MapCache.LoadMaps(ModData);
|
||||||
|
|
||||||
var grid = ModData.Manifest.Contains<MapGrid>() ? ModData.Manifest.Get<MapGrid>() : null;
|
|
||||||
Renderer.InitializeDepthBuffer(grid);
|
|
||||||
|
|
||||||
Cursor?.Dispose();
|
Cursor?.Dispose();
|
||||||
Cursor = new CursorManager(ModData.CursorProvider, ModData.Manifest.CursorSheetSize);
|
Cursor = new CursorManager(ModData);
|
||||||
|
|
||||||
var metadata = ModData.Manifest.Metadata;
|
var metadata = ModData.Manifest.Metadata;
|
||||||
if (!string.IsNullOrEmpty(metadata.WindowTitleTranslated))
|
if (!string.IsNullOrEmpty(metadata.WindowTitleTranslated))
|
||||||
@@ -514,10 +531,16 @@ namespace OpenRA
|
|||||||
ModData.LoadScreen.StartGame(args);
|
ModData.LoadScreen.StartGame(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void LoadEditor(string mapUid)
|
public static void LoadEditor(string uid)
|
||||||
{
|
{
|
||||||
JoinLocal();
|
JoinLocal();
|
||||||
StartGame(mapUid, WorldType.Editor);
|
StartGame(uid, WorldType.Editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LoadEditor(Map map)
|
||||||
|
{
|
||||||
|
JoinLocal();
|
||||||
|
StartGame(map, WorldType.Editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void LoadShellMap()
|
public static void LoadShellMap()
|
||||||
@@ -591,7 +614,7 @@ namespace OpenRA
|
|||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
|
|
||||||
var filename = TimestampedFilename(true);
|
var filename = TimestampedFilename(true);
|
||||||
var path = Path.Combine(directory, string.Concat(filename, ".png"));
|
var path = Path.Combine(directory, $"{filename}.png");
|
||||||
Log.Write("debug", "Taking screenshot " + path);
|
Log.Write("debug", "Taking screenshot " + path);
|
||||||
|
|
||||||
Renderer.SaveScreenshot(path);
|
Renderer.SaveScreenshot(path);
|
||||||
@@ -638,7 +661,7 @@ namespace OpenRA
|
|||||||
|
|
||||||
world.Tick();
|
world.Tick();
|
||||||
|
|
||||||
PerfHistory.Tick();
|
PerfHistory.Tick(!world.Paused);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait until we have done our first world Tick before TickRendering
|
// Wait until we have done our first world Tick before TickRendering
|
||||||
@@ -702,7 +725,7 @@ namespace OpenRA
|
|||||||
// Use worldRenderer.World instead of OrderManager.World to avoid a rendering mismatch while processing orders
|
// Use worldRenderer.World instead of OrderManager.World to avoid a rendering mismatch while processing orders
|
||||||
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
|
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
|
||||||
{
|
{
|
||||||
Renderer.BeginWorld(worldRenderer.Viewport.Rectangle);
|
Renderer.BeginWorld(worldRenderer.Viewport.CenterLocation, worldRenderer.Viewport.ViewportSize);
|
||||||
Sound.SetListenerPosition(worldRenderer.Viewport.CenterPosition);
|
Sound.SetListenerPosition(worldRenderer.Viewport.CenterPosition);
|
||||||
using (new PerfSample("render_world"))
|
using (new PerfSample("render_world"))
|
||||||
worldRenderer.Draw();
|
worldRenderer.Draw();
|
||||||
@@ -717,15 +740,12 @@ namespace OpenRA
|
|||||||
|
|
||||||
Ui.Draw();
|
Ui.Draw();
|
||||||
|
|
||||||
if (ModData != null && ModData.CursorProvider != null)
|
|
||||||
{
|
|
||||||
if (HideCursor)
|
if (HideCursor)
|
||||||
Cursor.SetCursor(null);
|
Cursor?.SetCursor(null);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Cursor.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default");
|
Cursor?.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default");
|
||||||
Cursor.Render(Renderer);
|
Cursor?.Render(Renderer);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,12 +759,13 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PerfHistory.Items["render"].Tick();
|
var isActive = !(worldRenderer?.World.Paused ?? true);
|
||||||
PerfHistory.Items["batches"].Tick();
|
PerfHistory.Items["render"].Tick(isActive);
|
||||||
PerfHistory.Items["render_world"].Tick();
|
PerfHistory.Items["batches"].Tick(isActive);
|
||||||
PerfHistory.Items["render_widgets"].Tick();
|
PerfHistory.Items["render_world"].Tick(isActive);
|
||||||
PerfHistory.Items["render_flip"].Tick();
|
PerfHistory.Items["render_widgets"].Tick(isActive);
|
||||||
PerfHistory.Items["terrain_lighting"].Tick();
|
PerfHistory.Items["render_flip"].Tick(isActive);
|
||||||
|
PerfHistory.Items["terrain_lighting"].Tick(isActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Loop()
|
static void Loop()
|
||||||
@@ -841,8 +862,13 @@ namespace OpenRA
|
|||||||
|
|
||||||
var haveSomeTimeUntilNextLogic = now < nextLogic;
|
var haveSomeTimeUntilNextLogic = now < nextLogic;
|
||||||
var isTimeToRender = now >= nextRender;
|
var isTimeToRender = now >= nextRender;
|
||||||
if (!Renderer.WindowIsSuspended && ((isTimeToRender && haveSomeTimeUntilNextLogic) || forceRender))
|
if (!Renderer.WindowIsSuspended)
|
||||||
{
|
{
|
||||||
|
if (isTimeToRender || forceRender)
|
||||||
|
{
|
||||||
|
if (haveSomeTimeUntilNextLogic || forceRender)
|
||||||
|
RenderTick();
|
||||||
|
|
||||||
nextRender = now + renderInterval;
|
nextRender = now + renderInterval;
|
||||||
|
|
||||||
// Pick the minimum allowed FPS (the lower between 'minReplayFPS'
|
// Pick the minimum allowed FPS (the lower between 'minReplayFPS'
|
||||||
@@ -853,12 +879,13 @@ namespace OpenRA
|
|||||||
var maxRenderInterval = Math.Max(1000 / MinReplayFps, renderInterval);
|
var maxRenderInterval = Math.Max(1000 / MinReplayFps, renderInterval);
|
||||||
forcedNextRender = now + maxRenderInterval;
|
forcedNextRender = now + maxRenderInterval;
|
||||||
|
|
||||||
RenderTick();
|
|
||||||
renderBeforeNextTick = false;
|
renderBeforeNextTick = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// Simulate a render tick if it was time to render but we skip actually rendering
|
// Simulate a render tick if it was time to render but we skip actually rendering
|
||||||
if (Renderer.WindowIsSuspended && isTimeToRender)
|
if (isTimeToRender || forceRender)
|
||||||
{
|
{
|
||||||
// Make sure that nextUpdate is set to a proper minimum interval
|
// Make sure that nextUpdate is set to a proper minimum interval
|
||||||
nextRender = now + renderInterval;
|
nextRender = now + renderInterval;
|
||||||
@@ -869,6 +896,12 @@ namespace OpenRA
|
|||||||
// Ensure that we still logic tick despite not rendering
|
// Ensure that we still logic tick despite not rendering
|
||||||
renderBeforeNextTick = false;
|
renderBeforeNextTick = false;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Avoid busy wait.
|
||||||
|
Thread.Sleep((int)(nextRender - now));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
Thread.Sleep((int)(nextUpdate - now));
|
Thread.Sleep((int)(nextUpdate - now));
|
||||||
@@ -947,7 +980,8 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
Name = "Skirmish Game",
|
Name = "Skirmish Game",
|
||||||
Map = map,
|
Map = map,
|
||||||
AdvertiseOnline = false
|
AdvertiseOnline = false,
|
||||||
|
AdvertiseOnLocalNetwork = !isSkirmish
|
||||||
};
|
};
|
||||||
|
|
||||||
// Always connect to local games using the same loopback connection
|
// Always connect to local games using the same loopback connection
|
||||||
@@ -985,7 +1019,7 @@ namespace OpenRA
|
|||||||
Order.Command($"state {Session.ClientState.Ready}")
|
Order.Command($"state {Session.ClientState.Ready}")
|
||||||
};
|
};
|
||||||
|
|
||||||
var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.PackageName) == launchMap);
|
var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.Path) == launchMap);
|
||||||
if (map == null)
|
if (map == null)
|
||||||
throw new ArgumentException($"Could not find map '{launchMap}'.");
|
throw new ArgumentException($"Could not find map '{launchMap}'.");
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenRA.Network;
|
using OpenRA.Network;
|
||||||
@@ -39,17 +40,32 @@ namespace OpenRA
|
|||||||
public TimeSpan Duration => EndTimeUtc > StartTimeUtc ? EndTimeUtc - StartTimeUtc : TimeSpan.Zero;
|
public TimeSpan Duration => EndTimeUtc > StartTimeUtc ? EndTimeUtc - StartTimeUtc : TimeSpan.Zero;
|
||||||
|
|
||||||
public IList<Player> Players { get; }
|
public IList<Player> Players { get; }
|
||||||
public HashSet<int> DisabledSpawnPoints = new();
|
public FrozenSet<int> DisabledSpawnPoints = FrozenSet<int>.Empty;
|
||||||
public MapPreview MapPreview => Game.ModData.MapCache[MapUid];
|
|
||||||
public IEnumerable<Player> HumanPlayers { get { return Players.Where(p => p.IsHuman); } }
|
public IEnumerable<Player> HumanPlayers { get { return Players.Where(p => p.IsHuman); } }
|
||||||
public bool IsSinglePlayer => HumanPlayers.Count() == 1;
|
public bool IsSinglePlayer => HumanPlayers.Count() == 1;
|
||||||
|
|
||||||
|
[FieldLoader.Ignore]
|
||||||
|
public MapGenerationArgs MapGenerationArgs;
|
||||||
|
|
||||||
readonly Dictionary<OpenRA.Player, Player> playersByRuntime;
|
readonly Dictionary<OpenRA.Player, Player> playersByRuntime;
|
||||||
|
|
||||||
public GameInformation()
|
public GameInformation()
|
||||||
{
|
{
|
||||||
Players = new List<Player>();
|
Players = [];
|
||||||
playersByRuntime = new Dictionary<OpenRA.Player, Player>();
|
playersByRuntime = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public MapPreview GetMapPreview(ModData modData)
|
||||||
|
{
|
||||||
|
var preview = modData.MapCache[MapUid];
|
||||||
|
if (preview.Status != MapStatus.Available && MapGenerationArgs != null)
|
||||||
|
{
|
||||||
|
preview.UpdateFromGenerationArgs(MapGenerationArgs);
|
||||||
|
preview.Generate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return preview;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GameInformation Deserialize(string data, string path)
|
public static GameInformation Deserialize(string data, string path)
|
||||||
@@ -72,6 +88,10 @@ namespace OpenRA
|
|||||||
case "Player":
|
case "Player":
|
||||||
info.Players.Add(FieldLoader.Load<Player>(node.Value));
|
info.Players.Add(FieldLoader.Load<Player>(node.Value));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "MapGenerationArgs":
|
||||||
|
info.MapGenerationArgs = FieldLoader.Load<MapGenerationArgs>(node.Value);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,17 +114,18 @@ namespace OpenRA
|
|||||||
for (var i = 0; i < Players.Count; i++)
|
for (var i = 0; i < Players.Count; i++)
|
||||||
nodes.Add(new MiniYamlNode($"Player@{i}", FieldSaver.Save(Players[i])));
|
nodes.Add(new MiniYamlNode($"Player@{i}", FieldSaver.Save(Players[i])));
|
||||||
|
|
||||||
|
if (MapGenerationArgs != null)
|
||||||
|
nodes.Add(new MiniYamlNode("MapGenerationArgs", new MiniYaml("", MapGenerationArgs.Serialize())));
|
||||||
|
|
||||||
return nodes.WriteToString();
|
return nodes.WriteToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Adds the player information at start-up.</summary>
|
/// <summary>Adds the player information at start-up.</summary>
|
||||||
public void AddPlayer(OpenRA.Player runtimePlayer, Session lobbyInfo)
|
public void AddPlayer(OpenRA.Player runtimePlayer, Session lobbyInfo)
|
||||||
{
|
{
|
||||||
if (runtimePlayer == null)
|
ArgumentNullException.ThrowIfNull(runtimePlayer);
|
||||||
throw new ArgumentNullException(nameof(runtimePlayer));
|
|
||||||
|
|
||||||
if (lobbyInfo == null)
|
ArgumentNullException.ThrowIfNull(lobbyInfo);
|
||||||
throw new ArgumentNullException(nameof(lobbyInfo));
|
|
||||||
|
|
||||||
// We don't care about spectators and map players
|
// We don't care about spectators and map players
|
||||||
if (runtimePlayer.NonCombatant || !runtimePlayer.Playable)
|
if (runtimePlayer.NonCombatant || !runtimePlayer.Playable)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ namespace OpenRA
|
|||||||
/// You can remove inherited traits by adding a - in front of them as in -TraitName: to inherit everything, but this trait.
|
/// You can remove inherited traits by adding a - in front of them as in -TraitName: to inherit everything, but this trait.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly string Name;
|
public readonly string Name;
|
||||||
readonly TypeDictionary traits = new();
|
readonly TypeDictionary traits = [];
|
||||||
TraitInfo[] constructOrderCache = null;
|
TraitInfo[] constructOrderCache = null;
|
||||||
|
|
||||||
public ActorInfo(ObjectCreator creator, string name, MiniYaml node)
|
public ActorInfo(ObjectCreator creator, string name, MiniYaml node)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using OpenRA.FileSystem;
|
using OpenRA.FileSystem;
|
||||||
@@ -268,7 +269,7 @@ namespace OpenRA
|
|||||||
|
|
||||||
if (mapRules.Value != null)
|
if (mapRules.Value != null)
|
||||||
{
|
{
|
||||||
var mapFiles = FieldLoader.GetValue<string[]>("value", mapRules.Value);
|
var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", mapRules.Value);
|
||||||
foreach (var f in mapFiles)
|
foreach (var f in mapFiles)
|
||||||
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
|
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -10,37 +10,38 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
namespace OpenRA.GameRules
|
namespace OpenRA.GameRules
|
||||||
{
|
{
|
||||||
public class SoundInfo
|
public class SoundInfo
|
||||||
{
|
{
|
||||||
public readonly Dictionary<string, string[]> Variants = new();
|
public readonly FrozenDictionary<string, ImmutableArray<string>> Variants = FrozenDictionary<string, ImmutableArray<string>>.Empty;
|
||||||
public readonly Dictionary<string, string[]> Prefixes = new();
|
public readonly FrozenDictionary<string, ImmutableArray<string>> Prefixes = FrozenDictionary<string, ImmutableArray<string>>.Empty;
|
||||||
public readonly Dictionary<string, string[]> Voices = new();
|
public readonly FrozenDictionary<string, ImmutableArray<string>> Voices = FrozenDictionary<string, ImmutableArray<string>>.Empty;
|
||||||
public readonly Dictionary<string, string[]> Notifications = new();
|
public readonly FrozenDictionary<string, ImmutableArray<string>> Notifications = FrozenDictionary<string, ImmutableArray<string>>.Empty;
|
||||||
public readonly string DefaultVariant = ".aud";
|
public readonly string DefaultVariant = ".aud";
|
||||||
public readonly string DefaultPrefix = "";
|
public readonly string DefaultPrefix = "";
|
||||||
public readonly HashSet<string> DisableVariants = new();
|
public readonly FrozenSet<string> DisableVariants = FrozenSet<string>.Empty;
|
||||||
public readonly HashSet<string> DisablePrefixes = new();
|
public readonly FrozenSet<string> DisablePrefixes = FrozenSet<string>.Empty;
|
||||||
|
|
||||||
public readonly Lazy<Dictionary<string, SoundPool>> VoicePools;
|
public readonly Lazy<FrozenDictionary<string, SoundPool>> VoicePools;
|
||||||
public readonly Lazy<Dictionary<string, SoundPool>> NotificationsPools;
|
public readonly Lazy<FrozenDictionary<string, SoundPool>> NotificationsPools;
|
||||||
|
|
||||||
public SoundInfo(MiniYaml y)
|
public SoundInfo(MiniYaml y)
|
||||||
{
|
{
|
||||||
FieldLoader.Load(this, y);
|
FieldLoader.Load(this, y);
|
||||||
|
|
||||||
VoicePools = Exts.Lazy(() => Voices.ToDictionary(a => a.Key, a => new SoundPool(1f, SoundPool.DefaultInterruptType, a.Value)));
|
VoicePools = Exts.Lazy(() => Voices.ToFrozenDictionary(a => a.Key, a => new SoundPool(1f, SoundPool.DefaultInterruptType, a.Value)));
|
||||||
NotificationsPools = Exts.Lazy(() => ParseSoundPool(y, "Notifications"));
|
NotificationsPools = Exts.Lazy(() => ParseSoundPool(y, "Notifications"));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Dictionary<string, SoundPool> ParseSoundPool(MiniYaml y, string key)
|
static FrozenDictionary<string, SoundPool> ParseSoundPool(MiniYaml y, string key)
|
||||||
{
|
{
|
||||||
var ret = new Dictionary<string, SoundPool>();
|
|
||||||
var classifiction = y.NodeWithKey(key);
|
var classifiction = y.NodeWithKey(key);
|
||||||
|
var ret = new Dictionary<string, SoundPool>(classifiction.Value.Nodes.Length);
|
||||||
foreach (var t in classifiction.Value.Nodes)
|
foreach (var t in classifiction.Value.Nodes)
|
||||||
{
|
{
|
||||||
var volumeModifier = 1f;
|
var volumeModifier = 1f;
|
||||||
@@ -53,12 +54,12 @@ namespace OpenRA.GameRules
|
|||||||
if (interruptTypeNode != null)
|
if (interruptTypeNode != null)
|
||||||
interruptType = FieldLoader.GetValue<SoundPool.InterruptType>(interruptTypeNode.Key, interruptTypeNode.Value.Value);
|
interruptType = FieldLoader.GetValue<SoundPool.InterruptType>(interruptTypeNode.Key, interruptTypeNode.Value.Value);
|
||||||
|
|
||||||
var names = FieldLoader.GetValue<string[]>(t.Key, t.Value.Value);
|
var names = FieldLoader.GetValue<ImmutableArray<string>>(t.Key, t.Value.Value);
|
||||||
var sp = new SoundPool(volumeModifier, interruptType, names);
|
var sp = new SoundPool(volumeModifier, interruptType, names);
|
||||||
ret.Add(t.Key, sp);
|
ret.Add(t.Key, sp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret.ToFrozenDictionary();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,10 +69,10 @@ namespace OpenRA.GameRules
|
|||||||
public const InterruptType DefaultInterruptType = InterruptType.DoNotPlay;
|
public const InterruptType DefaultInterruptType = InterruptType.DoNotPlay;
|
||||||
public readonly float VolumeModifier;
|
public readonly float VolumeModifier;
|
||||||
public readonly InterruptType Type;
|
public readonly InterruptType Type;
|
||||||
readonly string[] clips;
|
readonly ImmutableArray<string> clips;
|
||||||
readonly List<string> liveclips = new();
|
readonly List<string> liveclips = [];
|
||||||
|
|
||||||
public SoundPool(float volumeModifier, InterruptType interruptType, params string[] clips)
|
public SoundPool(float volumeModifier, InterruptType interruptType, ImmutableArray<string> clips)
|
||||||
{
|
{
|
||||||
VolumeModifier = volumeModifier;
|
VolumeModifier = volumeModifier;
|
||||||
Type = interruptType;
|
Type = interruptType;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenRA.Effects;
|
using OpenRA.Effects;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
@@ -36,7 +37,7 @@ namespace OpenRA.GameRules
|
|||||||
public class WarheadArgs
|
public class WarheadArgs
|
||||||
{
|
{
|
||||||
public WeaponInfo Weapon;
|
public WeaponInfo Weapon;
|
||||||
public int[] DamageModifiers = Array.Empty<int>();
|
public int[] DamageModifiers = [];
|
||||||
public WPos? Source;
|
public WPos? Source;
|
||||||
public WRot ImpactOrientation;
|
public WRot ImpactOrientation;
|
||||||
public WPos ImpactPosition;
|
public WPos ImpactPosition;
|
||||||
@@ -82,13 +83,13 @@ namespace OpenRA.GameRules
|
|||||||
public readonly WVec FollowingBurstTargetOffset = WVec.Zero;
|
public readonly WVec FollowingBurstTargetOffset = WVec.Zero;
|
||||||
|
|
||||||
[Desc("The sound played each time the weapon is fired.")]
|
[Desc("The sound played each time the weapon is fired.")]
|
||||||
public readonly string[] Report = null;
|
public readonly ImmutableArray<string> Report = default;
|
||||||
|
|
||||||
[Desc("Sound played only on first burst in a salvo.")]
|
[Desc("Sound played only on first burst in a salvo.")]
|
||||||
public readonly string[] StartBurstReport = null;
|
public readonly ImmutableArray<string> StartBurstReport = default;
|
||||||
|
|
||||||
[Desc("The sound played when the weapon is reloaded.")]
|
[Desc("The sound played when the weapon is reloaded.")]
|
||||||
public readonly string[] AfterFireSound = null;
|
public readonly ImmutableArray<string> AfterFireSound = default;
|
||||||
|
|
||||||
[Desc("Delay in ticks to play reloading sound.")]
|
[Desc("Delay in ticks to play reloading sound.")]
|
||||||
public readonly int AfterFireSoundDelay = 0;
|
public readonly int AfterFireSoundDelay = 0;
|
||||||
@@ -116,7 +117,7 @@ namespace OpenRA.GameRules
|
|||||||
|
|
||||||
[Desc("Delay in ticks between firing shots from the same ammo magazine. If one entry, it will be used for all bursts.",
|
[Desc("Delay in ticks between firing shots from the same ammo magazine. If one entry, it will be used for all bursts.",
|
||||||
"If multiple entries, their number needs to match Burst - 1.")]
|
"If multiple entries, their number needs to match Burst - 1.")]
|
||||||
public readonly int[] BurstDelays = { 5 };
|
public readonly ImmutableArray<int> BurstDelays = [5];
|
||||||
|
|
||||||
[Desc("The minimum range the weapon can fire.")]
|
[Desc("The minimum range the weapon can fire.")]
|
||||||
public readonly WDist MinRange = WDist.Zero;
|
public readonly WDist MinRange = WDist.Zero;
|
||||||
@@ -128,7 +129,7 @@ namespace OpenRA.GameRules
|
|||||||
public readonly IProjectileInfo Projectile;
|
public readonly IProjectileInfo Projectile;
|
||||||
|
|
||||||
[FieldLoader.LoadUsing(nameof(LoadWarheads))]
|
[FieldLoader.LoadUsing(nameof(LoadWarheads))]
|
||||||
public readonly List<IWarhead> Warheads = new();
|
public readonly ImmutableArray<IWarhead> Warheads = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This constructor is used solely for documentation generation.
|
/// This constructor is used solely for documentation generation.
|
||||||
@@ -139,7 +140,7 @@ namespace OpenRA.GameRules
|
|||||||
{
|
{
|
||||||
// Resolve any weapon-level yaml inheritance or removals
|
// Resolve any weapon-level yaml inheritance or removals
|
||||||
// HACK: The "Defaults" sequence syntax prevents us from doing this generally during yaml parsing
|
// HACK: The "Defaults" sequence syntax prevents us from doing this generally during yaml parsing
|
||||||
content = content.WithNodes(MiniYaml.Merge(new IReadOnlyCollection<MiniYamlNode>[] { content.Nodes }));
|
content = content.WithNodes(MiniYaml.Merge([content.Nodes]));
|
||||||
FieldLoader.Load(this, content);
|
FieldLoader.Load(this, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +171,7 @@ namespace OpenRA.GameRules
|
|||||||
retList.Add(ret);
|
retList.Add(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
return retList;
|
return retList.ToImmutableArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsValidTarget(BitSet<TargetableType> targetTypes)
|
public bool IsValidTarget(BitSet<TargetableType> targetTypes)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using OpenRA.Traits;
|
||||||
|
|
||||||
namespace OpenRA
|
namespace OpenRA
|
||||||
{
|
{
|
||||||
@@ -31,6 +32,7 @@ namespace OpenRA
|
|||||||
[FieldLoader.Require]
|
[FieldLoader.Require]
|
||||||
public readonly string DefaultSpeed;
|
public readonly string DefaultSpeed;
|
||||||
|
|
||||||
|
[IncludeFluentReferences(LintDictionaryReference.Values)]
|
||||||
[FieldLoader.LoadUsing(nameof(LoadSpeeds))]
|
[FieldLoader.LoadUsing(nameof(LoadSpeeds))]
|
||||||
public readonly Dictionary<string, GameSpeed> Speeds;
|
public readonly Dictionary<string, GameSpeed> Speeds;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
using OpenRA.Support;
|
using OpenRA.Support;
|
||||||
@@ -22,6 +23,7 @@ namespace OpenRA.Graphics
|
|||||||
public string Name { get; private set; }
|
public string Name { get; private set; }
|
||||||
public bool IsDecoration { get; set; }
|
public bool IsDecoration { get; set; }
|
||||||
|
|
||||||
|
readonly Map map;
|
||||||
readonly SequenceSet sequences;
|
readonly SequenceSet sequences;
|
||||||
readonly Func<WAngle> facingFunc;
|
readonly Func<WAngle> facingFunc;
|
||||||
readonly Func<bool> paused;
|
readonly Func<bool> paused;
|
||||||
@@ -43,6 +45,7 @@ namespace OpenRA.Graphics
|
|||||||
|
|
||||||
public Animation(World world, string name, Func<WAngle> facingFunc, Func<bool> paused)
|
public Animation(World world, string name, Func<WAngle> facingFunc, Func<bool> paused)
|
||||||
{
|
{
|
||||||
|
map = world.Map;
|
||||||
sequences = world.Map.Sequences;
|
sequences = world.Map.Sequences;
|
||||||
Name = name.ToLowerInvariant();
|
Name = name.ToLowerInvariant();
|
||||||
this.facingFunc = facingFunc;
|
this.facingFunc = facingFunc;
|
||||||
@@ -65,14 +68,16 @@ namespace OpenRA.Graphics
|
|||||||
var shadow = CurrentSequence.GetShadow(CurrentFrame, facingFunc());
|
var shadow = CurrentSequence.GetShadow(CurrentFrame, facingFunc());
|
||||||
if (shadow != null)
|
if (shadow != null)
|
||||||
{
|
{
|
||||||
|
var height = map.DistanceAboveTerrain(pos).Length;
|
||||||
|
|
||||||
var shadowRenderable = new SpriteRenderable(
|
var shadowRenderable = new SpriteRenderable(
|
||||||
shadow, pos, offset, CurrentSequence.ShadowZOffset + zOffset, palette,
|
shadow, pos, offset - new WVec(0, 0, height), CurrentSequence.ShadowZOffset + zOffset + height, palette,
|
||||||
CurrentSequence.Scale, 1f, float3.Ones, tintModifiers,
|
CurrentSequence.Scale, 1f, float3.Ones, tintModifiers,
|
||||||
true, rotation);
|
true, rotation);
|
||||||
return new IRenderable[] { shadowRenderable, imageRenderable };
|
return [shadowRenderable, imageRenderable];
|
||||||
}
|
}
|
||||||
|
|
||||||
return new IRenderable[] { imageRenderable };
|
return [imageRenderable];
|
||||||
}
|
}
|
||||||
|
|
||||||
public IRenderable[] RenderUI(WorldRenderer wr, int2 pos, in WVec offset, int zOffset, PaletteReference palette, float scale = 1f, float rotation = 0f)
|
public IRenderable[] RenderUI(WorldRenderer wr, int2 pos, in WVec offset, int zOffset, PaletteReference palette, float scale = 1f, float rotation = 0f)
|
||||||
@@ -88,10 +93,10 @@ namespace OpenRA.Graphics
|
|||||||
{
|
{
|
||||||
var shadowPos = pos - new int2((int)(scale * shadow.Size.X / 2), (int)(scale * shadow.Size.Y / 2));
|
var shadowPos = pos - new int2((int)(scale * shadow.Size.X / 2), (int)(scale * shadow.Size.Y / 2));
|
||||||
var shadowRenderable = new UISpriteRenderable(shadow, WPos.Zero + offset, shadowPos, CurrentSequence.ShadowZOffset + zOffset, palette, scale, 1f, rotation);
|
var shadowRenderable = new UISpriteRenderable(shadow, WPos.Zero + offset, shadowPos, CurrentSequence.ShadowZOffset + zOffset, palette, scale, 1f, rotation);
|
||||||
return new IRenderable[] { shadowRenderable, imageRenderable };
|
return [shadowRenderable, imageRenderable];
|
||||||
}
|
}
|
||||||
|
|
||||||
return new IRenderable[] { imageRenderable };
|
return [imageRenderable];
|
||||||
}
|
}
|
||||||
|
|
||||||
public Rectangle ScreenBounds(WorldRenderer wr, WPos pos, in WVec offset)
|
public Rectangle ScreenBounds(WorldRenderer wr, WPos pos, in WVec offset)
|
||||||
@@ -246,9 +251,9 @@ namespace OpenRA.Graphics
|
|||||||
return sequences.GetSequence(Name, sequenceName);
|
return sequences.GetSequence(Name, sequenceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetRandomExistingSequence(string[] sequences, MersenneTwister random)
|
public string GetRandomExistingSequence(ImmutableArray<string> sequences, MersenneTwister random)
|
||||||
{
|
{
|
||||||
return sequences.Where(s => HasSequence(s)).RandomOrDefault(random);
|
return sequences.Where(HasSequence).RandomOrDefault(random);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenRA.FileSystem;
|
using OpenRA.FileSystem;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
@@ -47,9 +49,9 @@ namespace OpenRA.Graphics
|
|||||||
public readonly string Image2x = null;
|
public readonly string Image2x = null;
|
||||||
public readonly string Image3x = null;
|
public readonly string Image3x = null;
|
||||||
|
|
||||||
public readonly int[] PanelRegion = null;
|
public readonly ImmutableArray<int> PanelRegion = default;
|
||||||
public readonly PanelSides PanelSides = PanelSides.All;
|
public readonly PanelSides PanelSides = PanelSides.All;
|
||||||
public readonly Dictionary<string, Rectangle> Regions = new();
|
public readonly FrozenDictionary<string, Rectangle> Regions = FrozenDictionary<string, Rectangle>.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IReadOnlyDictionary<string, Collection> Collections => collections;
|
public static IReadOnlyDictionary<string, Collection> Collections => collections;
|
||||||
@@ -71,11 +73,11 @@ namespace OpenRA.Graphics
|
|||||||
dpiScale = Game.Renderer.WindowScale;
|
dpiScale = Game.Renderer.WindowScale;
|
||||||
|
|
||||||
fileSystem = modData.DefaultFileSystem;
|
fileSystem = modData.DefaultFileSystem;
|
||||||
collections = new Dictionary<string, Collection>();
|
collections = [];
|
||||||
cachedSheets = new Dictionary<string, (Sheet, int)>();
|
cachedSheets = [];
|
||||||
cachedSprites = new Dictionary<string, Dictionary<string, Sprite>>();
|
cachedSprites = [];
|
||||||
cachedPanelSprites = new Dictionary<string, Sprite[]>();
|
cachedPanelSprites = [];
|
||||||
cachedCollectionSheets = new Dictionary<Collection, (Sheet, int)>();
|
cachedCollectionSheets = [];
|
||||||
|
|
||||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||||
var chrome = MiniYaml.Merge(modData.Manifest.Chrome
|
var chrome = MiniYaml.Merge(modData.Manifest.Chrome
|
||||||
@@ -170,7 +172,7 @@ namespace OpenRA.Graphics
|
|||||||
var sheetDensity = SheetForCollection(collection);
|
var sheetDensity = SheetForCollection(collection);
|
||||||
if (cachedCollection == null)
|
if (cachedCollection == null)
|
||||||
{
|
{
|
||||||
cachedCollection = new Dictionary<string, Sprite>();
|
cachedCollection = [];
|
||||||
cachedSprites.Add(collectionName, cachedCollection);
|
cachedSprites.Add(collectionName, cachedCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,11 +242,11 @@ namespace OpenRA.Graphics
|
|||||||
// PERF: We don't need to search for images if there are no definitions.
|
// 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.
|
// PERF: It's more efficient to send an empty array rather than an array of 9 nulls.
|
||||||
if (collection.Regions.Count == 0)
|
if (collection.Regions.Count == 0)
|
||||||
return Array.Empty<Sprite>();
|
return [];
|
||||||
|
|
||||||
// Support manual definitions for unusual dialog layouts
|
// Support manual definitions for unusual dialog layouts
|
||||||
sprites = new[]
|
sprites =
|
||||||
{
|
[
|
||||||
TryGetImage(collectionName, "corner-tl"),
|
TryGetImage(collectionName, "corner-tl"),
|
||||||
TryGetImage(collectionName, "border-t"),
|
TryGetImage(collectionName, "border-t"),
|
||||||
TryGetImage(collectionName, "corner-tr"),
|
TryGetImage(collectionName, "corner-tr"),
|
||||||
@@ -254,7 +256,7 @@ namespace OpenRA.Graphics
|
|||||||
TryGetImage(collectionName, "corner-bl"),
|
TryGetImage(collectionName, "corner-bl"),
|
||||||
TryGetImage(collectionName, "border-b"),
|
TryGetImage(collectionName, "border-b"),
|
||||||
TryGetImage(collectionName, "corner-br")
|
TryGetImage(collectionName, "corner-br")
|
||||||
};
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedPanelSprites.Add(collectionName, sprites);
|
cachedPanelSprites.Add(collectionName, sprites);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
|
using OpenRA.Traits;
|
||||||
|
|
||||||
namespace OpenRA.Graphics
|
namespace OpenRA.Graphics
|
||||||
{
|
{
|
||||||
@@ -29,8 +30,8 @@ namespace OpenRA.Graphics
|
|||||||
public IHardwareCursor[] Cursors;
|
public IHardwareCursor[] Cursors;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly Dictionary<string, Cursor> cursors = new();
|
readonly Dictionary<string, Cursor> cursors = [];
|
||||||
readonly SheetBuilder sheetBuilder;
|
public readonly SheetBuilder SheetBuilder;
|
||||||
readonly GraphicSettings graphicSettings;
|
readonly GraphicSettings graphicSettings;
|
||||||
|
|
||||||
Cursor cursor;
|
Cursor cursor;
|
||||||
@@ -39,19 +40,35 @@ namespace OpenRA.Graphics
|
|||||||
readonly bool hardwareCursorsDisabled = false;
|
readonly bool hardwareCursorsDisabled = false;
|
||||||
bool hardwareCursorsDoubled = false;
|
bool hardwareCursorsDoubled = false;
|
||||||
|
|
||||||
public CursorManager(CursorProvider cursorProvider, int cursorSheetSize)
|
public CursorManager(ModData modData)
|
||||||
{
|
{
|
||||||
hardwareCursorsDisabled = Game.Settings.Graphics.DisableHardwareCursors;
|
|
||||||
|
|
||||||
graphicSettings = Game.Settings.Graphics;
|
graphicSettings = Game.Settings.Graphics;
|
||||||
sheetBuilder = new SheetBuilder(SheetType.BGRA, cursorSheetSize);
|
hardwareCursorsDisabled = graphicSettings.DisableHardwareCursors;
|
||||||
|
SheetBuilder = new SheetBuilder(SheetType.BGRA, modData.Manifest.RendererConstants.CursorSheetSize);
|
||||||
|
|
||||||
|
// Overwrite previous definitions if there are duplicates
|
||||||
|
var pals = new Dictionary<string, IProvidesCursorPaletteInfo>();
|
||||||
|
foreach (var p in modData.DefaultRules.Actors[SystemActors.World].TraitInfos<IProvidesCursorPaletteInfo>())
|
||||||
|
if (p.Palette != null)
|
||||||
|
pals[p.Palette] = p;
|
||||||
|
|
||||||
|
var paletteCache = new Cache<string, ImmutablePalette>(p => pals[p].ReadPalette(modData.DefaultFileSystem));
|
||||||
|
var frameCache = new FrameCache(modData.DefaultFileSystem, modData.SpriteLoaders);
|
||||||
|
|
||||||
// Sort the cursors for better packing onto the sheet.
|
// Sort the cursors for better packing onto the sheet.
|
||||||
foreach (var kv in cursorProvider.Cursors
|
foreach (var kv in modData.Cursors)
|
||||||
.OrderBy(kvp => kvp.Value.Frames.Max(f => f.Size.Height)))
|
|
||||||
{
|
{
|
||||||
var frames = kv.Value.Frames;
|
var cursorSprites = frameCache[kv.Value.Src];
|
||||||
var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? cursorProvider.Palettes[kv.Value.Palette] : null;
|
var length = kv.Value.Length ?? cursorSprites.Length - kv.Value.Start;
|
||||||
|
|
||||||
|
if (kv.Value.Start > cursorSprites.Length)
|
||||||
|
throw new YamlException($"Cursor {kv.Value.Name}: {nameof(kv.Value.Start)} is greater than the length of the sprite sequence.");
|
||||||
|
|
||||||
|
if (kv.Value.Length > cursorSprites.Length)
|
||||||
|
throw new YamlException($"Cursor {kv.Value.Name}: {nameof(kv.Value.Length)} is greater than the length of the sprite sequence.");
|
||||||
|
|
||||||
|
var frames = cursorSprites.Skip(kv.Value.Start).Take(length).ToArray();
|
||||||
|
var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? paletteCache[kv.Value.Palette] : null;
|
||||||
|
|
||||||
var c = new Cursor
|
var c = new Cursor
|
||||||
{
|
{
|
||||||
@@ -82,7 +99,7 @@ namespace OpenRA.Graphics
|
|||||||
type = SpriteFrameType.Bgra32;
|
type = SpriteFrameType.Bgra32;
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Sprites[c.Length++] = sheetBuilder.Add(data, type, f.Size, 0, hotspot);
|
c.Sprites[c.Length++] = SheetBuilder.Add(data, type, f.Size, 0, hotspot);
|
||||||
|
|
||||||
// Bounds relative to the hotspot
|
// Bounds relative to the hotspot
|
||||||
c.Bounds = Rectangle.Union(c.Bounds, new Rectangle(hotspot, f.Size));
|
c.Bounds = Rectangle.Union(c.Bounds, new Rectangle(hotspot, f.Size));
|
||||||
@@ -94,9 +111,13 @@ namespace OpenRA.Graphics
|
|||||||
cursors.Add(kv.Key, c);
|
cursors.Add(kv.Key, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow the utility to create a cursor manager.
|
||||||
|
if (Game.Renderer != null)
|
||||||
|
{
|
||||||
CreateOrUpdateHardwareCursors();
|
CreateOrUpdateHardwareCursors();
|
||||||
Update();
|
Update();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CreateOrUpdateHardwareCursors()
|
void CreateOrUpdateHardwareCursors()
|
||||||
{
|
{
|
||||||
@@ -128,7 +149,7 @@ namespace OpenRA.Graphics
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sheetBuilder.Current.ReleaseBuffer();
|
SheetBuilder.Current?.ReleaseBuffer();
|
||||||
|
|
||||||
hardwareCursorsDoubled = graphicSettings.CursorDouble;
|
hardwareCursorsDoubled = graphicSettings.CursorDouble;
|
||||||
}
|
}
|
||||||
@@ -233,7 +254,7 @@ namespace OpenRA.Graphics
|
|||||||
var height = frame.Size.Height;
|
var height = frame.Size.Height;
|
||||||
|
|
||||||
if (width == 0 || height == 0)
|
if (width == 0 || height == 0)
|
||||||
return Array.Empty<byte>();
|
return [];
|
||||||
|
|
||||||
var data = new byte[4 * width * height];
|
var data = new byte[4 * width * height];
|
||||||
unsafe
|
unsafe
|
||||||
@@ -279,21 +300,18 @@ namespace OpenRA.Graphics
|
|||||||
{
|
{
|
||||||
for (var i = 0; i < c.Cursors.Length; i++)
|
for (var i = 0; i < c.Cursors.Length; i++)
|
||||||
{
|
{
|
||||||
if (c.Cursors[i] != null)
|
c.Cursors[i]?.Dispose();
|
||||||
{
|
|
||||||
c.Cursors[i].Dispose();
|
|
||||||
c.Cursors[i] = null;
|
c.Cursors[i] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
ClearHardwareCursors();
|
ClearHardwareCursors();
|
||||||
|
|
||||||
cursors.Clear();
|
cursors.Clear();
|
||||||
sheetBuilder.Dispose();
|
SheetBuilder.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
#region Copyright & License Information
|
|
||||||
/*
|
|
||||||
* Copyright (c) The OpenRA Developers and Contributors
|
|
||||||
* This file is part of OpenRA, which is free software. It is made
|
|
||||||
* available to you under the terms of the GNU General Public License
|
|
||||||
* as published by the Free Software Foundation, either version 3 of
|
|
||||||
* the License, or (at your option) any later version. For more
|
|
||||||
* information, see COPYING.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using OpenRA.Traits;
|
|
||||||
|
|
||||||
namespace OpenRA.Graphics
|
|
||||||
{
|
|
||||||
public sealed class CursorProvider
|
|
||||||
{
|
|
||||||
public readonly IReadOnlyDictionary<string, CursorSequence> Cursors;
|
|
||||||
public readonly IReadOnlyDictionary<string, ImmutablePalette> Palettes;
|
|
||||||
|
|
||||||
public CursorProvider(ModData modData)
|
|
||||||
{
|
|
||||||
var fileSystem = modData.DefaultFileSystem;
|
|
||||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
|
||||||
var sequenceYaml = MiniYaml.Merge(modData.Manifest.Cursors.Select(
|
|
||||||
s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool)));
|
|
||||||
|
|
||||||
var cursorsYaml = new MiniYaml(null, sequenceYaml).NodeWithKey("Cursors").Value;
|
|
||||||
|
|
||||||
// Overwrite previous definitions if there are duplicates
|
|
||||||
var pals = new Dictionary<string, IProvidesCursorPaletteInfo>();
|
|
||||||
foreach (var p in modData.DefaultRules.Actors[SystemActors.World].TraitInfos<IProvidesCursorPaletteInfo>())
|
|
||||||
if (p.Palette != null)
|
|
||||||
pals[p.Palette] = p;
|
|
||||||
|
|
||||||
Palettes = 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 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));
|
|
||||||
|
|
||||||
Cursors = cursors;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasCursorSequence(string cursor)
|
|
||||||
{
|
|
||||||
return Cursors.ContainsKey(cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CursorSequence GetCursorSequence(string cursor)
|
|
||||||
{
|
|
||||||
try { return Cursors[cursor]; }
|
|
||||||
catch (KeyNotFoundException)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Cursor does not have a sequence `{cursor}`");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,50 +9,26 @@
|
|||||||
*/
|
*/
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace OpenRA.Graphics
|
namespace OpenRA.Graphics
|
||||||
{
|
{
|
||||||
public class CursorSequence
|
public class CursorSequence
|
||||||
{
|
{
|
||||||
public readonly string Name;
|
public readonly string Name;
|
||||||
|
public readonly string Src;
|
||||||
public readonly int Start;
|
public readonly int Start;
|
||||||
public readonly int Length;
|
public readonly int? Length;
|
||||||
public readonly string Palette;
|
public readonly string Palette;
|
||||||
public readonly int2 Hotspot;
|
public readonly int2 Hotspot;
|
||||||
|
|
||||||
public readonly ISpriteFrame[] Frames;
|
public CursorSequence(string name, string cursorSrc, string palette, MiniYaml info)
|
||||||
|
|
||||||
public CursorSequence(FrameCache cache, string name, string cursorSrc, string palette, MiniYaml info)
|
|
||||||
{
|
{
|
||||||
var d = info.ToDictionary();
|
var d = info.ToDictionary();
|
||||||
|
|
||||||
Start = Exts.ParseInt32Invariant(d["Start"].Value);
|
|
||||||
Palette = palette;
|
Palette = palette;
|
||||||
Name = name;
|
Name = name;
|
||||||
|
Src = cursorSrc;
|
||||||
|
|
||||||
var cursorSprites = cache[cursorSrc];
|
if (d.TryGetValue("X", out var yaml))
|
||||||
Frames = cursorSprites.Skip(Start).ToArray();
|
|
||||||
|
|
||||||
if ((d.TryGetValue("Length", out var yaml) && yaml.Value == "*") ||
|
|
||||||
(d.TryGetValue("End", out yaml) && yaml.Value == "*"))
|
|
||||||
Length = Frames.Length;
|
|
||||||
else if (d.TryGetValue("Length", out yaml))
|
|
||||||
Length = Exts.ParseInt32Invariant(yaml.Value);
|
|
||||||
else if (d.TryGetValue("End", out yaml))
|
|
||||||
Length = Exts.ParseInt32Invariant(yaml.Value) - Start;
|
|
||||||
else
|
|
||||||
Length = 1;
|
|
||||||
|
|
||||||
Frames = Frames.Take(Length).ToArray();
|
|
||||||
|
|
||||||
if (Start > cursorSprites.Length)
|
|
||||||
throw new YamlException($"Cursor {name}: {nameof(Start)} is greater than the length of the sprite sequence.");
|
|
||||||
|
|
||||||
if (Length > cursorSprites.Length)
|
|
||||||
throw new YamlException($"Cursor {name}: {nameof(Length)} is greater than the length of the sprite sequence.");
|
|
||||||
|
|
||||||
if (d.TryGetValue("X", out yaml))
|
|
||||||
{
|
{
|
||||||
Exts.TryParseInt32Invariant(yaml.Value, out var x);
|
Exts.TryParseInt32Invariant(yaml.Value, out var x);
|
||||||
Hotspot = Hotspot.WithX(x);
|
Hotspot = Hotspot.WithX(x);
|
||||||
@@ -63,6 +39,14 @@ namespace OpenRA.Graphics
|
|||||||
Exts.TryParseInt32Invariant(yaml.Value, out var y);
|
Exts.TryParseInt32Invariant(yaml.Value, out var y);
|
||||||
Hotspot = Hotspot.WithY(y);
|
Hotspot = Hotspot.WithY(y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Start = Exts.ParseInt32Invariant(d["Start"].Value);
|
||||||
|
if (d.TryGetValue("Length", out yaml))
|
||||||
|
Length = yaml.Value != "*" ? Exts.ParseInt32Invariant(yaml.Value) : null;
|
||||||
|
else if (d.TryGetValue("End", out yaml) && yaml.Value == "*")
|
||||||
|
Length = yaml.Value != "*" ? Exts.ParseInt32Invariant(yaml.Value) - Start : null;
|
||||||
|
else
|
||||||
|
Length = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ namespace OpenRA.Graphics
|
|||||||
public ITexture ColorShifts { get; }
|
public ITexture ColorShifts { get; }
|
||||||
|
|
||||||
public int Height { get; private set; }
|
public int Height { get; private set; }
|
||||||
readonly Dictionary<string, ImmutablePalette> palettes = new();
|
readonly Dictionary<string, ImmutablePalette> palettes = [];
|
||||||
readonly Dictionary<string, MutablePalette> mutablePalettes = new();
|
readonly Dictionary<string, MutablePalette> mutablePalettes = [];
|
||||||
readonly Dictionary<string, int> indices = new();
|
readonly Dictionary<string, int> indices = [];
|
||||||
byte[] buffer = Array.Empty<byte>();
|
byte[] buffer = [];
|
||||||
float[] colorShiftBuffer = Array.Empty<float>();
|
float[] colorShiftBuffer = [];
|
||||||
|
|
||||||
public HardwarePalette()
|
public HardwarePalette()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,25 +31,13 @@ namespace OpenRA.Graphics
|
|||||||
|
|
||||||
public interface IModelWidget
|
public interface IModelWidget
|
||||||
{
|
{
|
||||||
public string Palette { get; }
|
string Palette { get; }
|
||||||
public float Scale { get; }
|
float Scale { get; }
|
||||||
public void Setup(Func<bool> isVisible, Func<string> getPalette, Func<string> getPlayerPalette,
|
void Setup(Func<bool> isVisible, Func<string> getPalette, Func<string> getPlayerPalette,
|
||||||
Func<float> getScale, Func<IModel> getVoxel, Func<WRot> getRotation);
|
Func<float> getScale, Func<IModel> getVoxel, Func<WRot> getRotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly struct ModelRenderData
|
public readonly record struct ModelRenderData(int Start, int Count, Sheet Sheet);
|
||||||
{
|
|
||||||
public readonly int Start;
|
|
||||||
public readonly int Count;
|
|
||||||
public readonly Sheet Sheet;
|
|
||||||
|
|
||||||
public ModelRenderData(int start, int count, Sheet sheet)
|
|
||||||
{
|
|
||||||
Start = start;
|
|
||||||
Count = count;
|
|
||||||
Sheet = sheet;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IModelCacheInfo : ITraitInfoInterface { }
|
public interface IModelCacheInfo : ITraitInfoInterface { }
|
||||||
|
|
||||||
|
|||||||
@@ -43,11 +43,11 @@ namespace OpenRA.Graphics
|
|||||||
: base("model")
|
: base("model")
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public override ShaderVertexAttribute[] Attributes { get; } = new[]
|
public override ShaderVertexAttribute[] Attributes { get; } =
|
||||||
{
|
[
|
||||||
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
|
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
|
||||||
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
|
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
|
||||||
new ShaderVertexAttribute("aVertexTexMetadata", ShaderVertexAttributeType.Float, 2, 28),
|
new ShaderVertexAttribute("aVertexTexMetadata", ShaderVertexAttributeType.Float, 2, 28),
|
||||||
};
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
|
|
||||||
@@ -64,18 +65,18 @@ namespace OpenRA.Graphics
|
|||||||
Buffer.BlockCopy(colors, 0, destination, destinationOffset * 4, Palette.Size * 4);
|
Buffer.BlockCopy(colors, 0, destination, destinationOffset * 4, Palette.Size * 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImmutablePalette(string filename, int[] remapTransparent, int[] remap)
|
public ImmutablePalette(string filename, ImmutableArray<int> remapTransparent, ImmutableArray<int> remap)
|
||||||
{
|
{
|
||||||
using (var s = File.OpenRead(filename))
|
using (var s = File.OpenRead(filename))
|
||||||
LoadFromStream(s, remapTransparent, remap);
|
LoadFromStream(s, remapTransparent, remap);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImmutablePalette(Stream s, int[] remapTransparent, int[] remapShadow)
|
public ImmutablePalette(Stream s, ImmutableArray<int> remapTransparent, ImmutableArray<int> remapShadow)
|
||||||
{
|
{
|
||||||
LoadFromStream(s, remapTransparent, remapShadow);
|
LoadFromStream(s, remapTransparent, remapShadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoadFromStream(Stream s, int[] remapTransparent, int[] remapShadow)
|
void LoadFromStream(Stream s, ImmutableArray<int> remapTransparent, ImmutableArray<int> remapShadow)
|
||||||
{
|
{
|
||||||
using (var reader = new BinaryReader(s))
|
using (var reader = new BinaryReader(s))
|
||||||
for (var i = 0; i < Palette.Size; i++)
|
for (var i = 0; i < Palette.Size; i++)
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ namespace OpenRA
|
|||||||
void PumpInput(IInputHandler inputHandler);
|
void PumpInput(IInputHandler inputHandler);
|
||||||
string GetClipboardText();
|
string GetClipboardText();
|
||||||
bool SetClipboardText(string text);
|
bool SetClipboardText(string text);
|
||||||
|
bool TryOpenUrl(string url);
|
||||||
|
|
||||||
void GrabWindowMouseFocus();
|
void GrabWindowMouseFocus();
|
||||||
void ReleaseWindowMouseFocus();
|
void ReleaseWindowMouseFocus();
|
||||||
@@ -83,7 +84,8 @@ namespace OpenRA
|
|||||||
|
|
||||||
public interface IGraphicsContext : IDisposable
|
public interface IGraphicsContext : IDisposable
|
||||||
{
|
{
|
||||||
IVertexBuffer<T> CreateVertexBuffer<T>(int size) where T : struct;
|
IVertexBuffer<T> CreateEmptyVertexBuffer<T>(int size) where T : struct;
|
||||||
|
IVertexBuffer<T> CreateVertexBuffer<T>(T[] data, bool dynamic = true) where T : struct;
|
||||||
T[] CreateVertices<T>(int size) where T : struct;
|
T[] CreateVertices<T>(int size) where T : struct;
|
||||||
IIndexBuffer CreateIndexBuffer(uint[] indices);
|
IIndexBuffer CreateIndexBuffer(uint[] indices);
|
||||||
ITexture CreateTexture();
|
ITexture CreateTexture();
|
||||||
@@ -134,7 +136,7 @@ namespace OpenRA
|
|||||||
void SetVec(string name, float x);
|
void SetVec(string name, float x);
|
||||||
void SetVec(string name, float x, float y);
|
void SetVec(string name, float x, float y);
|
||||||
void SetVec(string name, float x, float y, float z);
|
void SetVec(string name, float x, float y, float z);
|
||||||
void SetVec(string name, float[] vec, int length);
|
void SetVec(string name, ReadOnlyMemory<float> vec, int length);
|
||||||
void SetTexture(string param, ITexture texture);
|
void SetTexture(string param, ITexture texture);
|
||||||
void SetMatrix(string param, float[] mtx);
|
void SetMatrix(string param, float[] mtx);
|
||||||
void PrepareRender();
|
void PrepareRender();
|
||||||
@@ -179,12 +181,6 @@ namespace OpenRA
|
|||||||
TriangleList,
|
TriangleList,
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly struct Range<T>
|
|
||||||
{
|
|
||||||
public readonly T Start, End;
|
|
||||||
public Range(T start, T end) { Start = start; End = end; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum WindowMode
|
public enum WindowMode
|
||||||
{
|
{
|
||||||
Windowed,
|
Windowed,
|
||||||
|
|||||||
@@ -10,19 +10,19 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Collections.Immutable;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
|
|
||||||
namespace OpenRA.Graphics
|
namespace OpenRA.Graphics
|
||||||
{
|
{
|
||||||
public class PlayerColorRemap : IPaletteRemap
|
public class PlayerColorRemap : IPaletteRemap
|
||||||
{
|
{
|
||||||
readonly int[] remapIndices;
|
readonly ImmutableArray<int> remapIndices;
|
||||||
readonly float hue;
|
readonly float hue;
|
||||||
readonly float saturation;
|
readonly float saturation;
|
||||||
readonly float value;
|
readonly float value;
|
||||||
|
|
||||||
public PlayerColorRemap(int[] remapIndices, Color color)
|
public PlayerColorRemap(ImmutableArray<int> remapIndices, Color color)
|
||||||
{
|
{
|
||||||
this.remapIndices = remapIndices;
|
this.remapIndices = remapIndices;
|
||||||
|
|
||||||
|
|||||||
@@ -14,39 +14,20 @@ using System.Runtime.InteropServices;
|
|||||||
namespace OpenRA.Graphics
|
namespace OpenRA.Graphics
|
||||||
{
|
{
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
public readonly struct RenderPostProcessPassVertex
|
public readonly record struct RenderPostProcessPassVertex(float X, float Y);
|
||||||
{
|
|
||||||
public readonly float X, Y;
|
|
||||||
|
|
||||||
public RenderPostProcessPassVertex(float x, float y)
|
|
||||||
{
|
|
||||||
X = x; Y = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
public readonly struct RenderPostProcessPassTexturedVertex
|
public readonly record struct RenderPostProcessPassTexturedVertex(float X, float Y, float S, float T);
|
||||||
{
|
|
||||||
// 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 sealed class RenderPostProcessPassShaderBindings : ShaderBindings
|
||||||
{
|
{
|
||||||
public RenderPostProcessPassShaderBindings(string name)
|
public RenderPostProcessPassShaderBindings(string name)
|
||||||
: base("postprocess", "postprocess_" + name) { }
|
: base("postprocess", "postprocess_" + name) { }
|
||||||
|
|
||||||
public override ShaderVertexAttribute[] Attributes { get; } = new[]
|
public override ShaderVertexAttribute[] Attributes { get; } =
|
||||||
{
|
[
|
||||||
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0)
|
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0)
|
||||||
};
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class RenderPostProcessPassTexturedShaderBindings : ShaderBindings
|
public sealed class RenderPostProcessPassTexturedShaderBindings : ShaderBindings
|
||||||
@@ -55,10 +36,10 @@ namespace OpenRA.Graphics
|
|||||||
: base("postprocess_textured", "postprocess_textured_" + name)
|
: base("postprocess_textured", "postprocess_textured_" + name)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public override ShaderVertexAttribute[] Attributes { get; } = new[]
|
public override ShaderVertexAttribute[] Attributes { get; } =
|
||||||
{
|
[
|
||||||
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0),
|
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0),
|
||||||
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 2, 8),
|
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 2, 8),
|
||||||
};
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ namespace OpenRA.Graphics
|
|||||||
{
|
{
|
||||||
var tr = new float3(br.X, tl.Y, tl.Z);
|
var tr = new float3(br.X, tl.Y, tl.Z);
|
||||||
var bl = new float3(tl.X, br.Y, br.Z);
|
var bl = new float3(tl.X, br.Y, br.Z);
|
||||||
DrawPolygon(new[] { tl, tr, br, bl }, width, color, blendMode);
|
DrawPolygon([tl, tr, br, bl], width, color, blendMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FillRect(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha)
|
public void FillRect(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha)
|
||||||
|
|||||||
@@ -37,23 +37,23 @@ namespace OpenRA.Graphics
|
|||||||
|
|
||||||
public interface ISpriteSequenceLoader
|
public interface ISpriteSequenceLoader
|
||||||
{
|
{
|
||||||
int BgraSheetSize { get; }
|
|
||||||
int IndexedSheetSize { get; }
|
|
||||||
IReadOnlyDictionary<string, ISpriteSequence> ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode node);
|
IReadOnlyDictionary<string, ISpriteSequence> ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode node);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class SequenceSet : IDisposable
|
public sealed class SequenceSet : IDisposable
|
||||||
{
|
{
|
||||||
|
public readonly string TileSet;
|
||||||
readonly ModData modData;
|
readonly ModData modData;
|
||||||
readonly string tileSet;
|
|
||||||
readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, ISpriteSequence>> images;
|
readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, ISpriteSequence>> images;
|
||||||
public SpriteCache SpriteCache { get; }
|
public SpriteCache SpriteCache { get; }
|
||||||
|
|
||||||
public SequenceSet(IReadOnlyFileSystem fileSystem, ModData modData, string tileSet, MiniYaml additionalSequences)
|
public SequenceSet(IReadOnlyFileSystem fileSystem, ModData modData, string tileSet, MiniYaml additionalSequences)
|
||||||
{
|
{
|
||||||
this.modData = modData;
|
this.modData = modData;
|
||||||
this.tileSet = tileSet;
|
TileSet = tileSet;
|
||||||
SpriteCache = new SpriteCache(fileSystem, modData.SpriteLoaders, modData.SpriteSequenceLoader.BgraSheetSize, modData.SpriteSequenceLoader.IndexedSheetSize);
|
|
||||||
|
var rc = modData.Manifest.RendererConstants;
|
||||||
|
SpriteCache = new SpriteCache(fileSystem, modData.SpriteLoaders, rc.SequenceBgraSheetSize, rc.SequenceIndexedSheetSize);
|
||||||
using (new Support.PerfTimer("LoadSequences"))
|
using (new Support.PerfTimer("LoadSequences"))
|
||||||
images = Load(fileSystem, additionalSequences);
|
images = Load(fileSystem, additionalSequences);
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ namespace OpenRA.Graphics
|
|||||||
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix))
|
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node);
|
images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, TileSet, SpriteCache, node);
|
||||||
}
|
}
|
||||||
|
|
||||||
return images;
|
return images;
|
||||||
|
|||||||
@@ -23,21 +23,7 @@ namespace OpenRA.Graphics
|
|||||||
UInt = 0x1405 // GL_UNSIGNED_INT
|
UInt = 0x1405 // GL_UNSIGNED_INT
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly struct ShaderVertexAttribute
|
public readonly record struct ShaderVertexAttribute(string Name, ShaderVertexAttributeType Type, int Components, int Offset);
|
||||||
{
|
|
||||||
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 abstract class ShaderBindings : IShaderBindings
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -149,6 +149,10 @@ namespace OpenRA.Graphics
|
|||||||
var buffer = data;
|
var buffer = data;
|
||||||
ReleaseBuffer();
|
ReleaseBuffer();
|
||||||
|
|
||||||
|
// We aren't committing data to the GPU, so let's not delete our data.
|
||||||
|
if (Game.Renderer == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
// Only transfer if the destination has no data that would be lost by overwriting.
|
// Only transfer if the destination has no data that would be lost by overwriting.
|
||||||
if (buffer != null && destination.data == null && destination.texture == null)
|
if (buffer != null && destination.data == null && destination.texture == null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ using OpenRA.Primitives;
|
|||||||
|
|
||||||
namespace OpenRA.Graphics
|
namespace OpenRA.Graphics
|
||||||
{
|
{
|
||||||
[Serializable]
|
|
||||||
public class SheetOverflowException : Exception
|
public class SheetOverflowException : Exception
|
||||||
{
|
{
|
||||||
public SheetOverflowException(string message)
|
public SheetOverflowException(string message)
|
||||||
@@ -34,7 +33,7 @@ namespace OpenRA.Graphics
|
|||||||
public sealed class SheetBuilder : IDisposable
|
public sealed class SheetBuilder : IDisposable
|
||||||
{
|
{
|
||||||
public readonly SheetType Type;
|
public readonly SheetType Type;
|
||||||
readonly List<Sheet> sheets = new();
|
readonly List<Sheet> sheets = [];
|
||||||
readonly Func<Sheet> allocateSheet;
|
readonly Func<Sheet> allocateSheet;
|
||||||
readonly int margin;
|
readonly int margin;
|
||||||
int rowHeight = 0;
|
int rowHeight = 0;
|
||||||
@@ -66,9 +65,6 @@ namespace OpenRA.Graphics
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public SheetBuilder(SheetType t)
|
|
||||||
: this(t, Game.Settings.Graphics.SheetSize) { }
|
|
||||||
|
|
||||||
public SheetBuilder(SheetType t, int sheetSize, int margin = 1)
|
public SheetBuilder(SheetType t, int sheetSize, int margin = 1)
|
||||||
: this(t, () => AllocateSheet(t, sheetSize), margin) { }
|
: this(t, () => AllocateSheet(t, sheetSize), margin) { }
|
||||||
|
|
||||||
@@ -76,8 +72,6 @@ namespace OpenRA.Graphics
|
|||||||
{
|
{
|
||||||
CurrentChannel = t == SheetType.Indexed ? TextureChannel.Red : TextureChannel.RGBA;
|
CurrentChannel = t == SheetType.Indexed ? TextureChannel.Red : TextureChannel.RGBA;
|
||||||
Type = t;
|
Type = t;
|
||||||
Current = allocateSheet();
|
|
||||||
sheets.Add(Current);
|
|
||||||
this.allocateSheet = allocateSheet;
|
this.allocateSheet = allocateSheet;
|
||||||
this.margin = margin;
|
this.margin = margin;
|
||||||
}
|
}
|
||||||
@@ -86,6 +80,12 @@ namespace OpenRA.Graphics
|
|||||||
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, 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)
|
public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset, bool premultiplied = false)
|
||||||
{
|
{
|
||||||
|
if (Current == null)
|
||||||
|
{
|
||||||
|
Current = allocateSheet();
|
||||||
|
sheets.Add(Current);
|
||||||
|
}
|
||||||
|
|
||||||
// Don't bother allocating empty sprites
|
// Don't bother allocating empty sprites
|
||||||
if (size.Width == 0 || size.Height == 0)
|
if (size.Width == 0 || size.Height == 0)
|
||||||
return new Sprite(Current, Rectangle.Empty, 0, spriteOffset, CurrentChannel, BlendMode.Alpha);
|
return new Sprite(Current, Rectangle.Empty, 0, spriteOffset, CurrentChannel, BlendMode.Alpha);
|
||||||
@@ -116,6 +116,12 @@ namespace OpenRA.Graphics
|
|||||||
public Sprite Allocate(Size imageSize, float scale = 1f) { return Allocate(imageSize, 0, float3.Zero, scale); }
|
public Sprite Allocate(Size imageSize, float scale = 1f) { return Allocate(imageSize, 0, float3.Zero, scale); }
|
||||||
public Sprite Allocate(Size imageSize, float zRamp, in float3 spriteOffset, float scale = 1f)
|
public Sprite Allocate(Size imageSize, float zRamp, in float3 spriteOffset, float scale = 1f)
|
||||||
{
|
{
|
||||||
|
if (Current == null)
|
||||||
|
{
|
||||||
|
Current = allocateSheet();
|
||||||
|
sheets.Add(Current);
|
||||||
|
}
|
||||||
|
|
||||||
if (imageSize.Width + p.X + margin > Current.Size.Width)
|
if (imageSize.Width + p.X + margin > Current.Size.Width)
|
||||||
{
|
{
|
||||||
p = new int2(0, p.Y + rowHeight + margin);
|
p = new int2(0, p.Y + rowHeight + margin);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenRA.FileSystem;
|
using OpenRA.FileSystem;
|
||||||
@@ -27,12 +28,12 @@ namespace OpenRA.Graphics
|
|||||||
|
|
||||||
readonly Dictionary<
|
readonly Dictionary<
|
||||||
int,
|
int,
|
||||||
(int[] Frames, MiniYamlNode.SourceLocation Location, AdjustFrame AdjustFrame, bool Premultiplied)> spriteReservations = new();
|
(ImmutableArray<int> Frames, MiniYamlNode.SourceLocation Location, AdjustFrame AdjustFrame, bool Premultiplied)> spriteReservations = [];
|
||||||
readonly Dictionary<string, List<int>> reservationsByFilename = new();
|
readonly Dictionary<string, List<int>> reservationsByFilename = [];
|
||||||
|
|
||||||
readonly Dictionary<int, Sprite[]> resolvedSprites = new();
|
readonly Dictionary<int, Sprite[]> resolvedSprites = [];
|
||||||
|
|
||||||
readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = new();
|
readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = [];
|
||||||
|
|
||||||
int nextReservationToken = 1;
|
int nextReservationToken = 1;
|
||||||
|
|
||||||
@@ -49,12 +50,12 @@ namespace OpenRA.Graphics
|
|||||||
this.loaders = loaders;
|
this.loaders = loaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location,
|
public int ReserveSprites(string filename, ImmutableArray<int> frames, MiniYamlNode.SourceLocation location,
|
||||||
AdjustFrame adjustFrame = null, bool premultiplied = false)
|
AdjustFrame adjustFrame = null, bool premultiplied = false)
|
||||||
{
|
{
|
||||||
var token = nextReservationToken++;
|
var token = nextReservationToken++;
|
||||||
spriteReservations[token] = (frames?.ToArray(), location, adjustFrame, premultiplied);
|
spriteReservations[token] = (frames, location, adjustFrame, premultiplied);
|
||||||
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
|
reservationsByFilename.GetOrAdd(filename, _ => []).Add(token);
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,8 +104,8 @@ namespace OpenRA.Graphics
|
|||||||
throw new InvalidOperationException($"{rs.Location}: {filename} does not contain frames: " +
|
throw new InvalidOperationException($"{rs.Location}: {filename} does not contain frames: " +
|
||||||
string.Join(',', rs.Frames.Where(f => f >= loadedFrames.Length)));
|
string.Join(',', rs.Frames.Where(f => f >= loadedFrames.Length)));
|
||||||
|
|
||||||
var frames = rs.Frames ?? Enumerable.Range(0, loadedFrames.Length);
|
var frames = rs.Frames != null ? rs.Frames : Enumerable.Range(0, loadedFrames.Length);
|
||||||
var total = rs.Frames?.Length ?? loadedFrames.Length;
|
var total = rs.Frames != null ? rs.Frames.Length : loadedFrames.Length;
|
||||||
|
|
||||||
var j = 0;
|
var j = 0;
|
||||||
foreach (var i in frames)
|
foreach (var i in frames)
|
||||||
@@ -155,7 +156,7 @@ namespace OpenRA.Graphics
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach (var sb in SheetBuilders.Values)
|
foreach (var sb in SheetBuilders.Values)
|
||||||
sb.Current.ReleaseBuffer();
|
sb.Current?.ReleaseBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Sprite[] ResolveSprites(int token)
|
public Sprite[] ResolveSprites(int token)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
*/
|
*/
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
|
|
||||||
@@ -17,7 +16,7 @@ namespace OpenRA.Graphics
|
|||||||
{
|
{
|
||||||
public class SpriteRenderable : IPalettedRenderable, IModifyableRenderable, IFinalizedRenderable
|
public class SpriteRenderable : IPalettedRenderable, IModifyableRenderable, IFinalizedRenderable
|
||||||
{
|
{
|
||||||
public static readonly IEnumerable<IRenderable> None = Array.Empty<IRenderable>();
|
public static readonly IEnumerable<IRenderable> None = [];
|
||||||
|
|
||||||
readonly Sprite sprite;
|
readonly Sprite sprite;
|
||||||
readonly WPos pos;
|
readonly WPos pos;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace OpenRA.Graphics
|
|||||||
public sealed class TerrainSpriteLayer : IDisposable
|
public sealed class TerrainSpriteLayer : IDisposable
|
||||||
{
|
{
|
||||||
// PERF: we can reuse the IndexBuffer as all layers have the same size.
|
// PERF: we can reuse the IndexBuffer as all layers have the same size.
|
||||||
static readonly ConditionalWeakTable<World, IndexBufferRc> IndexBuffers = new();
|
static readonly ConditionalWeakTable<World, IndexBufferRc> IndexBuffers = [];
|
||||||
readonly IndexBufferRc indexBufferWrapper;
|
readonly IndexBufferRc indexBufferWrapper;
|
||||||
|
|
||||||
public readonly BlendMode BlendMode;
|
public readonly BlendMode BlendMode;
|
||||||
@@ -30,7 +30,7 @@ namespace OpenRA.Graphics
|
|||||||
readonly IVertexBuffer<Vertex> vertexBuffer;
|
readonly IVertexBuffer<Vertex> vertexBuffer;
|
||||||
readonly Vertex[] vertices;
|
readonly Vertex[] vertices;
|
||||||
readonly bool[] ignoreTint;
|
readonly bool[] ignoreTint;
|
||||||
readonly HashSet<int> dirtyRows = new();
|
readonly HashSet<int> dirtyRows = [];
|
||||||
readonly int indexRowStride;
|
readonly int indexRowStride;
|
||||||
readonly int vertexRowStride;
|
readonly int vertexRowStride;
|
||||||
readonly bool restrictToBounds;
|
readonly bool restrictToBounds;
|
||||||
@@ -49,23 +49,23 @@ namespace OpenRA.Graphics
|
|||||||
BlendMode = blendMode;
|
BlendMode = blendMode;
|
||||||
map = world.Map;
|
map = world.Map;
|
||||||
|
|
||||||
vertexRowStride = 4 * map.MapSize.X;
|
vertexRowStride = 4 * map.MapSize.Width;
|
||||||
vertices = new Vertex[vertexRowStride * map.MapSize.Y];
|
vertices = new Vertex[vertexRowStride * map.MapSize.Height];
|
||||||
vertexBuffer = Game.Renderer.Context.CreateVertexBuffer<Vertex>(vertices.Length);
|
vertexBuffer = Game.Renderer.Context.CreateEmptyVertexBuffer<Vertex>(vertices.Length);
|
||||||
|
|
||||||
indexRowStride = 6 * map.MapSize.X;
|
indexRowStride = 6 * map.MapSize.Width;
|
||||||
lock (IndexBuffers)
|
lock (IndexBuffers)
|
||||||
{
|
{
|
||||||
indexBufferWrapper = IndexBuffers.GetValue(world, world => new IndexBufferRc(world));
|
indexBufferWrapper = IndexBuffers.GetValue(world, world => new IndexBufferRc(world));
|
||||||
indexBufferWrapper.AddRef();
|
indexBufferWrapper.AddRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
palettes = new PaletteReference[map.MapSize.X * map.MapSize.Y];
|
palettes = new PaletteReference[map.MapSize.Width * map.MapSize.Height];
|
||||||
wr.PaletteInvalidated += UpdatePaletteIndices;
|
wr.PaletteInvalidated += UpdatePaletteIndices;
|
||||||
|
|
||||||
if (wr.TerrainLighting != null)
|
if (wr.TerrainLighting != null)
|
||||||
{
|
{
|
||||||
ignoreTint = new bool[vertexRowStride * map.MapSize.Y];
|
ignoreTint = new bool[vertexRowStride * map.MapSize.Height];
|
||||||
wr.TerrainLighting.CellChanged += UpdateTint;
|
wr.TerrainLighting.CellChanged += UpdateTint;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ namespace OpenRA.Graphics
|
|||||||
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);
|
vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, c, v.R, v.G, v.B, v.A);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var row = 0; row < map.MapSize.Y; row++)
|
for (var row = 0; row < map.MapSize.Height; row++)
|
||||||
dirtyRows.Add(row);
|
dirtyRows.Add(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ namespace OpenRA.Graphics
|
|||||||
|
|
||||||
var offset = vertexRowStride * uv.V + 4 * 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);
|
Util.FastCreateQuad(vertices, pos, sprite, samplers, palette?.TextureIndex ?? 0, offset, scale * sprite.Size, alpha * float3.Ones, alpha);
|
||||||
palettes[uv.V * map.MapSize.X + uv.U] = palette;
|
palettes[uv.V * map.MapSize.Width + uv.U] = palette;
|
||||||
|
|
||||||
if (worldRenderer.TerrainLighting != null)
|
if (worldRenderer.TerrainLighting != null)
|
||||||
{
|
{
|
||||||
@@ -209,8 +209,8 @@ namespace OpenRA.Graphics
|
|||||||
var cells = restrictToBounds ? viewport.VisibleCellsInsideBounds : viewport.AllVisibleCells;
|
var cells = restrictToBounds ? viewport.VisibleCellsInsideBounds : viewport.AllVisibleCells;
|
||||||
|
|
||||||
// Only draw the rows that are visible.
|
// Only draw the rows that are visible.
|
||||||
var firstRow = cells.CandidateMapCoords.TopLeft.V.Clamp(0, map.MapSize.Y);
|
var firstRow = cells.CandidateMapCoords.TopLeft.V.Clamp(0, map.MapSize.Height);
|
||||||
var lastRow = (cells.CandidateMapCoords.BottomRight.V + 1).Clamp(firstRow, map.MapSize.Y);
|
var lastRow = (cells.CandidateMapCoords.BottomRight.V + 1).Clamp(firstRow, map.MapSize.Height);
|
||||||
|
|
||||||
Game.Renderer.Flush();
|
Game.Renderer.Flush();
|
||||||
|
|
||||||
@@ -250,7 +250,8 @@ namespace OpenRA.Graphics
|
|||||||
|
|
||||||
public IndexBufferRc(World world)
|
public IndexBufferRc(World world)
|
||||||
{
|
{
|
||||||
Buffer = Game.Renderer.Context.CreateIndexBuffer(Util.CreateQuadIndices(world.Map.MapSize.X * world.Map.MapSize.Y));
|
Buffer = Game.Renderer.Context.CreateIndexBuffer(
|
||||||
|
Util.CreateQuadIndices(world.Map.MapSize.Width * world.Map.MapSize.Height));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddRef() { count++; }
|
public void AddRef() { count++; }
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ namespace OpenRA.Graphics
|
|||||||
public static class Util
|
public static class Util
|
||||||
{
|
{
|
||||||
// yes, our channel order is nuts.
|
// yes, our channel order is nuts.
|
||||||
static readonly int[] ChannelMasks = { 2, 1, 0, 3 };
|
static readonly int[] ChannelMasks = [2, 1, 0, 3];
|
||||||
|
|
||||||
public static uint[] CreateQuadIndices(int quads)
|
public static uint[] CreateQuadIndices(int quads)
|
||||||
{
|
{
|
||||||
var indices = new uint[quads * 6];
|
var indices = new uint[quads * 6];
|
||||||
ReadOnlySpan<uint> cornerVertexMap = stackalloc uint[] { 0, 1, 2, 2, 3, 0 };
|
ReadOnlySpan<uint> cornerVertexMap = [0, 1, 2, 2, 3, 0];
|
||||||
for (var i = 0; i < indices.Length; i++)
|
for (var i = 0; i < indices.Length; i++)
|
||||||
indices[i] = cornerVertexMap[i % 6] + (uint)(4 * (i / 6));
|
indices[i] = cornerVertexMap[i % 6] + (uint)(4 * (i / 6));
|
||||||
|
|
||||||
@@ -277,13 +277,13 @@ namespace OpenRA.Graphics
|
|||||||
size.X * angleSin - size.Y * angleCos,
|
size.X * angleSin - size.Y * angleCos,
|
||||||
(size.X * angleSin - size.Y * angleCos) * size.Z / size.Y);
|
(size.X * angleSin - size.Y * angleCos) * size.Z / size.Y);
|
||||||
|
|
||||||
return new float3[]
|
return
|
||||||
{
|
[
|
||||||
center - ra,
|
center - ra,
|
||||||
center + rb,
|
center + rb,
|
||||||
center + ra,
|
center + ra,
|
||||||
center - rb
|
center - rb
|
||||||
};
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -53,12 +53,12 @@ namespace OpenRA.Graphics
|
|||||||
: base("combined")
|
: base("combined")
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public override ShaderVertexAttribute[] Attributes { get; } = new[]
|
public override ShaderVertexAttribute[] Attributes { get; } =
|
||||||
{
|
[
|
||||||
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
|
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
|
||||||
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
|
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
|
||||||
new ShaderVertexAttribute("aVertexAttributes", ShaderVertexAttributeType.UInt, 1, 28),
|
new ShaderVertexAttribute("aVertexAttributes", ShaderVertexAttributeType.UInt, 1, 28),
|
||||||
new ShaderVertexAttribute("aVertexTint", ShaderVertexAttributeType.Float, 4, 32)
|
new ShaderVertexAttribute("aVertexTint", ShaderVertexAttributeType.Float, 4, 32)
|
||||||
};
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
|
|
||||||
@@ -49,14 +50,13 @@ namespace OpenRA.Graphics
|
|||||||
readonly Size tileSize;
|
readonly Size tileSize;
|
||||||
|
|
||||||
// Viewport geometry (world-px)
|
// Viewport geometry (world-px)
|
||||||
public int2 CenterLocation { get; private set; }
|
public float2 CenterLocation { get; private set; }
|
||||||
|
|
||||||
public WPos CenterPosition => worldRenderer.ProjectedPosition(CenterLocation);
|
public WPos CenterPosition => worldRenderer.ProjectedPosition(CenterLocation.ToInt2());
|
||||||
|
|
||||||
public Rectangle Rectangle => new(TopLeft, new Size(viewportSize.X, viewportSize.Y));
|
public int2 TopLeft => CenterLocation.ToInt2() - ViewportSize.ToInt2() / 2;
|
||||||
public int2 TopLeft => CenterLocation - viewportSize / 2;
|
public int2 BottomRight => CenterLocation.ToInt2() + ViewportSize.ToInt2() / 2;
|
||||||
public int2 BottomRight => CenterLocation + viewportSize / 2;
|
public Size ViewportSize { get; private set; }
|
||||||
int2 viewportSize;
|
|
||||||
ProjectedCellRegion cells;
|
ProjectedCellRegion cells;
|
||||||
bool cellsDirty = true;
|
bool cellsDirty = true;
|
||||||
|
|
||||||
@@ -69,6 +69,10 @@ namespace OpenRA.Graphics
|
|||||||
bool unlockMinZoom;
|
bool unlockMinZoom;
|
||||||
float unlockedMinZoomScale;
|
float unlockedMinZoomScale;
|
||||||
float unlockedMinZoom = 1f;
|
float unlockedMinZoom = 1f;
|
||||||
|
float defaultScale;
|
||||||
|
bool overrideUserScale;
|
||||||
|
|
||||||
|
public Func<float2> ViewportCenterProvider;
|
||||||
|
|
||||||
public float Zoom
|
public float Zoom
|
||||||
{
|
{
|
||||||
@@ -77,7 +81,7 @@ namespace OpenRA.Graphics
|
|||||||
private set
|
private set
|
||||||
{
|
{
|
||||||
zoom = value;
|
zoom = value;
|
||||||
viewportSize = (1f / zoom * new float2(Game.Renderer.NativeResolution)).ToInt2();
|
ViewportSize = Size.FromInt2((1f / zoom * new float2(Game.Renderer.NativeResolution)).ToInt2());
|
||||||
cellsDirty = true;
|
cellsDirty = true;
|
||||||
allCellsDirty = true;
|
allCellsDirty = true;
|
||||||
}
|
}
|
||||||
@@ -86,6 +90,13 @@ namespace OpenRA.Graphics
|
|||||||
public float MinZoom { get; private set; } = 1f;
|
public float MinZoom { get; private set; } = 1f;
|
||||||
public float MaxZoom { get; private set; } = 2f;
|
public float MaxZoom { get; private set; } = 2f;
|
||||||
|
|
||||||
|
public void OverrideDefaultHeight(float height)
|
||||||
|
{
|
||||||
|
defaultScale = viewportSizes.DefaultScale * Game.Renderer.NativeResolution.Height / height;
|
||||||
|
overrideUserScale = true;
|
||||||
|
UpdateViewportZooms(false);
|
||||||
|
}
|
||||||
|
|
||||||
public void AdjustZoom(float dz)
|
public void AdjustZoom(float dz)
|
||||||
{
|
{
|
||||||
// Exponential ensures that equal positive and negative steps have the same effect
|
// Exponential ensures that equal positive and negative steps have the same effect
|
||||||
@@ -97,7 +108,9 @@ namespace OpenRA.Graphics
|
|||||||
var oldCenter = worldRenderer.Viewport.ViewToWorldPx(center);
|
var oldCenter = worldRenderer.Viewport.ViewToWorldPx(center);
|
||||||
AdjustZoom(dz);
|
AdjustZoom(dz);
|
||||||
var newCenter = worldRenderer.Viewport.ViewToWorldPx(center);
|
var newCenter = worldRenderer.Viewport.ViewToWorldPx(center);
|
||||||
CenterLocation += oldCenter - newCenter;
|
|
||||||
|
var candidateCenterLocation = CenterLocation + oldCenter - newCenter;
|
||||||
|
CenterLocation = candidateCenterLocation.Clamp(mapBounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ToggleZoom()
|
public void ToggleZoom()
|
||||||
@@ -137,16 +150,17 @@ namespace OpenRA.Graphics
|
|||||||
public Viewport(WorldRenderer wr, Map map)
|
public Viewport(WorldRenderer wr, Map map)
|
||||||
{
|
{
|
||||||
worldRenderer = wr;
|
worldRenderer = wr;
|
||||||
var grid = Game.ModData.Manifest.Get<MapGrid>();
|
tileSize = map.Rules.TerrainInfo.TileSize;
|
||||||
viewportSizes = Game.ModData.Manifest.Get<WorldViewportSizes>();
|
viewportSizes = Game.ModData.GetOrCreate<WorldViewportSizes>();
|
||||||
graphicSettings = Game.Settings.Graphics;
|
graphicSettings = Game.Settings.Graphics;
|
||||||
|
defaultScale = viewportSizes.DefaultScale;
|
||||||
|
|
||||||
// Calculate map bounds in world-px
|
// Calculate map bounds in world-px
|
||||||
if (wr.World.Type == WorldType.Editor)
|
if (wr.World.Type == WorldType.Editor)
|
||||||
{
|
{
|
||||||
// The full map is visible in the editor
|
// The full map is visible in the editor
|
||||||
var width = map.MapSize.X * grid.TileSize.Width;
|
var width = map.MapSize.Width * tileSize.Width;
|
||||||
var height = map.MapSize.Y * grid.TileSize.Height;
|
var height = map.MapSize.Height * tileSize.Height;
|
||||||
if (wr.World.Map.Grid.Type == MapGridType.RectangularIsometric)
|
if (wr.World.Map.Grid.Type == MapGridType.RectangularIsometric)
|
||||||
height /= 2;
|
height /= 2;
|
||||||
|
|
||||||
@@ -161,8 +175,6 @@ namespace OpenRA.Graphics
|
|||||||
CenterLocation = (tl + br) / 2;
|
CenterLocation = (tl + br) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
tileSize = grid.TileSize;
|
|
||||||
|
|
||||||
UpdateViewportZooms();
|
UpdateViewportZooms();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +182,9 @@ namespace OpenRA.Graphics
|
|||||||
{
|
{
|
||||||
if (lastViewportDistance != graphicSettings.ViewportDistance)
|
if (lastViewportDistance != graphicSettings.ViewportDistance)
|
||||||
UpdateViewportZooms();
|
UpdateViewportZooms();
|
||||||
|
|
||||||
|
if (ViewportCenterProvider != null)
|
||||||
|
Center(ViewportCenterProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
static float CalculateMinimumZoom(float minHeight, float maxHeight)
|
static float CalculateMinimumZoom(float minHeight, float maxHeight)
|
||||||
@@ -207,17 +222,17 @@ namespace OpenRA.Graphics
|
|||||||
lastViewportDistance = graphicSettings.ViewportDistance;
|
lastViewportDistance = graphicSettings.ViewportDistance;
|
||||||
|
|
||||||
var vd = graphicSettings.ViewportDistance;
|
var vd = graphicSettings.ViewportDistance;
|
||||||
if (viewportSizes.AllowNativeZoom && vd == WorldViewport.Native)
|
if (overrideUserScale || (viewportSizes.AllowNativeZoom && vd == WorldViewport.Native))
|
||||||
MinZoom = viewportSizes.DefaultScale;
|
MinZoom = defaultScale;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var range = viewportSizes.GetSizeRange(vd);
|
var range = viewportSizes.GetSizeRange(vd);
|
||||||
MinZoom = CalculateMinimumZoom(range.X, range.Y) * viewportSizes.DefaultScale;
|
MinZoom = CalculateMinimumZoom(range.X, range.Y) * defaultScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
MaxZoom = Math.Min(
|
MaxZoom = Math.Min(
|
||||||
MinZoom * viewportSizes.MaxZoomScale,
|
MinZoom * viewportSizes.MaxZoomScale,
|
||||||
Game.Renderer.NativeResolution.Height * viewportSizes.DefaultScale / viewportSizes.MaxZoomWindowHeight);
|
Game.Renderer.NativeResolution.Height * defaultScale / viewportSizes.MaxZoomWindowHeight);
|
||||||
|
|
||||||
if (unlockMinZoom)
|
if (unlockMinZoom)
|
||||||
{
|
{
|
||||||
@@ -243,7 +258,7 @@ namespace OpenRA.Graphics
|
|||||||
|
|
||||||
public CPos ViewToWorld(int2 view)
|
public CPos ViewToWorld(int2 view)
|
||||||
{
|
{
|
||||||
var world = worldRenderer.Viewport.ViewToWorldPx(view);
|
var world = ViewToWorldPx(view);
|
||||||
var map = worldRenderer.World.Map;
|
var map = worldRenderer.World.Map;
|
||||||
var candidates = CandidateMouseoverCells(world).ToList();
|
var candidates = CandidateMouseoverCells(world).ToList();
|
||||||
|
|
||||||
@@ -256,7 +271,7 @@ namespace OpenRA.Graphics
|
|||||||
{
|
{
|
||||||
var ramp = map.Grid.Ramps[map.Ramp.Contains(uv) ? map.Ramp[uv] : 0];
|
var ramp = map.Grid.Ramps[map.Ramp.Contains(uv) ? map.Ramp[uv] : 0];
|
||||||
var pos = map.CenterOfCell(uv.ToCPos(map)) - new WVec(0, 0, ramp.CenterHeightOffset);
|
var pos = map.CenterOfCell(uv.ToCPos(map)) - new WVec(0, 0, ramp.CenterHeightOffset);
|
||||||
var screen = ramp.Corners.Select(c => worldRenderer.ScreenPxPosition(pos + c)).ToArray();
|
var screen = ramp.Corners.Select(c => worldRenderer.ScreenPxPosition(pos + c)).ToImmutableArray();
|
||||||
if (screen.PolygonContains(world))
|
if (screen.PolygonContains(world))
|
||||||
return uv.ToCPos(map);
|
return uv.ToCPos(map);
|
||||||
}
|
}
|
||||||
@@ -308,9 +323,9 @@ namespace OpenRA.Graphics
|
|||||||
yield return new MPos(u, v);
|
yield return new MPos(u, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int2 ViewToWorldPx(int2 view) { return (graphicSettings.UIScale / Zoom * view.ToFloat2()).ToInt2() + TopLeft; }
|
public int2 ViewToWorldPx(int2 view) => (graphicSettings.UIScale / Zoom * view.ToFloat2() + CenterLocation - ViewportSize.ToInt2() / 2).ToInt2();
|
||||||
public int2 WorldToViewPx(int2 world) { return (Zoom / graphicSettings.UIScale * (world - TopLeft).ToFloat2()).ToInt2(); }
|
public int2 WorldToViewPx(int2 world) => (Zoom / graphicSettings.UIScale * (world - CenterLocation + ViewportSize.ToInt2() / 2)).ToInt2();
|
||||||
public int2 WorldToViewPx(in float3 world) { return (Zoom / graphicSettings.UIScale * (world - TopLeft).XY).ToInt2(); }
|
public int2 WorldToViewPx(in float3 world) => (Zoom / graphicSettings.UIScale * (world - CenterLocation + ViewportSize.ToInt2() / 2).XY).ToInt2();
|
||||||
|
|
||||||
public void Center(IEnumerable<Actor> actors)
|
public void Center(IEnumerable<Actor> actors)
|
||||||
{
|
{
|
||||||
@@ -330,10 +345,17 @@ namespace OpenRA.Graphics
|
|||||||
allCellsDirty = true;
|
allCellsDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Center(float2 pos)
|
||||||
|
{
|
||||||
|
CenterLocation = worldRenderer.ScreenPosition(pos).Clamp(mapBounds);
|
||||||
|
cellsDirty = true;
|
||||||
|
allCellsDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
public void Scroll(float2 delta, bool ignoreBorders)
|
public void Scroll(float2 delta, bool ignoreBorders)
|
||||||
{
|
{
|
||||||
// Convert scroll delta from world-px to viewport-px
|
// Convert scroll delta from world-px to viewport-px
|
||||||
CenterLocation += (1f / Zoom * delta).ToInt2();
|
CenterLocation += 1f / Zoom * delta;
|
||||||
cellsDirty = true;
|
cellsDirty = true;
|
||||||
allCellsDirty = true;
|
allCellsDirty = true;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using OpenRA.Effects;
|
using OpenRA.Effects;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
using OpenRA.Traits;
|
using OpenRA.Traits;
|
||||||
@@ -31,32 +32,33 @@ namespace OpenRA.Graphics
|
|||||||
|
|
||||||
public event Action PaletteInvalidated = null;
|
public event Action PaletteInvalidated = null;
|
||||||
|
|
||||||
readonly HashSet<Actor> onScreenActors = new();
|
readonly HashSet<Actor> onScreenActors = [];
|
||||||
readonly HardwarePalette palette = new();
|
readonly HardwarePalette palette = new();
|
||||||
readonly Dictionary<string, PaletteReference> palettes = new();
|
readonly Dictionary<string, PaletteReference> palettes = [];
|
||||||
readonly IRenderTerrain terrainRenderer;
|
readonly IRenderTerrain terrainRenderer;
|
||||||
readonly Lazy<DebugVisualizations> debugVis;
|
readonly Lazy<DebugVisualizations> debugVis;
|
||||||
readonly Func<string, PaletteReference> createPaletteReference;
|
readonly Func<string, PaletteReference> createPaletteReference;
|
||||||
readonly bool enableDepthBuffer;
|
readonly bool enableDepthBuffer;
|
||||||
|
|
||||||
readonly List<IFinalizedRenderable> preparedRenderables = new();
|
readonly List<IFinalizedRenderable> preparedRenderables = [];
|
||||||
readonly List<IFinalizedRenderable> preparedOverlayRenderables = new();
|
readonly List<IFinalizedRenderable> preparedOverlayRenderables = [];
|
||||||
readonly List<IFinalizedRenderable> preparedAnnotationRenderables = new();
|
readonly List<IFinalizedRenderable> preparedAnnotationRenderables = [];
|
||||||
|
|
||||||
readonly List<IRenderable> renderablesBuffer = new();
|
readonly List<IRenderable> renderablesBuffer = [];
|
||||||
readonly IRenderer[] renderers;
|
readonly IRenderer[] renderers;
|
||||||
readonly IRenderPostProcessPass[] postProcessPasses;
|
readonly IRenderPostProcessPass[] postProcessPasses;
|
||||||
|
long[] renderablesKeysBuffer = [];
|
||||||
|
|
||||||
internal WorldRenderer(ModData modData, World world)
|
internal WorldRenderer(ModData modData, World world)
|
||||||
{
|
{
|
||||||
World = world;
|
World = world;
|
||||||
TileSize = World.Map.Grid.TileSize;
|
TileSize = World.Map.Rules.TerrainInfo.TileSize;
|
||||||
TileScale = World.Map.Grid.TileScale;
|
TileScale = World.Map.Grid.TileScale;
|
||||||
Viewport = new Viewport(this, world.Map);
|
Viewport = new Viewport(this, world.Map);
|
||||||
|
|
||||||
createPaletteReference = CreatePaletteReference;
|
createPaletteReference = CreatePaletteReference;
|
||||||
|
|
||||||
var mapGrid = modData.Manifest.Get<MapGrid>();
|
var mapGrid = modData.GetOrCreate<MapGrid>();
|
||||||
enableDepthBuffer = mapGrid.EnableDepthBuffer;
|
enableDepthBuffer = mapGrid.EnableDepthBuffer;
|
||||||
|
|
||||||
foreach (var pal in world.TraitDict.ActorsWithTrait<ILoadsPalettes>())
|
foreach (var pal in world.TraitDict.ActorsWithTrait<ILoadsPalettes>())
|
||||||
@@ -70,7 +72,7 @@ namespace OpenRA.Graphics
|
|||||||
renderers = world.WorldActor.TraitsImplementing<IRenderer>().ToArray();
|
renderers = world.WorldActor.TraitsImplementing<IRenderer>().ToArray();
|
||||||
terrainRenderer = world.WorldActor.TraitOrDefault<IRenderTerrain>();
|
terrainRenderer = world.WorldActor.TraitOrDefault<IRenderTerrain>();
|
||||||
|
|
||||||
debugVis = Exts.Lazy(() => world.WorldActor.TraitOrDefault<DebugVisualizations>());
|
debugVis = Exts.Lazy(world.WorldActor.TraitOrDefault<DebugVisualizations>);
|
||||||
|
|
||||||
postProcessPasses = world.WorldActor.TraitsImplementing<IRenderPostProcessPass>().ToArray();
|
postProcessPasses = world.WorldActor.TraitsImplementing<IRenderPostProcessPass>().ToArray();
|
||||||
}
|
}
|
||||||
@@ -157,7 +159,14 @@ namespace OpenRA.Graphics
|
|||||||
renderablesBuffer.AddRange(e.Render(this));
|
renderablesBuffer.AddRange(e.Render(this));
|
||||||
|
|
||||||
// Renderables must be ordered using a stable sorting algorithm to avoid flickering artefacts
|
// Renderables must be ordered using a stable sorting algorithm to avoid flickering artefacts
|
||||||
foreach (var renderable in renderablesBuffer.OrderBy(RenderableZPositionComparisonKey))
|
if (renderablesKeysBuffer.Length < renderablesBuffer.Count)
|
||||||
|
renderablesKeysBuffer = new long[Exts.NextPowerOf2(renderablesBuffer.Count)];
|
||||||
|
for (var i = 0; i < renderablesBuffer.Count; i++)
|
||||||
|
renderablesKeysBuffer[i] = ((long)RenderableZPositionComparisonKey(renderablesBuffer[i]) << 32) + i;
|
||||||
|
var keys = renderablesKeysBuffer.AsSpan(0, renderablesBuffer.Count);
|
||||||
|
keys.Sort(CollectionsMarshal.AsSpan(renderablesBuffer));
|
||||||
|
|
||||||
|
foreach (var renderable in renderablesBuffer)
|
||||||
preparedRenderables.Add(renderable.PrepareRender(this));
|
preparedRenderables.Add(renderable.PrepareRender(this));
|
||||||
|
|
||||||
// PERF: Reuse collection to avoid allocations.
|
// PERF: Reuse collection to avoid allocations.
|
||||||
@@ -371,6 +380,8 @@ namespace OpenRA.Graphics
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ApplyPostProcessing(PostProcessPassType.AfterAnnotations);
|
||||||
|
|
||||||
Game.Renderer.Flush();
|
Game.Renderer.Flush();
|
||||||
|
|
||||||
preparedRenderables.Clear();
|
preparedRenderables.Clear();
|
||||||
@@ -384,12 +395,22 @@ namespace OpenRA.Graphics
|
|||||||
Game.Renderer.SetPalette(palette);
|
Game.Renderer.SetPalette(palette);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conversion between world and screen coordinates
|
/// <summary>
|
||||||
|
/// Converts a world position to a screen position.
|
||||||
|
/// </summary>
|
||||||
public float2 ScreenPosition(WPos pos)
|
public float2 ScreenPosition(WPos pos)
|
||||||
{
|
{
|
||||||
return new float2((float)TileSize.Width * pos.X / TileScale, (float)TileSize.Height * (pos.Y - pos.Z) / TileScale);
|
return new float2((float)TileSize.Width * pos.X / TileScale, (float)TileSize.Height * (pos.Y - pos.Z) / TileScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a world position to a screen position.
|
||||||
|
/// </summary>
|
||||||
|
public float2 ScreenPosition(float2 pos)
|
||||||
|
{
|
||||||
|
return new float2(TileSize.Width * pos.X / TileScale, TileSize.Height * pos.Y / TileScale);
|
||||||
|
}
|
||||||
|
|
||||||
public float3 Screen3DPosition(WPos pos)
|
public float3 Screen3DPosition(WPos pos)
|
||||||
{
|
{
|
||||||
// The projection from world coordinates to screen coordinates has
|
// The projection from world coordinates to screen coordinates has
|
||||||
@@ -429,7 +450,7 @@ namespace OpenRA.Graphics
|
|||||||
public float[] ScreenVector(in WVec vec)
|
public float[] ScreenVector(in WVec vec)
|
||||||
{
|
{
|
||||||
var xyz = ScreenVectorComponents(vec);
|
var xyz = ScreenVectorComponents(vec);
|
||||||
return new[] { xyz.X, xyz.Y, xyz.Z, 1f };
|
return [xyz.X, xyz.Y, xyz.Z, 1f];
|
||||||
}
|
}
|
||||||
|
|
||||||
public int2 ScreenPxOffset(in WVec vec)
|
public int2 ScreenPxOffset(in WVec vec)
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
*/
|
*/
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Frozen;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace OpenRA
|
namespace OpenRA
|
||||||
@@ -24,10 +25,10 @@ namespace OpenRA
|
|||||||
[FluentReference]
|
[FluentReference]
|
||||||
public readonly string Description = "";
|
public readonly string Description = "";
|
||||||
|
|
||||||
public readonly HashSet<string> Types = new();
|
public readonly FrozenSet<string> Types = FrozenSet<string>.Empty;
|
||||||
|
|
||||||
[FluentReference]
|
[FluentReference]
|
||||||
public readonly HashSet<string> Contexts = new();
|
public readonly FrozenSet<string> Contexts = FrozenSet<string>.Empty;
|
||||||
|
|
||||||
public readonly bool Readonly = false;
|
public readonly bool Readonly = false;
|
||||||
public bool HasDuplicates { get; internal set; }
|
public bool HasDuplicates { get; internal set; }
|
||||||
@@ -45,11 +46,11 @@ namespace OpenRA
|
|||||||
Description = descriptionYaml.Value;
|
Description = descriptionYaml.Value;
|
||||||
|
|
||||||
if (nodeDict.TryGetValue("Types", out var typesYaml))
|
if (nodeDict.TryGetValue("Types", out var typesYaml))
|
||||||
Types = FieldLoader.GetValue<HashSet<string>>("Types", typesYaml.Value);
|
Types = FieldLoader.GetValue<FrozenSet<string>>("Types", typesYaml.Value);
|
||||||
|
|
||||||
if (nodeDict.TryGetValue("Contexts", out var contextYaml))
|
if (nodeDict.TryGetValue("Contexts", out var contextYaml))
|
||||||
Contexts = FieldLoader.GetValue<HashSet<string>>("Contexts", contextYaml.Value)
|
Contexts = FieldLoader.GetValue<ImmutableArray<string>>("Contexts", contextYaml.Value)
|
||||||
.Select(c => ContextFluentPrefix + "." + c).ToHashSet();
|
.Select(c => ContextFluentPrefix + "." + c).ToFrozenSet();
|
||||||
|
|
||||||
if (nodeDict.TryGetValue("Platform", out var platformYaml))
|
if (nodeDict.TryGetValue("Platform", out var platformYaml))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,13 +17,16 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
public sealed class HotkeyManager
|
public sealed class HotkeyManager
|
||||||
{
|
{
|
||||||
readonly Dictionary<string, Hotkey> settings;
|
[YamlNode("Keys", shared: true)]
|
||||||
readonly Dictionary<string, HotkeyDefinition> definitions = new();
|
sealed class HotkeySettings : SettingsModule { }
|
||||||
readonly Dictionary<string, Hotkey> keys = new();
|
|
||||||
|
|
||||||
public HotkeyManager(IReadOnlyFileSystem fileSystem, Dictionary<string, Hotkey> settings, Manifest manifest)
|
readonly Dictionary<string, HotkeyDefinition> definitions = [];
|
||||||
|
readonly Dictionary<string, Hotkey> keys = [];
|
||||||
|
readonly HotkeySettings hotkeySettings;
|
||||||
|
|
||||||
|
public HotkeyManager(IReadOnlyFileSystem fileSystem, ObjectCreator objectCreator, Manifest manifest)
|
||||||
{
|
{
|
||||||
this.settings = settings;
|
hotkeySettings = Game.Settings.GetOrCreate<HotkeySettings>(objectCreator, manifest.Id);
|
||||||
|
|
||||||
var keyDefinitions = MiniYaml.Load(fileSystem, manifest.Hotkeys, null);
|
var keyDefinitions = MiniYaml.Load(fileSystem, manifest.Hotkeys, null);
|
||||||
foreach (var kd in keyDefinitions)
|
foreach (var kd in keyDefinitions)
|
||||||
@@ -33,11 +36,9 @@ namespace OpenRA
|
|||||||
keys[kd.Key] = definition.Default;
|
keys[kd.Key] = definition.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var kv in settings)
|
foreach (var node in hotkeySettings.Yaml.Nodes)
|
||||||
{
|
if (definitions.TryGetValue(node.Key, out var definition) && !definition.Readonly)
|
||||||
if (definitions.TryGetValue(kv.Key, out var definition) && !definition.Readonly)
|
keys[node.Key] = FieldLoader.GetValue<Hotkey>(node.Key, node.Value.Value);
|
||||||
keys[kv.Key] = kv.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var hd in definitions)
|
foreach (var hd in definitions)
|
||||||
hd.Value.HasDuplicates = GetFirstDuplicate(hd.Value, this[hd.Value.Name].GetValue()) != null;
|
hd.Value.HasDuplicates = GetFirstDuplicate(hd.Value, this[hd.Value.Name].GetValue()) != null;
|
||||||
@@ -68,10 +69,9 @@ namespace OpenRA
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
keys[name] = value;
|
keys[name] = value;
|
||||||
|
hotkeySettings.Yaml.Nodes.RemoveAll(n => n.Key == name);
|
||||||
if (value != definition.Default)
|
if (value != definition.Default)
|
||||||
settings[name] = value;
|
hotkeySettings.Yaml.Nodes.Add(new MiniYamlNodeBuilder(name, FieldSaver.FormatValue(value)));
|
||||||
else
|
|
||||||
settings.Remove(name);
|
|
||||||
|
|
||||||
var hadDuplicates = definition.HasDuplicates;
|
var hadDuplicates = definition.HasDuplicates;
|
||||||
definition.HasDuplicates = GetFirstDuplicate(definition, this[definition.Name].GetValue()) != null;
|
definition.HasDuplicates = GetFirstDuplicate(definition, this[definition.Name].GetValue()) != null;
|
||||||
@@ -108,5 +108,10 @@ namespace OpenRA
|
|||||||
public HotkeyReference this[string name] => new(GetHotkeyReference(name));
|
public HotkeyReference this[string name] => new(GetHotkeyReference(name));
|
||||||
|
|
||||||
public IEnumerable<HotkeyDefinition> Definitions => definitions.Values;
|
public IEnumerable<HotkeyDefinition> Definitions => definitions.Values;
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
hotkeySettings.Save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,7 @@ namespace OpenRA
|
|||||||
var total = response.Content.Headers.ContentLength ?? -1;
|
var total = response.Content.Headers.ContentLength ?? -1;
|
||||||
var canReportProgress = total > 0;
|
var canReportProgress = total > 0;
|
||||||
|
|
||||||
#if NET5_0_OR_GREATER
|
await using (var contentStream = await response.Content.ReadAsStreamAsync(token))
|
||||||
using (var contentStream = await response.Content.ReadAsStreamAsync(token))
|
|
||||||
#else
|
|
||||||
using (var contentStream = await response.Content.ReadAsStreamAsync())
|
|
||||||
#endif
|
|
||||||
{
|
{
|
||||||
var totalRead = 0L;
|
var totalRead = 0L;
|
||||||
var buffer = new byte[8192];
|
var buffer = new byte[8192];
|
||||||
|
|||||||
@@ -27,25 +27,21 @@ namespace OpenRA
|
|||||||
public static bool TryParse(string s, out Hotkey result)
|
public static bool TryParse(string s, out Hotkey result)
|
||||||
{
|
{
|
||||||
result = Invalid;
|
result = Invalid;
|
||||||
if (string.IsNullOrWhiteSpace(s))
|
if (s == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var parts = s.Split(' ');
|
Span<Range> ranges = stackalloc Range[2];
|
||||||
|
var span = s.AsSpan();
|
||||||
if (!Enum<Keycode>.TryParse(parts[0], true, out var key))
|
var count = span.Split(ranges, ' ');
|
||||||
{
|
if (count == 0)
|
||||||
if (!int.TryParse(parts[0], out var c))
|
return false;
|
||||||
|
|
||||||
|
if (!Enum.TryParse(span[ranges[0]], true, out Keycode key))
|
||||||
return false;
|
return false;
|
||||||
key = (Keycode)c;
|
|
||||||
}
|
|
||||||
|
|
||||||
var mods = Modifiers.None;
|
var mods = Modifiers.None;
|
||||||
if (parts.Length >= 2)
|
if (count == 2 && !Enum.TryParse(span[ranges[1]], true, out mods))
|
||||||
{
|
|
||||||
var modString = s[s.IndexOf(' ')..];
|
|
||||||
if (!Enum<Modifiers>.TryParse(modString, true, out mods))
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
result = new Hotkey(key, mods);
|
result = new Hotkey(key, mods);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -23,25 +23,7 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum MouseInputEvent { Down, Move, Up, Scroll }
|
public enum MouseInputEvent { Down, Move, Up, Scroll }
|
||||||
public struct MouseInput
|
public record struct MouseInput(MouseInputEvent Event, MouseButton Button, int2 Location, int2 Delta, Modifiers Modifiers, int MultiTapCount);
|
||||||
{
|
|
||||||
public MouseInputEvent Event;
|
|
||||||
public MouseButton Button;
|
|
||||||
public int2 Location;
|
|
||||||
public int2 Delta;
|
|
||||||
public Modifiers Modifiers;
|
|
||||||
public int MultiTapCount;
|
|
||||||
|
|
||||||
public MouseInput(MouseInputEvent ev, MouseButton button, int2 location, int2 delta, Modifiers mods, int multiTapCount)
|
|
||||||
{
|
|
||||||
Event = ev;
|
|
||||||
Button = button;
|
|
||||||
Location = location;
|
|
||||||
Delta = delta;
|
|
||||||
Modifiers = mods;
|
|
||||||
MultiTapCount = multiTapCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum MouseButton
|
public enum MouseButton
|
||||||
@@ -65,7 +47,7 @@ namespace OpenRA
|
|||||||
public static class ModifiersExts
|
public static class ModifiersExts
|
||||||
{
|
{
|
||||||
[FluentReference]
|
[FluentReference]
|
||||||
public const string Cmd = "keycode-modifier.cmd";
|
const string Cmd = "keycode-modifier.cmd";
|
||||||
|
|
||||||
[FluentReference(Traits.LintDictionaryReference.Values)]
|
[FluentReference(Traits.LintDictionaryReference.Values)]
|
||||||
public static readonly IReadOnlyDictionary<Modifiers, string> ModifierFluentKeys = new Dictionary<Modifiers, string>()
|
public static readonly IReadOnlyDictionary<Modifiers, string> ModifierFluentKeys = new Dictionary<Modifiers, string>()
|
||||||
|
|||||||
@@ -50,11 +50,4 @@ namespace OpenRA
|
|||||||
Sync.RunUnsynced(world, () => Ui.HandleInput(input));
|
Sync.RunUnsynced(world, () => Ui.HandleInput(input));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MouseButtonPreference
|
|
||||||
{
|
|
||||||
public MouseButton Action => Game.Settings.Game.UseClassicMouseStyle ? MouseButton.Left : MouseButton.Right;
|
|
||||||
|
|
||||||
public MouseButton Cancel => Game.Settings.Game.UseClassicMouseStyle ? MouseButton.Right : MouseButton.Left;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,7 @@ namespace OpenRA
|
|||||||
// (b) Therefore:
|
// (b) Therefore:
|
||||||
// - au + 2bv adds (a + b) to (x, y)
|
// - au + 2bv adds (a + b) to (x, y)
|
||||||
// - a correction factor is added if v is odd
|
// - a correction factor is added if v is odd
|
||||||
var offset = (V & 1) == 1 ? 1 : 0;
|
var y = (V - (V & 1)) / 2 - U;
|
||||||
var y = (V - offset) / 2 - U;
|
|
||||||
var x = V - y;
|
var x = V - y;
|
||||||
return new CPos(x, y);
|
return new CPos(x, y);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,51 +9,27 @@
|
|||||||
*/
|
*/
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System.Collections.Frozen;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenRA.FileSystem;
|
using OpenRA.FileSystem;
|
||||||
using OpenRA.Primitives;
|
|
||||||
|
|
||||||
namespace OpenRA
|
namespace OpenRA
|
||||||
{
|
{
|
||||||
public interface IGlobalModData { }
|
|
||||||
|
|
||||||
public sealed class TerrainFormat : IGlobalModData
|
|
||||||
{
|
|
||||||
public readonly string Type;
|
|
||||||
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
|
|
||||||
public TerrainFormat(MiniYaml yaml)
|
|
||||||
{
|
|
||||||
Type = yaml.Value;
|
|
||||||
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class SpriteSequenceFormat : IGlobalModData
|
|
||||||
{
|
|
||||||
public readonly string Type;
|
|
||||||
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
|
|
||||||
public SpriteSequenceFormat(MiniYaml yaml)
|
|
||||||
{
|
|
||||||
Type = yaml.Value;
|
|
||||||
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ModMetadata
|
public class ModMetadata
|
||||||
{
|
{
|
||||||
// FieldLoader used here, must matching naming in YAML.
|
// FieldLoader used here, must matching naming in YAML.
|
||||||
#pragma warning disable IDE1006 // Naming Styles
|
#pragma warning disable IDE1006 // Naming Styles
|
||||||
[FluentReference]
|
[FluentReference]
|
||||||
readonly string Title;
|
public readonly string Title;
|
||||||
public readonly string Version;
|
public readonly string Version;
|
||||||
public readonly string Website;
|
public readonly string Website;
|
||||||
public readonly string WebIcon32;
|
public readonly string WebIcon32;
|
||||||
[FluentReference]
|
|
||||||
readonly string WindowTitle;
|
[FluentReference(optional: true)]
|
||||||
|
public readonly string WindowTitle;
|
||||||
public readonly bool Hidden;
|
public readonly bool Hidden;
|
||||||
#pragma warning restore IDE1006 // Naming Styles
|
#pragma warning restore IDE1006 // Naming Styles
|
||||||
|
|
||||||
@@ -61,48 +37,56 @@ namespace OpenRA
|
|||||||
public string WindowTitleTranslated => WindowTitle != null ? FluentProvider.GetMessage(WindowTitle) : null;
|
public string WindowTitleTranslated => WindowTitle != null ? FluentProvider.GetMessage(WindowTitle) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class RendererConstants
|
||||||
|
{
|
||||||
|
public readonly int FontSheetSize = 512;
|
||||||
|
public readonly int CursorSheetSize = 512;
|
||||||
|
public readonly int MapPreviewSheetSize = 2048;
|
||||||
|
public readonly int SequenceBgraSheetSize = 2048;
|
||||||
|
public readonly int SequenceIndexedSheetSize = 2048;
|
||||||
|
public readonly int VertexBatchSize = 8192;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Describes what is to be loaded in order to run a mod.</summary>
|
/// <summary>Describes what is to be loaded in order to run a mod.</summary>
|
||||||
public sealed class Manifest : IDisposable
|
public sealed class Manifest
|
||||||
{
|
{
|
||||||
public readonly string Id;
|
public readonly string Id;
|
||||||
public readonly IReadOnlyPackage Package;
|
public readonly IReadOnlyPackage Package;
|
||||||
public readonly ModMetadata Metadata;
|
public readonly ModMetadata Metadata;
|
||||||
public readonly string[]
|
public readonly ImmutableArray<string>
|
||||||
Rules, ServerTraits,
|
Rules, ServerTraits,
|
||||||
Sequences, ModelSequences, Cursors, Chrome, ChromeLayout,
|
Sequences, ModelSequences, Cursors, Chrome, ChromeLayout,
|
||||||
Weapons, Voices, Notifications, Music, FluentMessages, TileSets,
|
Weapons, Voices, Notifications, Music, FluentMessages, TileSets,
|
||||||
ChromeMetrics, MapCompatibility, Missions, Hotkeys;
|
ChromeMetrics, MapCompatibility, Missions, Hotkeys;
|
||||||
|
|
||||||
public readonly IReadOnlyDictionary<string, string> MapFolders;
|
public readonly FrozenDictionary<string, string> MapFolders;
|
||||||
public readonly MiniYaml FileSystem;
|
public readonly MiniYaml FileSystem;
|
||||||
public readonly MiniYaml LoadScreen;
|
public readonly MiniYaml LoadScreen;
|
||||||
public readonly string DefaultOrderGenerator;
|
public readonly string DefaultOrderGenerator;
|
||||||
|
public readonly RendererConstants RendererConstants;
|
||||||
|
|
||||||
public readonly string[] Assemblies = Array.Empty<string>();
|
public readonly ImmutableArray<string> Assemblies = [];
|
||||||
public readonly string[] SoundFormats = Array.Empty<string>();
|
public readonly ImmutableArray<string> SoundFormats = [];
|
||||||
public readonly string[] SpriteFormats = Array.Empty<string>();
|
public readonly ImmutableArray<string> SpriteFormats = [];
|
||||||
public readonly string[] PackageFormats = Array.Empty<string>();
|
public readonly ImmutableArray<string> PackageFormats = [];
|
||||||
public readonly string[] VideoFormats = Array.Empty<string>();
|
public readonly ImmutableArray<string> VideoFormats = [];
|
||||||
public readonly int FontSheetSize = 512;
|
public readonly string SpriteSequenceFormat;
|
||||||
public readonly int CursorSheetSize = 512;
|
public readonly string TerrainFormat;
|
||||||
|
|
||||||
// TODO: This should be controlled by a user-selected translation bundle!
|
// TODO: This should be controlled by a user-selected translation bundle!
|
||||||
public readonly string FluentCulture = "en";
|
public readonly string FluentCulture = "en";
|
||||||
public readonly bool AllowUnusedFluentMessagesInExternalPackages = true;
|
public readonly bool AllowUnusedFluentMessagesInExternalPackages = true;
|
||||||
|
|
||||||
readonly string[] reservedModuleNames =
|
static readonly FrozenSet<string> ReservedModuleNames = new HashSet<string>
|
||||||
{
|
{
|
||||||
"Include", "Metadata", "FileSystem", "MapFolders", "Rules",
|
"Include", "Metadata", "FileSystem", "MapFolders", "Rules",
|
||||||
"Sequences", "ModelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons",
|
"Sequences", "ModelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons",
|
||||||
"Voices", "Notifications", "Music", "FluentMessages", "TileSets", "ChromeMetrics", "Missions", "Hotkeys",
|
"Voices", "Notifications", "Music", "FluentMessages", "TileSets", "ChromeMetrics", "Missions", "Hotkeys",
|
||||||
"ServerTraits", "LoadScreen", "DefaultOrderGenerator", "SupportsMapsFrom", "SoundFormats", "SpriteFormats", "VideoFormats",
|
"ServerTraits", "LoadScreen", "DefaultOrderGenerator", "SupportsMapsFrom", "SoundFormats", "SpriteFormats", "VideoFormats",
|
||||||
"RequiresMods", "PackageFormats", "AllowUnusedFluentMessagesInExternalPackages", "FontSheetSize", "CursorSheetSize"
|
"SpriteSequenceFormat", "TerrainFormat", "RequiresMods", "PackageFormats", "AllowUnusedFluentMessagesInExternalPackages", "RendererConstants"
|
||||||
};
|
}.ToFrozenSet();
|
||||||
|
|
||||||
readonly TypeDictionary modules = new();
|
public readonly FrozenDictionary<string, MiniYaml> GlobalModData;
|
||||||
readonly Dictionary<string, MiniYaml> yaml;
|
|
||||||
|
|
||||||
bool customDataLoaded;
|
|
||||||
|
|
||||||
public Manifest(string modId, IReadOnlyPackage package)
|
public Manifest(string modId, IReadOnlyPackage package)
|
||||||
{
|
{
|
||||||
@@ -110,7 +94,7 @@ namespace OpenRA
|
|||||||
Package = package;
|
Package = package;
|
||||||
|
|
||||||
var stringPool = new HashSet<string>(); // Reuse common strings in 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);
|
var nodes = MiniYaml.FromStream(package.GetStream("mod.yaml"), $"{package.Name}:mod.yaml", stringPool: stringPool).ToList();
|
||||||
for (var i = nodes.Count - 1; i >= 0; i--)
|
for (var i = nodes.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
if (nodes[i].Key != "Include")
|
if (nodes[i].Key != "Include")
|
||||||
@@ -127,7 +111,7 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Merge inherited overrides
|
// Merge inherited overrides
|
||||||
yaml = new MiniYaml(null, MiniYaml.Merge(new[] { nodes })).ToDictionary();
|
var yaml = new MiniYaml(null, MiniYaml.Merge([nodes])).ToDictionary();
|
||||||
|
|
||||||
Metadata = FieldLoader.Load<ModMetadata>(yaml["Metadata"]);
|
Metadata = FieldLoader.Load<ModMetadata>(yaml["Metadata"]);
|
||||||
|
|
||||||
@@ -164,144 +148,59 @@ namespace OpenRA
|
|||||||
if (yaml.TryGetValue("SupportsMapsFrom", out var entry))
|
if (yaml.TryGetValue("SupportsMapsFrom", out var entry))
|
||||||
compat.AddRange(entry.Value.Split(',').Select(c => c.Trim()));
|
compat.AddRange(entry.Value.Split(',').Select(c => c.Trim()));
|
||||||
|
|
||||||
MapCompatibility = compat.ToArray();
|
MapCompatibility = compat.ToImmutableArray();
|
||||||
|
|
||||||
if (yaml.TryGetValue("DefaultOrderGenerator", out entry))
|
if (yaml.TryGetValue("DefaultOrderGenerator", out entry))
|
||||||
DefaultOrderGenerator = entry.Value;
|
DefaultOrderGenerator = entry.Value;
|
||||||
|
|
||||||
if (yaml.TryGetValue("Assemblies", out entry))
|
if (yaml.TryGetValue("Assemblies", out entry))
|
||||||
Assemblies = FieldLoader.GetValue<string[]>("Assemblies", entry.Value);
|
Assemblies = FieldLoader.GetValue<ImmutableArray<string>>("Assemblies", entry.Value);
|
||||||
|
|
||||||
if (yaml.TryGetValue("PackageFormats", out entry))
|
if (yaml.TryGetValue("PackageFormats", out entry))
|
||||||
PackageFormats = FieldLoader.GetValue<string[]>("PackageFormats", entry.Value);
|
PackageFormats = FieldLoader.GetValue<ImmutableArray<string>>("PackageFormats", entry.Value);
|
||||||
|
|
||||||
if (yaml.TryGetValue("SoundFormats", out entry))
|
if (yaml.TryGetValue("SoundFormats", out entry))
|
||||||
SoundFormats = FieldLoader.GetValue<string[]>("SoundFormats", entry.Value);
|
SoundFormats = FieldLoader.GetValue<ImmutableArray<string>>("SoundFormats", entry.Value);
|
||||||
|
|
||||||
if (yaml.TryGetValue("SpriteFormats", out entry))
|
if (yaml.TryGetValue("SpriteFormats", out entry))
|
||||||
SpriteFormats = FieldLoader.GetValue<string[]>("SpriteFormats", entry.Value);
|
SpriteFormats = FieldLoader.GetValue<ImmutableArray<string>>("SpriteFormats", entry.Value);
|
||||||
|
|
||||||
if (yaml.TryGetValue("VideoFormats", out entry))
|
if (yaml.TryGetValue("VideoFormats", out entry))
|
||||||
VideoFormats = FieldLoader.GetValue<string[]>("VideoFormats", entry.Value);
|
VideoFormats = FieldLoader.GetValue<ImmutableArray<string>>("VideoFormats", entry.Value);
|
||||||
|
|
||||||
|
if (yaml.TryGetValue("SpriteSequenceFormat", out entry))
|
||||||
|
SpriteSequenceFormat = entry.Value;
|
||||||
|
|
||||||
|
if (yaml.TryGetValue("TerrainFormat", out entry))
|
||||||
|
TerrainFormat = entry.Value;
|
||||||
|
|
||||||
if (yaml.TryGetValue("AllowUnusedFluentMessagesInExternalPackages", out entry))
|
if (yaml.TryGetValue("AllowUnusedFluentMessagesInExternalPackages", out entry))
|
||||||
AllowUnusedFluentMessagesInExternalPackages =
|
AllowUnusedFluentMessagesInExternalPackages =
|
||||||
FieldLoader.GetValue<bool>("AllowUnusedFluentMessagesInExternalPackages", entry.Value);
|
FieldLoader.GetValue<bool>("AllowUnusedFluentMessagesInExternalPackages", entry.Value);
|
||||||
|
|
||||||
if (yaml.TryGetValue("FontSheetSize", out entry))
|
if (yaml.TryGetValue("RendererConstants", out entry))
|
||||||
FontSheetSize = FieldLoader.GetValue<int>("FontSheetSize", entry.Value);
|
RendererConstants = FieldLoader.Load<RendererConstants>(entry);
|
||||||
|
|
||||||
if (yaml.TryGetValue("CursorSheetSize", out entry))
|
|
||||||
CursorSheetSize = FieldLoader.GetValue<int>("CursorSheetSize", entry.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadCustomData(ObjectCreator oc)
|
|
||||||
{
|
|
||||||
foreach (var kv in yaml)
|
|
||||||
{
|
|
||||||
if (reservedModuleNames.Contains(kv.Key))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var t = oc.FindType(kv.Key);
|
|
||||||
if (t == null || !typeof(IGlobalModData).IsAssignableFrom(t))
|
|
||||||
throw new InvalidDataException($"`{kv.Key}` is not a valid mod manifest entry.");
|
|
||||||
|
|
||||||
IGlobalModData module;
|
|
||||||
var ctor = t.GetConstructor(new[] { typeof(MiniYaml) });
|
|
||||||
if (ctor != null)
|
|
||||||
{
|
|
||||||
// Class has opted-in to DIY initialization
|
|
||||||
module = (IGlobalModData)ctor.Invoke(new object[] { kv.Value });
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
RendererConstants = new RendererConstants();
|
||||||
// Automatically load the child nodes using FieldLoader
|
|
||||||
module = oc.CreateObject<IGlobalModData>(kv.Key);
|
GlobalModData = yaml.Where(n => !ReservedModuleNames.Contains(n.Key))
|
||||||
FieldLoader.Load(module, kv.Value);
|
.ToFrozenDictionary(n => n.Key, n => n.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
modules.Add(module);
|
static ImmutableArray<string> YamlList(Dictionary<string, MiniYaml> yaml, string key)
|
||||||
}
|
|
||||||
|
|
||||||
customDataLoaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static string[] YamlList(Dictionary<string, MiniYaml> yaml, string key)
|
|
||||||
{
|
{
|
||||||
if (!yaml.TryGetValue(key, out var value))
|
if (!yaml.TryGetValue(key, out var value))
|
||||||
return Array.Empty<string>();
|
return [];
|
||||||
|
|
||||||
return value.Nodes.Select(n => n.Key).ToArray();
|
return value.Nodes.Select(n => n.Key).ToImmutableArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
static IReadOnlyDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
|
static FrozenDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
|
||||||
{
|
{
|
||||||
if (!yaml.TryGetValue(key, out var value))
|
if (!yaml.TryGetValue(key, out var value))
|
||||||
return new Dictionary<string, string>();
|
return FrozenDictionary<string, string>.Empty;
|
||||||
|
|
||||||
return value.ToDictionary(my => my.Value);
|
return value.ToDictionary(my => my.Value).ToFrozenDictionary();
|
||||||
}
|
|
||||||
|
|
||||||
public bool Contains<T>() where T : IGlobalModData
|
|
||||||
{
|
|
||||||
return modules.Contains<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Load a cached IGlobalModData instance.</summary>
|
|
||||||
public T Get<T>() where T : IGlobalModData
|
|
||||||
{
|
|
||||||
if (!customDataLoaded)
|
|
||||||
throw new InvalidOperationException("Attempted to call Manifest.Get() before loading custom data!");
|
|
||||||
|
|
||||||
var module = modules.GetOrDefault<T>();
|
|
||||||
|
|
||||||
// Lazily create the default values if not explicitly defined.
|
|
||||||
if (module == null)
|
|
||||||
{
|
|
||||||
module = (T)Game.ModData.ObjectCreator.CreateBasic(typeof(T));
|
|
||||||
modules.Add(module);
|
|
||||||
}
|
|
||||||
|
|
||||||
return module;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Load an uncached IGlobalModData instance directly from the manifest yaml.
|
|
||||||
/// This should only be used by external mods that want to query data from this mod.
|
|
||||||
/// </summary>
|
|
||||||
public T Get<T>(ObjectCreator oc) where T : IGlobalModData
|
|
||||||
{
|
|
||||||
var t = typeof(T);
|
|
||||||
if (!yaml.TryGetValue(t.Name, out var data))
|
|
||||||
{
|
|
||||||
// Lazily create the default values if not explicitly defined.
|
|
||||||
return (T)oc.CreateBasic(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
IGlobalModData module;
|
|
||||||
var ctor = t.GetConstructor(new[] { typeof(MiniYaml) });
|
|
||||||
if (ctor != null)
|
|
||||||
{
|
|
||||||
// Class has opted-in to DIY initialization
|
|
||||||
module = (IGlobalModData)ctor.Invoke(new object[] { data.Value });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Automatically load the child nodes using FieldLoader
|
|
||||||
module = oc.CreateObject<IGlobalModData>(t.Name);
|
|
||||||
FieldLoader.Load(module, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (T)module;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
foreach (var module in modules)
|
|
||||||
{
|
|
||||||
var disposableModule = module as IDisposable;
|
|
||||||
disposableModule?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,10 +216,8 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LocationInit : ValueActorInit<CPos>, ISingleInstanceInit
|
public class LocationInit(CPos value) : ValueActorInit<CPos>(value), ISingleInstanceInit
|
||||||
{
|
{
|
||||||
public LocationInit(CPos value)
|
|
||||||
: base(value) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OwnerInit : ActorInit, ISingleInstanceInit
|
public class OwnerInit : ActorInit, ISingleInstanceInit
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ using System.Collections;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.CompilerServices;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
using OpenRA.Traits;
|
using OpenRA.Traits;
|
||||||
|
|
||||||
@@ -30,15 +30,15 @@ namespace OpenRA
|
|||||||
internal TypeDictionary InitDict => initDict.Value;
|
internal TypeDictionary InitDict => initDict.Value;
|
||||||
|
|
||||||
public ActorReference(string type)
|
public ActorReference(string type)
|
||||||
: this(type, new Dictionary<string, MiniYaml>()) { }
|
: this(type, new MiniYaml("")) { }
|
||||||
|
|
||||||
public ActorReference(string type, Dictionary<string, MiniYaml> inits)
|
public ActorReference(string type, MiniYaml inits)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
initDict = Exts.Lazy(() =>
|
initDict = Exts.Lazy(() =>
|
||||||
{
|
{
|
||||||
var dict = new TypeDictionary();
|
var dict = new TypeDictionary();
|
||||||
foreach (var i in inits)
|
foreach (var i in inits.Nodes)
|
||||||
{
|
{
|
||||||
var init = LoadInit(i.Key, i.Value);
|
var init = LoadInit(i.Key, i.Value);
|
||||||
if (init is ISingleInstanceInit && dict.Contains(init.GetType()))
|
if (init is ISingleInstanceInit && dict.Contains(init.GetType()))
|
||||||
@@ -54,13 +54,8 @@ namespace OpenRA
|
|||||||
public ActorReference(string type, TypeDictionary inits)
|
public ActorReference(string type, TypeDictionary inits)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
initDict = new Lazy<TypeDictionary>(() =>
|
var initsClone = new TypeDictionary(inits);
|
||||||
{
|
initDict = Exts.Lazy(() => initsClone);
|
||||||
var dict = new TypeDictionary();
|
|
||||||
foreach (var i in inits)
|
|
||||||
dict.Add(i);
|
|
||||||
return dict;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static ActorInit LoadInit(string initName, MiniYaml initYaml)
|
static ActorInit LoadInit(string initName, MiniYaml initYaml)
|
||||||
@@ -70,15 +65,15 @@ namespace OpenRA
|
|||||||
if (type == null)
|
if (type == null)
|
||||||
throw new InvalidDataException($"Unknown initializer type '{initInstance[0]}Init'");
|
throw new InvalidDataException($"Unknown initializer type '{initInstance[0]}Init'");
|
||||||
|
|
||||||
var init = (ActorInit)FormatterServices.GetUninitializedObject(type);
|
var init = (ActorInit)RuntimeHelpers.GetUninitializedObject(type);
|
||||||
if (initInstance.Length > 1)
|
if (initInstance.Length > 1)
|
||||||
type.GetField(nameof(ActorInit.InstanceName)).SetValue(init, initInstance[1]);
|
type.GetField(nameof(ActorInit.InstanceName)).SetValue(init, initInstance[1]);
|
||||||
|
|
||||||
var loader = type.GetMethod("Initialize", new[] { typeof(MiniYaml) });
|
var loader = type.GetMethod("Initialize", [typeof(MiniYaml)]);
|
||||||
if (loader == null)
|
if (loader == null)
|
||||||
throw new InvalidDataException($"{initInstance[0]}Init does not define a yaml-assignable type.");
|
throw new InvalidDataException($"{initInstance[0]}Init does not define a yaml-assignable type.");
|
||||||
|
|
||||||
loader.Invoke(init, new[] { initYaml });
|
loader.Invoke(init, [initYaml]);
|
||||||
return init;
|
return init;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,11 +105,7 @@ namespace OpenRA
|
|||||||
|
|
||||||
public ActorReference Clone()
|
public ActorReference Clone()
|
||||||
{
|
{
|
||||||
var clone = new ActorReference(Type);
|
return new ActorReference(Type, initDict.Value);
|
||||||
foreach (var init in initDict.Value)
|
|
||||||
clone.initDict.Value.Add(init);
|
|
||||||
|
|
||||||
return clone;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Add(ActorInit init)
|
public void Add(ActorInit init)
|
||||||
@@ -125,6 +116,15 @@ namespace OpenRA
|
|||||||
InitDict.Add(init);
|
InitDict.Add(init);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Replace<T>(T init) where T : ActorInit, ISingleInstanceInit
|
||||||
|
{
|
||||||
|
var original = GetOrDefault<T>();
|
||||||
|
if (original != null)
|
||||||
|
Remove(original);
|
||||||
|
|
||||||
|
Add(init);
|
||||||
|
}
|
||||||
|
|
||||||
public void Remove(ActorInit o) { initDict.Value.Remove(o); }
|
public void Remove(ActorInit o) { initDict.Value.Remove(o); }
|
||||||
|
|
||||||
public int RemoveAll<T>() where T : ActorInit
|
public int RemoveAll<T>() where T : ActorInit
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
@@ -89,6 +90,31 @@ namespace OpenRA
|
|||||||
return GetEnumerator();
|
return GetEnumerator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the minimal region that covers at least the specified cells.</summary>
|
||||||
|
public static CellCoordsRegion BoundingRegion(IReadOnlyCollection<CPos> cells)
|
||||||
|
{
|
||||||
|
if (cells == null || cells.Count == 0)
|
||||||
|
throw new ArgumentException("cells must not be null or empty.", nameof(cells));
|
||||||
|
|
||||||
|
var minX = int.MaxValue;
|
||||||
|
var minY = int.MaxValue;
|
||||||
|
var maxX = int.MinValue;
|
||||||
|
var maxY = int.MinValue;
|
||||||
|
foreach (var cell in cells)
|
||||||
|
{
|
||||||
|
if (minX > cell.X)
|
||||||
|
minX = cell.X;
|
||||||
|
if (maxX < cell.X)
|
||||||
|
maxX = cell.X;
|
||||||
|
if (minY > cell.Y)
|
||||||
|
minY = cell.Y;
|
||||||
|
if (maxY < cell.Y)
|
||||||
|
maxY = cell.Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CellCoordsRegion(new CPos(minX, minY), new CPos(maxX, maxY));
|
||||||
|
}
|
||||||
|
|
||||||
public CPos TopLeft { get; }
|
public CPos TopLeft { get; }
|
||||||
public CPos BottomRight { get; }
|
public CPos BottomRight { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,12 +63,16 @@ namespace OpenRA
|
|||||||
|
|
||||||
var u = (x - y) / 2;
|
var u = (x - y) / 2;
|
||||||
var v = x + y;
|
var v = x + y;
|
||||||
|
if (!Bounds.Contains(u, v))
|
||||||
|
throw new IndexOutOfRangeException();
|
||||||
return v * Size.Width + u;
|
return v * Size.Width + u;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve an array index from map coordinates
|
// Resolve an array index from map coordinates
|
||||||
int Index(MPos uv)
|
int Index(MPos uv)
|
||||||
{
|
{
|
||||||
|
if (!Bounds.Contains(uv.U, uv.V))
|
||||||
|
throw new IndexOutOfRangeException();
|
||||||
return uv.V * Size.Width + uv.U;
|
return uv.V * Size.Width + uv.U;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +149,9 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
return uv.Clamp(new Rectangle(0, 0, Size.Width - 1, Size.Height - 1));
|
return uv.Clamp(new Rectangle(0, 0, Size.Width - 1, Size.Height - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CellRegion CellRegion =>
|
||||||
|
new(GridType, new MPos(0, 0), new MPos(Size.Width - 1, Size.Height - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace OpenRA
|
|||||||
protected readonly Rectangle Bounds;
|
protected readonly Rectangle Bounds;
|
||||||
|
|
||||||
protected CellLayerBase(Map map)
|
protected CellLayerBase(Map map)
|
||||||
: this(map.Grid.Type, new Size(map.MapSize.X, map.MapSize.Y)) { }
|
: this(map.Grid.Type, map.MapSize) { }
|
||||||
|
|
||||||
protected CellLayerBase(MapGridType gridType, Size size)
|
protected CellLayerBase(MapGridType gridType, Size size)
|
||||||
{
|
{
|
||||||
@@ -40,19 +40,19 @@ namespace OpenRA
|
|||||||
if (Size != anotherLayer.Size || GridType != anotherLayer.GridType)
|
if (Size != anotherLayer.Size || GridType != anotherLayer.GridType)
|
||||||
throw new ArgumentException("Layers must have a matching size and shape (grid type).", nameof(anotherLayer));
|
throw new ArgumentException("Layers must have a matching size and shape (grid type).", nameof(anotherLayer));
|
||||||
|
|
||||||
Array.Copy(anotherLayer.Entries, Entries, Entries.Length);
|
anotherLayer.Entries.AsSpan().CopyTo(Entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Clears the layer contents with their default value.</summary>
|
/// <summary>Clears the layer contents with their default value.</summary>
|
||||||
public virtual void Clear()
|
public virtual void Clear()
|
||||||
{
|
{
|
||||||
Array.Clear(Entries, 0, Entries.Length);
|
Entries.AsSpan().Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Clears the layer contents with a known value.</summary>
|
/// <summary>Clears the layer contents with a known value.</summary>
|
||||||
public virtual void Clear(T clearValue)
|
public virtual void Clear(T clearValue)
|
||||||
{
|
{
|
||||||
Array.Fill(Entries, clearValue);
|
Entries.AsSpan().Fill(clearValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerator<T> GetEnumerator()
|
public IEnumerator<T> GetEnumerator()
|
||||||
@@ -64,5 +64,15 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
return GetEnumerator();
|
return GetEnumerator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ReadOnlyMemory<T> AsReadOnlyMemory()
|
||||||
|
{
|
||||||
|
return Entries.AsMemory();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Memory<T> AsMemory()
|
||||||
|
{
|
||||||
|
return Entries.AsMemory();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ namespace OpenRA
|
|||||||
public readonly uint HeightsOffset;
|
public readonly uint HeightsOffset;
|
||||||
public readonly uint ResourcesOffset;
|
public readonly uint ResourcesOffset;
|
||||||
|
|
||||||
public BinaryDataHeader(Stream s, int2 expectedSize)
|
public BinaryDataHeader(Stream s, Size expectedSize)
|
||||||
{
|
{
|
||||||
Format = s.ReadUInt8();
|
Format = s.ReadUInt8();
|
||||||
var width = s.ReadUInt16();
|
var width = s.ReadUInt16();
|
||||||
var height = s.ReadUInt16();
|
var height = s.ReadUInt16();
|
||||||
if (width != expectedSize.X || height != expectedSize.Y)
|
if (width != expectedSize.Width || height != expectedSize.Height)
|
||||||
throw new InvalidDataException("Invalid tile data");
|
throw new InvalidDataException("Invalid tile data");
|
||||||
|
|
||||||
if (Format == 1)
|
if (Format == 1)
|
||||||
@@ -112,7 +112,7 @@ namespace OpenRA
|
|||||||
else if (type == Type.MiniYaml)
|
else if (type == Type.MiniYaml)
|
||||||
field.SetValue(map, node.Value);
|
field.SetValue(map, node.Value);
|
||||||
else
|
else
|
||||||
FieldLoader.LoadField(map, fieldName, node.Value.Value);
|
FieldLoader.LoadFieldOrProperty(map, fieldName, node.Value.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (property != null)
|
if (property != null)
|
||||||
@@ -122,7 +122,7 @@ namespace OpenRA
|
|||||||
else if (type == Type.MiniYaml)
|
else if (type == Type.MiniYaml)
|
||||||
property.SetValue(map, node.Value, null);
|
property.SetValue(map, node.Value, null);
|
||||||
else
|
else
|
||||||
FieldLoader.LoadField(map, fieldName, node.Value.Value);
|
FieldLoader.LoadFieldOrProperty(map, fieldName, node.Value.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ namespace OpenRA
|
|||||||
|
|
||||||
/// <summary>Defines the order of the fields in map.yaml.</summary>
|
/// <summary>Defines the order of the fields in map.yaml.</summary>
|
||||||
static readonly MapField[] YamlFields =
|
static readonly MapField[] YamlFields =
|
||||||
{
|
[
|
||||||
new("MapFormat"),
|
new("MapFormat"),
|
||||||
new("RequiresMod"),
|
new("RequiresMod"),
|
||||||
new("Title"),
|
new("Title"),
|
||||||
@@ -179,7 +179,7 @@ namespace OpenRA
|
|||||||
new("Voices", nameof(VoiceDefinitions), required: false),
|
new("Voices", nameof(VoiceDefinitions), required: false),
|
||||||
new("Music", nameof(MusicDefinitions), required: false),
|
new("Music", nameof(MusicDefinitions), required: false),
|
||||||
new("Notifications", nameof(NotificationDefinitions), required: false),
|
new("Notifications", nameof(NotificationDefinitions), required: false),
|
||||||
};
|
];
|
||||||
|
|
||||||
// Format versions
|
// Format versions
|
||||||
public int MapFormat { get; private set; }
|
public int MapFormat { get; private set; }
|
||||||
@@ -193,13 +193,13 @@ namespace OpenRA
|
|||||||
public bool LockPreview;
|
public bool LockPreview;
|
||||||
public Rectangle Bounds;
|
public Rectangle Bounds;
|
||||||
public MapVisibility Visibility = MapVisibility.Lobby;
|
public MapVisibility Visibility = MapVisibility.Lobby;
|
||||||
public string[] Categories = { "Conquest" };
|
public ImmutableArray<string> Categories = ["Conquest"];
|
||||||
|
|
||||||
public int2 MapSize { get; private set; }
|
public Size MapSize { get; private set; }
|
||||||
|
|
||||||
// Player and actor yaml. Public for access by the map importers and lint checks.
|
// Player and actor yaml. Public for access by the map importers and lint checks.
|
||||||
public IReadOnlyCollection<MiniYamlNode> PlayerDefinitions = ImmutableArray<MiniYamlNode>.Empty;
|
public IReadOnlyCollection<MiniYamlNode> PlayerDefinitions = [];
|
||||||
public IReadOnlyCollection<MiniYamlNode> ActorDefinitions = ImmutableArray<MiniYamlNode>.Empty;
|
public IReadOnlyCollection<MiniYamlNode> ActorDefinitions = [];
|
||||||
|
|
||||||
// Custom map yaml. Public for access by the map importers and lint checks
|
// Custom map yaml. Public for access by the map importers and lint checks
|
||||||
public MiniYaml RuleDefinitions;
|
public MiniYaml RuleDefinitions;
|
||||||
@@ -211,7 +211,7 @@ namespace OpenRA
|
|||||||
public MiniYaml MusicDefinitions;
|
public MiniYaml MusicDefinitions;
|
||||||
public MiniYaml NotificationDefinitions;
|
public MiniYaml NotificationDefinitions;
|
||||||
|
|
||||||
public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = new();
|
public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = [];
|
||||||
|
|
||||||
// Generated data
|
// Generated data
|
||||||
public readonly MapGrid Grid;
|
public readonly MapGrid Grid;
|
||||||
@@ -283,7 +283,7 @@ namespace OpenRA
|
|||||||
|
|
||||||
// Take the SHA1
|
// Take the SHA1
|
||||||
if (streams.Count == 0)
|
if (streams.Count == 0)
|
||||||
return CryptoUtil.SHA1Hash(Array.Empty<byte>());
|
return CryptoUtil.SHA1Hash([]);
|
||||||
|
|
||||||
var merged = streams[0];
|
var merged = streams[0];
|
||||||
for (var i = 1; i < streams.Count; i++)
|
for (var i = 1; i < streams.Count; i++)
|
||||||
@@ -321,26 +321,25 @@ namespace OpenRA
|
|||||||
/// Initializes a new map created by the editor or importer.
|
/// Initializes a new map created by the editor or importer.
|
||||||
/// The map will not receive a valid UID until after it has been saved and reloaded.
|
/// The map will not receive a valid UID until after it has been saved and reloaded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Map(ModData modData, ITerrainInfo terrainInfo, int width, int height)
|
public Map(ModData modData, ITerrainInfo terrainInfo, Size size)
|
||||||
{
|
{
|
||||||
this.modData = modData;
|
this.modData = modData;
|
||||||
var size = new Size(width, height);
|
MapSize = size;
|
||||||
Grid = modData.Manifest.Get<MapGrid>();
|
Grid = modData.GetOrCreate<MapGrid>();
|
||||||
|
|
||||||
Title = "Name your map here";
|
Title = "Name your map here";
|
||||||
Author = "Your name here";
|
Author = "Your name here";
|
||||||
|
|
||||||
MapSize = new int2(size);
|
|
||||||
Tileset = terrainInfo.Id;
|
Tileset = terrainInfo.Id;
|
||||||
|
|
||||||
// Empty rules that can be added to by the importers.
|
// Empty rules that can be added to by the importers.
|
||||||
// Will be dropped on save if nothing is added to it
|
// Will be dropped on save if nothing is added to it
|
||||||
RuleDefinitions = new MiniYaml("");
|
RuleDefinitions = new MiniYaml("");
|
||||||
|
|
||||||
Tiles = new CellLayer<TerrainTile>(Grid.Type, size);
|
Tiles = new CellLayer<TerrainTile>(Grid.Type, MapSize);
|
||||||
Resources = new CellLayer<ResourceTile>(Grid.Type, size);
|
Resources = new CellLayer<ResourceTile>(Grid.Type, MapSize);
|
||||||
Height = new CellLayer<byte>(Grid.Type, size);
|
Height = new CellLayer<byte>(Grid.Type, MapSize);
|
||||||
Ramp = new CellLayer<byte>(Grid.Type, size);
|
Ramp = new CellLayer<byte>(Grid.Type, MapSize);
|
||||||
Tiles.Clear(terrainInfo.DefaultTerrainTile);
|
Tiles.Clear(terrainInfo.DefaultTerrainTile);
|
||||||
|
|
||||||
if (Grid.MaximumTerrainHeight > 0)
|
if (Grid.MaximumTerrainHeight > 0)
|
||||||
@@ -368,16 +367,15 @@ namespace OpenRA
|
|||||||
if (MapFormat < SupportedMapFormat)
|
if (MapFormat < SupportedMapFormat)
|
||||||
throw new InvalidDataException($"Map format {MapFormat} is not supported.\n File: {package.Name}");
|
throw new InvalidDataException($"Map format {MapFormat} is not supported.\n File: {package.Name}");
|
||||||
|
|
||||||
PlayerDefinitions = yaml.NodeWithKeyOrDefault("Players")?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty;
|
PlayerDefinitions = yaml.NodeWithKeyOrDefault("Players")?.Value.Nodes ?? [];
|
||||||
ActorDefinitions = yaml.NodeWithKeyOrDefault("Actors")?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty;
|
ActorDefinitions = yaml.NodeWithKeyOrDefault("Actors")?.Value.Nodes ?? [];
|
||||||
|
|
||||||
Grid = modData.Manifest.Get<MapGrid>();
|
Grid = modData.GetOrCreate<MapGrid>();
|
||||||
|
|
||||||
var size = new Size(MapSize.X, MapSize.Y);
|
Tiles = new CellLayer<TerrainTile>(Grid.Type, MapSize);
|
||||||
Tiles = new CellLayer<TerrainTile>(Grid.Type, size);
|
Resources = new CellLayer<ResourceTile>(Grid.Type, MapSize);
|
||||||
Resources = new CellLayer<ResourceTile>(Grid.Type, size);
|
Height = new CellLayer<byte>(Grid.Type, MapSize);
|
||||||
Height = new CellLayer<byte>(Grid.Type, size);
|
Ramp = new CellLayer<byte>(Grid.Type, MapSize);
|
||||||
Ramp = new CellLayer<byte>(Grid.Type, size);
|
|
||||||
|
|
||||||
using (var s = Package.GetStream("map.bin"))
|
using (var s = Package.GetStream("map.bin"))
|
||||||
{
|
{
|
||||||
@@ -385,9 +383,9 @@ namespace OpenRA
|
|||||||
if (header.TilesOffset > 0)
|
if (header.TilesOffset > 0)
|
||||||
{
|
{
|
||||||
s.Position = header.TilesOffset;
|
s.Position = header.TilesOffset;
|
||||||
for (var i = 0; i < MapSize.X; i++)
|
for (var i = 0; i < MapSize.Width; i++)
|
||||||
{
|
{
|
||||||
for (var j = 0; j < MapSize.Y; j++)
|
for (var j = 0; j < MapSize.Height; j++)
|
||||||
{
|
{
|
||||||
var tile = s.ReadUInt16();
|
var tile = s.ReadUInt16();
|
||||||
var index = s.ReadUInt8();
|
var index = s.ReadUInt8();
|
||||||
@@ -404,9 +402,9 @@ namespace OpenRA
|
|||||||
if (header.ResourcesOffset > 0)
|
if (header.ResourcesOffset > 0)
|
||||||
{
|
{
|
||||||
s.Position = header.ResourcesOffset;
|
s.Position = header.ResourcesOffset;
|
||||||
for (var i = 0; i < MapSize.X; i++)
|
for (var i = 0; i < MapSize.Width; i++)
|
||||||
{
|
{
|
||||||
for (var j = 0; j < MapSize.Y; j++)
|
for (var j = 0; j < MapSize.Height; j++)
|
||||||
{
|
{
|
||||||
var type = s.ReadUInt8();
|
var type = s.ReadUInt8();
|
||||||
var density = s.ReadUInt8();
|
var density = s.ReadUInt8();
|
||||||
@@ -418,8 +416,8 @@ namespace OpenRA
|
|||||||
if (header.HeightsOffset > 0)
|
if (header.HeightsOffset > 0)
|
||||||
{
|
{
|
||||||
s.Position = header.HeightsOffset;
|
s.Position = header.HeightsOffset;
|
||||||
for (var i = 0; i < MapSize.X; i++)
|
for (var i = 0; i < MapSize.Width; i++)
|
||||||
for (var j = 0; j < MapSize.Y; j++)
|
for (var j = 0; j < MapSize.Height; j++)
|
||||||
Height[new MPos(i, j)] = s.ReadUInt8().Clamp((byte)0, Grid.MaximumTerrainHeight);
|
Height[new MPos(i, j)] = s.ReadUInt8().Clamp((byte)0, Grid.MaximumTerrainHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -455,7 +453,7 @@ namespace OpenRA
|
|||||||
Sequences = new SequenceSet(this, modData, Tileset, SequenceDefinitions);
|
Sequences = new SequenceSet(this, modData, Tileset, SequenceDefinitions);
|
||||||
|
|
||||||
var tl = new MPos(0, 0).ToCPos(this);
|
var tl = new MPos(0, 0).ToCPos(this);
|
||||||
var br = new MPos(MapSize.X - 1, MapSize.Y - 1).ToCPos(this);
|
var br = new MPos(MapSize.Width - 1, MapSize.Height - 1).ToCPos(this);
|
||||||
AllCells = new CellRegion(Grid.Type, tl, br);
|
AllCells = new CellRegion(Grid.Type, tl, br);
|
||||||
|
|
||||||
var btl = new PPos(Bounds.Left, Bounds.Top);
|
var btl = new PPos(Bounds.Left, Bounds.Top);
|
||||||
@@ -516,7 +514,7 @@ namespace OpenRA
|
|||||||
foreach (var cell in AllCells)
|
foreach (var cell in AllCells)
|
||||||
{
|
{
|
||||||
var uv = cell.ToMPos(Grid.Type);
|
var uv = cell.ToMPos(Grid.Type);
|
||||||
cellProjection[uv] = Array.Empty<PPos>();
|
cellProjection[uv] = [];
|
||||||
inverseCellProjection[uv] = new List<MPos>(1);
|
inverseCellProjection[uv] = new List<MPos>(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,7 +530,7 @@ namespace OpenRA
|
|||||||
if (Grid.MaximumTerrainHeight == 0)
|
if (Grid.MaximumTerrainHeight == 0)
|
||||||
{
|
{
|
||||||
uv = cell.ToMPos(Grid.Type);
|
uv = cell.ToMPos(Grid.Type);
|
||||||
cellProjection[cell] = new[] { (PPos)uv };
|
cellProjection[cell] = [(PPos)uv];
|
||||||
var inverse = inverseCellProjection[uv];
|
var inverse = inverseCellProjection[uv];
|
||||||
inverse.Clear();
|
inverse.Clear();
|
||||||
inverse.Add(uv);
|
inverse.Add(uv);
|
||||||
@@ -607,7 +605,7 @@ namespace OpenRA
|
|||||||
// Any changes to this function should be reflected when setting projectionSafeBounds.
|
// Any changes to this function should be reflected when setting projectionSafeBounds.
|
||||||
var height = mapHeight[uv];
|
var height = mapHeight[uv];
|
||||||
if (height == 0)
|
if (height == 0)
|
||||||
return new[] { (PPos)uv };
|
return [(PPos)uv];
|
||||||
|
|
||||||
// Odd-height ramps get bumped up a level to the next even height layer
|
// Odd-height ramps get bumped up a level to the next even height layer
|
||||||
if ((height & 1) == 1 && Ramp[uv] != 0)
|
if ((height & 1) == 1 && Ramp[uv] != 0)
|
||||||
@@ -684,13 +682,13 @@ namespace OpenRA
|
|||||||
writer.Write(TileFormat);
|
writer.Write(TileFormat);
|
||||||
|
|
||||||
// Size
|
// Size
|
||||||
writer.Write((ushort)MapSize.X);
|
writer.Write((ushort)MapSize.Width);
|
||||||
writer.Write((ushort)MapSize.Y);
|
writer.Write((ushort)MapSize.Height);
|
||||||
|
|
||||||
// Data offsets
|
// Data offsets
|
||||||
const int TilesOffset = 17;
|
const int TilesOffset = 17;
|
||||||
var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.X * MapSize.Y + 17 : 0;
|
var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.Width * MapSize.Height + 17 : 0;
|
||||||
var resourcesOffset = (Grid.MaximumTerrainHeight > 0 ? 4 : 3) * MapSize.X * MapSize.Y + 17;
|
var resourcesOffset = (Grid.MaximumTerrainHeight > 0 ? 4 : 3) * MapSize.Width * MapSize.Height + 17;
|
||||||
|
|
||||||
writer.Write((uint)TilesOffset);
|
writer.Write((uint)TilesOffset);
|
||||||
writer.Write((uint)heightsOffset);
|
writer.Write((uint)heightsOffset);
|
||||||
@@ -699,9 +697,9 @@ namespace OpenRA
|
|||||||
// Tile data
|
// Tile data
|
||||||
if (TilesOffset != 0)
|
if (TilesOffset != 0)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < MapSize.X; i++)
|
for (var i = 0; i < MapSize.Width; i++)
|
||||||
{
|
{
|
||||||
for (var j = 0; j < MapSize.Y; j++)
|
for (var j = 0; j < MapSize.Height; j++)
|
||||||
{
|
{
|
||||||
var tile = Tiles[new MPos(i, j)];
|
var tile = Tiles[new MPos(i, j)];
|
||||||
writer.Write(tile.Type);
|
writer.Write(tile.Type);
|
||||||
@@ -712,16 +710,16 @@ namespace OpenRA
|
|||||||
|
|
||||||
// Height data
|
// Height data
|
||||||
if (heightsOffset != 0)
|
if (heightsOffset != 0)
|
||||||
for (var i = 0; i < MapSize.X; i++)
|
for (var i = 0; i < MapSize.Width; i++)
|
||||||
for (var j = 0; j < MapSize.Y; j++)
|
for (var j = 0; j < MapSize.Height; j++)
|
||||||
writer.Write(Height[new MPos(i, j)]);
|
writer.Write(Height[new MPos(i, j)]);
|
||||||
|
|
||||||
// Resource data
|
// Resource data
|
||||||
if (resourcesOffset != 0)
|
if (resourcesOffset != 0)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < MapSize.X; i++)
|
for (var i = 0; i < MapSize.Width; i++)
|
||||||
{
|
{
|
||||||
for (var j = 0; j < MapSize.Y; j++)
|
for (var j = 0; j < MapSize.Height; j++)
|
||||||
{
|
{
|
||||||
var tile = Resources[new MPos(i, j)];
|
var tile = Resources[new MPos(i, j)];
|
||||||
writer.Write(tile.Type);
|
writer.Write(tile.Type);
|
||||||
@@ -758,7 +756,7 @@ namespace OpenRA
|
|||||||
var positions = new List<(MPos Uv, Color Color)>();
|
var positions = new List<(MPos Uv, Color Color)>();
|
||||||
foreach (var actor in actors)
|
foreach (var actor in actors)
|
||||||
{
|
{
|
||||||
var s = new ActorReference(actor.Value.Value, actor.Value.ToDictionary());
|
var s = new ActorReference(actor.Value.Value, actor.Value);
|
||||||
|
|
||||||
var ai = Rules.Actors[actor.Value.Value];
|
var ai = Rules.Actors[actor.Value.Value];
|
||||||
var impsis = ai.TraitInfos<IMapPreviewSignatureInfo>();
|
var impsis = ai.TraitInfos<IMapPreviewSignatureInfo>();
|
||||||
@@ -1044,7 +1042,7 @@ namespace OpenRA
|
|||||||
return (PPos)CellContaining(projectedPos).ToMPos(Grid.Type);
|
return (PPos)CellContaining(projectedPos).ToMPos(Grid.Type);
|
||||||
}
|
}
|
||||||
|
|
||||||
static readonly PPos[] NoProjectedCells = Array.Empty<PPos>();
|
static readonly PPos[] NoProjectedCells = [];
|
||||||
public PPos[] ProjectedCellsCovering(MPos uv)
|
public PPos[] ProjectedCellsCovering(MPos uv)
|
||||||
{
|
{
|
||||||
if (!initializedCellProjection)
|
if (!initializedCellProjection)
|
||||||
@@ -1064,7 +1062,7 @@ namespace OpenRA
|
|||||||
InitializeCellProjection();
|
InitializeCellProjection();
|
||||||
|
|
||||||
if (!inverseCellProjection.Contains(uv))
|
if (!inverseCellProjection.Contains(uv))
|
||||||
return new List<MPos>();
|
return [];
|
||||||
|
|
||||||
return inverseCellProjection[uv];
|
return inverseCellProjection[uv];
|
||||||
}
|
}
|
||||||
@@ -1089,16 +1087,15 @@ namespace OpenRA
|
|||||||
var oldMapResources = Resources;
|
var oldMapResources = Resources;
|
||||||
var oldMapHeight = Height;
|
var oldMapHeight = Height;
|
||||||
var oldMapRamp = Ramp;
|
var oldMapRamp = Ramp;
|
||||||
var newSize = new Size(width, height);
|
|
||||||
|
|
||||||
Tiles = CellLayer.Resize(oldMapTiles, newSize, oldMapTiles[MPos.Zero]);
|
MapSize = new Size(width, height);
|
||||||
Resources = CellLayer.Resize(oldMapResources, newSize, oldMapResources[MPos.Zero]);
|
Tiles = CellLayer.Resize(oldMapTiles, MapSize, oldMapTiles[MPos.Zero]);
|
||||||
Height = CellLayer.Resize(oldMapHeight, newSize, oldMapHeight[MPos.Zero]);
|
Resources = CellLayer.Resize(oldMapResources, MapSize, oldMapResources[MPos.Zero]);
|
||||||
Ramp = CellLayer.Resize(oldMapRamp, newSize, oldMapRamp[MPos.Zero]);
|
Height = CellLayer.Resize(oldMapHeight, MapSize, oldMapHeight[MPos.Zero]);
|
||||||
MapSize = new int2(newSize);
|
Ramp = CellLayer.Resize(oldMapRamp, MapSize, oldMapRamp[MPos.Zero]);
|
||||||
|
|
||||||
var tl = new MPos(0, 0);
|
var tl = new MPos(0, 0);
|
||||||
var br = new MPos(MapSize.X - 1, MapSize.Y - 1);
|
var br = new MPos(MapSize.Width - 1, MapSize.Height - 1);
|
||||||
AllCells = new CellRegion(Grid.Type, tl.ToCPos(this), br.ToCPos(this));
|
AllCells = new CellRegion(Grid.Type, tl.ToCPos(this), br.ToCPos(this));
|
||||||
SetBounds(new PPos(tl.U + 1, tl.V + 1), new PPos(br.U - 1, br.V - 1));
|
SetBounds(new PPos(tl.U + 1, tl.V + 1), new PPos(br.U - 1, br.V - 1));
|
||||||
}
|
}
|
||||||
@@ -1145,6 +1142,11 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
|
|
||||||
public byte GetTerrainIndex(CPos cell)
|
public byte GetTerrainIndex(CPos cell)
|
||||||
|
{
|
||||||
|
return GetTerrainIndex(cell.ToMPos(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte GetTerrainIndex(MPos uv)
|
||||||
{
|
{
|
||||||
// Lazily initialize a cache for terrain indexes.
|
// Lazily initialize a cache for terrain indexes.
|
||||||
if (cachedTerrainIndexes == null)
|
if (cachedTerrainIndexes == null)
|
||||||
@@ -1153,7 +1155,6 @@ namespace OpenRA
|
|||||||
cachedTerrainIndexes.Clear(InvalidCachedTerrainIndex);
|
cachedTerrainIndexes.Clear(InvalidCachedTerrainIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
var uv = cell.ToMPos(this);
|
|
||||||
var terrainIndex = cachedTerrainIndexes[uv];
|
var terrainIndex = cachedTerrainIndexes[uv];
|
||||||
|
|
||||||
// PERF: Cache terrain indexes per cell on demand.
|
// PERF: Cache terrain indexes per cell on demand.
|
||||||
@@ -1168,7 +1169,12 @@ namespace OpenRA
|
|||||||
|
|
||||||
public TerrainTypeInfo GetTerrainInfo(CPos cell)
|
public TerrainTypeInfo GetTerrainInfo(CPos cell)
|
||||||
{
|
{
|
||||||
return Rules.TerrainInfo.TerrainTypes[GetTerrainIndex(cell)];
|
return GetTerrainInfo(cell.ToMPos(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public TerrainTypeInfo GetTerrainInfo(MPos uv)
|
||||||
|
{
|
||||||
|
return Rules.TerrainInfo.TerrainTypes[GetTerrainIndex(uv)];
|
||||||
}
|
}
|
||||||
|
|
||||||
public CPos Clamp(CPos cell)
|
public CPos Clamp(CPos cell)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ using OpenRA.FileSystem;
|
|||||||
using OpenRA.Graphics;
|
using OpenRA.Graphics;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
using OpenRA.Support;
|
using OpenRA.Support;
|
||||||
|
using FS = OpenRA.FileSystem.FileSystem;
|
||||||
|
|
||||||
namespace OpenRA
|
namespace OpenRA
|
||||||
{
|
{
|
||||||
@@ -27,26 +28,27 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
public static readonly MapPreview UnknownMap = new(null, null, MapGridType.Rectangular, null);
|
public static readonly MapPreview UnknownMap = new(null, null, MapGridType.Rectangular, null);
|
||||||
public IReadOnlyDictionary<IReadOnlyPackage, MapClassification> MapLocations => mapLocations;
|
public IReadOnlyDictionary<IReadOnlyPackage, MapClassification> MapLocations => mapLocations;
|
||||||
readonly Dictionary<IReadOnlyPackage, MapClassification> mapLocations = new();
|
readonly Dictionary<IReadOnlyPackage, MapClassification> mapLocations = [];
|
||||||
public bool LoadPreviewImages = true;
|
public bool LoadPreviewImages = true;
|
||||||
|
|
||||||
readonly Cache<string, MapPreview> previews;
|
readonly Manifest manifest;
|
||||||
readonly ModData modData;
|
readonly FS modFiles;
|
||||||
|
Cache<string, MapPreview> previews;
|
||||||
readonly SheetBuilder sheetBuilder;
|
readonly SheetBuilder sheetBuilder;
|
||||||
Thread previewLoaderThread;
|
Thread previewLoaderThread;
|
||||||
bool previewLoaderThreadShutDown = true;
|
bool previewLoaderThreadShutDown = true;
|
||||||
readonly object syncRoot = new();
|
readonly object syncRoot = new();
|
||||||
readonly Queue<MapPreview> generateMinimap = new();
|
readonly Queue<MapPreview> generateMinimap = [];
|
||||||
|
|
||||||
public HashSet<string> StringPool { get; } = new();
|
public HashSet<string> StringPool { get; } = [];
|
||||||
|
|
||||||
readonly List<MapDirectoryTracker> mapDirectoryTrackers = new();
|
readonly List<MapDirectoryTracker> mapDirectoryTrackers = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The most recently modified or loaded map at runtime.
|
/// The most recently modified or loaded map at runtime.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string LastModifiedMap { get; private set; } = null;
|
public string LastModifiedMap { get; private set; } = null;
|
||||||
readonly Dictionary<string, string> mapUpdates = new();
|
readonly Dictionary<string, string> mapUpdates = [];
|
||||||
|
|
||||||
string lastLoadedLastModifiedMap;
|
string lastLoadedLastModifiedMap;
|
||||||
|
|
||||||
@@ -66,13 +68,11 @@ namespace OpenRA
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MapCache(ModData modData)
|
public MapCache(Manifest manifest, FS modFiles)
|
||||||
{
|
{
|
||||||
this.modData = modData;
|
this.manifest = manifest;
|
||||||
|
this.modFiles = modFiles;
|
||||||
var gridType = Exts.Lazy(() => modData.Manifest.Get<MapGrid>().Type);
|
sheetBuilder = new SheetBuilder(SheetType.BGRA, manifest.RendererConstants.MapPreviewSheetSize);
|
||||||
previews = new Cache<string, MapPreview>(uid => new MapPreview(modData, uid, gridType.Value, this));
|
|
||||||
sheetBuilder = new SheetBuilder(SheetType.BGRA);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateMaps()
|
public void UpdateMaps()
|
||||||
@@ -81,20 +81,21 @@ namespace OpenRA
|
|||||||
tracker.UpdateMaps(this);
|
tracker.UpdateMaps(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadMaps()
|
public void LoadMaps(ModData modData)
|
||||||
{
|
{
|
||||||
// Utility mod that does not support maps
|
// Utility mod that does not support maps
|
||||||
if (!modData.Manifest.Contains<MapGrid>())
|
if (manifest.MapFolders.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var mapGrid = modData.Manifest.Get<MapGrid>();
|
var gridType = modData.GetOrCreate<MapGrid>().Type;
|
||||||
|
previews = new Cache<string, MapPreview>(uid => new MapPreview(modData, uid, gridType, this));
|
||||||
|
|
||||||
// Enumerate map directories
|
// Enumerate map directories
|
||||||
foreach (var kv in modData.Manifest.MapFolders)
|
foreach (var kv in manifest.MapFolders)
|
||||||
{
|
{
|
||||||
var name = kv.Key;
|
var name = kv.Key;
|
||||||
var classification = string.IsNullOrEmpty(kv.Value)
|
var classification = string.IsNullOrEmpty(kv.Value)
|
||||||
? MapClassification.Unknown : Enum<MapClassification>.Parse(kv.Value);
|
? MapClassification.Unknown : Enum.Parse<MapClassification>(kv.Value);
|
||||||
|
|
||||||
IReadOnlyPackage package;
|
IReadOnlyPackage package;
|
||||||
var optional = name.StartsWith('~');
|
var optional = name.StartsWith('~');
|
||||||
@@ -109,7 +110,7 @@ namespace OpenRA
|
|||||||
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && !File.Exists(resolved))
|
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && !File.Exists(resolved))
|
||||||
Directory.CreateDirectory(resolved);
|
Directory.CreateDirectory(resolved);
|
||||||
|
|
||||||
package = modData.ModFiles.OpenPackage(name);
|
package = modFiles.OpenPackage(name);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -120,42 +121,38 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
|
|
||||||
mapLocations.Add(package, classification);
|
mapLocations.Add(package, classification);
|
||||||
mapDirectoryTrackers.Add(new MapDirectoryTracker(mapGrid, package, classification));
|
mapDirectoryTrackers.Add(new MapDirectoryTracker(package, classification));
|
||||||
}
|
}
|
||||||
|
|
||||||
// PERF: Load the mod YAML once outside the loop, and reuse it when resolving each maps custom YAML.
|
// PERF: Load the mod YAML once outside the loop, and reuse it when resolving each maps custom YAML.
|
||||||
var modDataRules = modData.GetRulesYaml();
|
var modDataRules = modData.GetRulesYaml();
|
||||||
foreach (var kv in MapLocations)
|
foreach (var kv in MapLocations)
|
||||||
{
|
|
||||||
foreach (var map in kv.Key.Contents)
|
foreach (var map in kv.Key.Contents)
|
||||||
LoadMapInternal(map, kv.Key, kv.Value, mapGrid, null, modDataRules);
|
LoadMapInternal(map, kv.Key, kv.Value, null, gridType, modDataRules);
|
||||||
}
|
|
||||||
|
|
||||||
// We only want to track maps in runtime, not at loadtime
|
// We only want to track maps in runtime, not at loadtime
|
||||||
LastModifiedMap = null;
|
LastModifiedMap = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadMap(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap)
|
public void LoadMap(string map, IReadOnlyPackage package, MapClassification classification, string oldMap)
|
||||||
{
|
{
|
||||||
LoadMapInternal(map, package, classification, mapGrid, oldMap, null);
|
LoadMapInternal(map, package, classification, oldMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap,
|
void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, string oldMap,
|
||||||
IEnumerable<List<MiniYamlNode>> modDataRules)
|
MapGridType? gridType = null, MiniYamlNode[][] modDataRules = null)
|
||||||
{
|
{
|
||||||
IReadOnlyPackage mapPackage = null;
|
IReadOnlyPackage mapPackage = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (new PerfTimer(map))
|
using (new PerfTimer(map))
|
||||||
{
|
{
|
||||||
mapPackage = package.OpenPackage(map, modData.ModFiles);
|
mapPackage = package.OpenPackage(map, modFiles);
|
||||||
if (mapPackage != null)
|
if (mapPackage != null)
|
||||||
{
|
{
|
||||||
var uid = Map.ComputeUID(mapPackage);
|
var uid = Map.ComputeUID(mapPackage);
|
||||||
previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type, modDataRules);
|
previews[uid].UpdateFromMapWithoutOwningPackage(mapPackage, package, classification, gridType, modDataRules);
|
||||||
|
mapPackage.Dispose();
|
||||||
// Freeing the package to save memory if there is a lot of Maps
|
|
||||||
previews[uid].DisposePackage();
|
|
||||||
|
|
||||||
if (oldMap != uid)
|
if (oldMap != uid)
|
||||||
{
|
{
|
||||||
@@ -180,12 +177,8 @@ namespace OpenRA
|
|||||||
|
|
||||||
public IEnumerable<IReadWritePackage> EnumerateMapDirPackages(MapClassification classification = MapClassification.System)
|
public IEnumerable<IReadWritePackage> EnumerateMapDirPackages(MapClassification classification = MapClassification.System)
|
||||||
{
|
{
|
||||||
// Utility mod that does not support maps
|
|
||||||
if (!modData.Manifest.Contains<MapGrid>())
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
// Enumerate map directories
|
// Enumerate map directories
|
||||||
foreach (var kv in modData.Manifest.MapFolders)
|
foreach (var kv in manifest.MapFolders)
|
||||||
{
|
{
|
||||||
if (!Enum.TryParse(kv.Value, out MapClassification packageClassification))
|
if (!Enum.TryParse(kv.Value, out MapClassification packageClassification))
|
||||||
continue;
|
continue;
|
||||||
@@ -203,7 +196,7 @@ namespace OpenRA
|
|||||||
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && (!Directory.Exists(resolved) || !File.Exists(resolved)))
|
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && (!Directory.Exists(resolved) || !File.Exists(resolved)))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
using (var package = (IReadWritePackage)modData.ModFiles.OpenPackage(name))
|
using (var package = (IReadWritePackage)modFiles.OpenPackage(name))
|
||||||
yield return package;
|
yield return package;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,7 +216,7 @@ namespace OpenRA
|
|||||||
|
|
||||||
foreach (var mapDirPackage in mapDirPackages)
|
foreach (var mapDirPackage in mapDirPackages)
|
||||||
foreach (var map in mapDirPackage.Contents)
|
foreach (var map in mapDirPackage.Contents)
|
||||||
if (mapDirPackage.OpenPackage(map, modData.ModFiles) is IReadWritePackage mapPackage)
|
if (mapDirPackage.OpenPackage(map, modFiles) is IReadWritePackage mapPackage)
|
||||||
yield return mapPackage;
|
yield return mapPackage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +231,7 @@ namespace OpenRA
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var uid in queryUids)
|
foreach (var uid in queryUids)
|
||||||
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null, null);
|
previews[uid].BeginRemoteSearch();
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
@@ -246,37 +239,29 @@ namespace OpenRA
|
|||||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||||
|
|
||||||
// Limit each query to 50 maps at a time to avoid request size limits
|
// Limit each query to 50 maps at a time to avoid request size limits
|
||||||
for (var i = 0; i < queryUids.Count; i += 50)
|
foreach (var batchUids in queryUids.Chunk(50))
|
||||||
{
|
{
|
||||||
var batchUids = queryUids.Skip(i).Take(50).ToList();
|
|
||||||
var url = repositoryUrl + "hash/" + string.Join(",", batchUids) + "/yaml";
|
var url = repositoryUrl + "hash/" + string.Join(",", batchUids) + "/yaml";
|
||||||
|
using (new PerfTimer("RemoteMapDetails"))
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var httpResponseMessage = await client.GetAsync(url);
|
var result = await client.GetStreamAsync(url);
|
||||||
var result = await httpResponseMessage.Content.ReadAsStreamAsync();
|
foreach (var kv in MiniYaml.FromStream(result, url, stringPool: stringPool))
|
||||||
|
previews[kv.Key].CompleteRemoteSearch(kv.Value, mapDetailsReceived);
|
||||||
var yaml = MiniYaml.FromStream(result, url, stringPool: stringPool);
|
|
||||||
foreach (var kv in yaml)
|
|
||||||
previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, modData.Manifest.MapCompatibility, mapDetailsReceived);
|
|
||||||
|
|
||||||
foreach (var uid in batchUids)
|
|
||||||
{
|
|
||||||
var p = previews[uid];
|
|
||||||
if (p.Status != MapStatus.DownloadAvailable)
|
|
||||||
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.Write("debug", "Remote map query failed with error:");
|
Log.Write("debug", "Remote map query failed with error:");
|
||||||
Log.Write("debug", e);
|
Log.Write("debug", e);
|
||||||
Log.Write("debug", $"URL was: {url}");
|
Log.Write("debug", $"URL was: {url}");
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var uid in batchUids)
|
foreach (var uid in batchUids)
|
||||||
{
|
{
|
||||||
var p = previews[uid];
|
var p = previews[uid];
|
||||||
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null);
|
if (p.Status == MapStatus.Searching)
|
||||||
mapQueryFailed?.Invoke(p);
|
p.CompleteRemoteSearch(null, mapQueryFailed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,7 +328,9 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Release the buffer by forcing changes to be written out to the texture, allowing the buffer to be reclaimed by GC.
|
// Release the buffer by forcing changes to be written out to the texture, allowing the buffer to be reclaimed by GC.
|
||||||
|
if (sheetBuilder.Current != null)
|
||||||
Game.RunAfterTick(sheetBuilder.Current.ReleaseBuffer);
|
Game.RunAfterTick(sheetBuilder.Current.ReleaseBuffer);
|
||||||
|
|
||||||
Log.Write("debug", "MapCache.LoadAsyncInternal ended");
|
Log.Write("debug", "MapCache.LoadAsyncInternal ended");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,26 +20,24 @@ namespace OpenRA
|
|||||||
public sealed class MapDirectoryTracker : IDisposable
|
public sealed class MapDirectoryTracker : IDisposable
|
||||||
{
|
{
|
||||||
readonly FileSystemWatcher watcher;
|
readonly FileSystemWatcher watcher;
|
||||||
readonly MapGrid mapGrid;
|
|
||||||
readonly IReadOnlyPackage package;
|
readonly IReadOnlyPackage package;
|
||||||
readonly MapClassification classification;
|
readonly MapClassification classification;
|
||||||
|
|
||||||
enum MapAction { Add, Delete, Update }
|
enum MapAction { Add, Delete, Update }
|
||||||
readonly Dictionary<string, MapAction> mapActionQueue = new();
|
readonly Dictionary<string, MapAction> mapActionQueue = [];
|
||||||
|
|
||||||
bool dirty = false;
|
bool dirty = false;
|
||||||
|
|
||||||
public MapDirectoryTracker(MapGrid mapGrid, IReadOnlyPackage package, MapClassification classification)
|
public MapDirectoryTracker(IReadOnlyPackage package, MapClassification classification)
|
||||||
{
|
{
|
||||||
this.mapGrid = mapGrid;
|
|
||||||
this.package = package;
|
this.package = package;
|
||||||
this.classification = classification;
|
this.classification = classification;
|
||||||
|
|
||||||
watcher = new FileSystemWatcher(package.Name);
|
watcher = new FileSystemWatcher(package.Name);
|
||||||
watcher.Changed += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Update, e.FullPath);
|
watcher.Changed += (_, e) => AddMapAction(MapAction.Update, e.FullPath);
|
||||||
watcher.Created += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Add, e.FullPath);
|
watcher.Created += (_, e) => AddMapAction(MapAction.Add, e.FullPath);
|
||||||
watcher.Deleted += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Delete, e.FullPath);
|
watcher.Deleted += (_, e) => AddMapAction(MapAction.Delete, e.FullPath);
|
||||||
watcher.Renamed += (object sender, RenamedEventArgs e) => AddMapAction(MapAction.Add, e.FullPath, e.OldFullPath);
|
watcher.Renamed += (_, e) => AddMapAction(MapAction.Add, e.FullPath, e.OldFullPath);
|
||||||
|
|
||||||
watcher.IncludeSubdirectories = true;
|
watcher.IncludeSubdirectories = true;
|
||||||
watcher.EnableRaisingEvents = true;
|
watcher.EnableRaisingEvents = true;
|
||||||
@@ -86,7 +84,7 @@ namespace OpenRA
|
|||||||
dirty = false;
|
dirty = false;
|
||||||
foreach (var mapAction in mapActionQueue)
|
foreach (var mapAction in mapActionQueue)
|
||||||
{
|
{
|
||||||
var map = mapcache.FirstOrDefault(x => x.PackageName == mapAction.Key && x.Status == MapStatus.Available);
|
var map = mapcache.FirstOrDefault(x => x.Path == mapAction.Key && x.Status == MapStatus.Available);
|
||||||
if (map != null)
|
if (map != null)
|
||||||
{
|
{
|
||||||
if (mapAction.Value == MapAction.Delete)
|
if (mapAction.Value == MapAction.Delete)
|
||||||
@@ -98,7 +96,7 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
Console.WriteLine(mapAction.Key + " was updated");
|
Console.WriteLine(mapAction.Key + " was updated");
|
||||||
map.Invalidate();
|
map.Invalidate();
|
||||||
mapcache.LoadMap(mapAction.Key.Replace(package.Name + Path.DirectorySeparatorChar, ""), package, classification, mapGrid, map.Uid);
|
mapcache.LoadMap(mapAction.Key.Replace(package.Name + Path.DirectorySeparatorChar, ""), package, classification, map.Uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -106,7 +104,7 @@ namespace OpenRA
|
|||||||
if (mapAction.Value != MapAction.Delete)
|
if (mapAction.Value != MapAction.Delete)
|
||||||
{
|
{
|
||||||
Console.WriteLine(mapAction.Key + " was added");
|
Console.WriteLine(mapAction.Key + " was added");
|
||||||
mapcache.LoadMap(mapAction.Key.Replace(package?.Name + Path.DirectorySeparatorChar, ""), package, classification, mapGrid, null);
|
mapcache.LoadMap(mapAction.Key.Replace(package?.Name + Path.DirectorySeparatorChar, ""), package, classification, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
OpenRA.Game/Map/MapGenerationArgs.cs
Normal file
58
OpenRA.Game/Map/MapGenerationArgs.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#region Copyright & License Information
|
||||||
|
/*
|
||||||
|
* Copyright (c) The OpenRA Developers and Contributors
|
||||||
|
* This file is part of OpenRA, which is free software. It is made
|
||||||
|
* available to you under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation, either version 3 of
|
||||||
|
* the License, or (at your option) any later version. For more
|
||||||
|
* information, see COPYING.
|
||||||
|
*/
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenRA.Primitives;
|
||||||
|
|
||||||
|
namespace OpenRA
|
||||||
|
{
|
||||||
|
public class MapGenerationArgs
|
||||||
|
{
|
||||||
|
[FieldLoader.Require]
|
||||||
|
public string Uid = null;
|
||||||
|
|
||||||
|
[FieldLoader.Require]
|
||||||
|
public string Generator = null;
|
||||||
|
|
||||||
|
[FieldLoader.Require]
|
||||||
|
public string Tileset = null;
|
||||||
|
|
||||||
|
[FieldLoader.Require]
|
||||||
|
public Size Size = default;
|
||||||
|
|
||||||
|
[FieldLoader.Require]
|
||||||
|
public string Title = null;
|
||||||
|
|
||||||
|
[FieldLoader.Require]
|
||||||
|
public string Author = null;
|
||||||
|
|
||||||
|
[FieldLoader.LoadUsing(nameof(LoadSettings))]
|
||||||
|
public MiniYaml Settings = null;
|
||||||
|
static MiniYaml LoadSettings(MiniYaml yaml)
|
||||||
|
{
|
||||||
|
return yaml.NodeWithKey("Settings").Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MiniYamlNode> Serialize()
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new("Uid", Uid),
|
||||||
|
new("Generator", Generator),
|
||||||
|
new("Tileset", Tileset),
|
||||||
|
new("Size", FieldSaver.FormatValue(Size)),
|
||||||
|
new("Settings", Settings),
|
||||||
|
new("Title", Title),
|
||||||
|
new("Author", Author)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,9 +10,9 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenRA.Primitives;
|
|
||||||
using OpenRA.Traits;
|
using OpenRA.Traits;
|
||||||
|
|
||||||
namespace OpenRA
|
namespace OpenRA
|
||||||
@@ -25,8 +25,8 @@ namespace OpenRA
|
|||||||
public readonly struct CellRamp
|
public readonly struct CellRamp
|
||||||
{
|
{
|
||||||
public readonly int CenterHeightOffset;
|
public readonly int CenterHeightOffset;
|
||||||
public readonly WVec[] Corners;
|
public readonly ImmutableArray<WVec> Corners;
|
||||||
public readonly WVec[][] Polygons;
|
public readonly ImmutableArray<ImmutableArray<WVec>> Polygons;
|
||||||
public readonly WRot Orientation;
|
public readonly WRot Orientation;
|
||||||
|
|
||||||
public CellRamp(MapGridType type, WRot orientation,
|
public CellRamp(MapGridType type, WRot orientation,
|
||||||
@@ -37,43 +37,43 @@ namespace OpenRA
|
|||||||
Orientation = orientation;
|
Orientation = orientation;
|
||||||
if (type == MapGridType.RectangularIsometric)
|
if (type == MapGridType.RectangularIsometric)
|
||||||
{
|
{
|
||||||
Corners = new[]
|
Corners =
|
||||||
{
|
[
|
||||||
new WVec(0, -724, 724 * (int)tl),
|
new WVec(0, -724, 724 * (int)tl),
|
||||||
new WVec(724, 0, 724 * (int)tr),
|
new WVec(724, 0, 724 * (int)tr),
|
||||||
new WVec(0, 724, 724 * (int)br),
|
new WVec(0, 724, 724 * (int)br),
|
||||||
new WVec(-724, 0, 724 * (int)bl),
|
new WVec(-724, 0, 724 * (int)bl),
|
||||||
};
|
];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Corners = new[]
|
Corners =
|
||||||
{
|
[
|
||||||
new WVec(-512, -512, 512 * (int)tl),
|
new WVec(-512, -512, 512 * (int)tl),
|
||||||
new WVec(512, -512, 512 * (int)tr),
|
new WVec(512, -512, 512 * (int)tr),
|
||||||
new WVec(512, 512, 512 * (int)br),
|
new WVec(512, 512, 512 * (int)br),
|
||||||
new WVec(-512, 512, 512 * (int)bl)
|
new WVec(-512, 512, 512 * (int)bl)
|
||||||
};
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (split == RampSplit.X)
|
if (split == RampSplit.X)
|
||||||
{
|
{
|
||||||
Polygons = new[]
|
Polygons =
|
||||||
{
|
[
|
||||||
new[] { Corners[0], Corners[1], Corners[3] },
|
[Corners[0], Corners[1], Corners[3]],
|
||||||
new[] { Corners[1], Corners[2], Corners[3] }
|
[Corners[1], Corners[2], Corners[3]]
|
||||||
};
|
];
|
||||||
}
|
}
|
||||||
else if (split == RampSplit.Y)
|
else if (split == RampSplit.Y)
|
||||||
{
|
{
|
||||||
Polygons = new[]
|
Polygons =
|
||||||
{
|
[
|
||||||
new[] { Corners[0], Corners[1], Corners[2] },
|
[Corners[0], Corners[1], Corners[2]],
|
||||||
new[] { Corners[0], Corners[2], Corners[3] }
|
[Corners[0], Corners[2], Corners[3]]
|
||||||
};
|
];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
Polygons = new[] { Corners };
|
Polygons = [Corners];
|
||||||
|
|
||||||
// Initial value must be assigned before HeightOffset can be called
|
// Initial value must be assigned before HeightOffset can be called
|
||||||
CenterHeightOffset = 0;
|
CenterHeightOffset = 0;
|
||||||
@@ -84,10 +84,11 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
// Enumerate over the polygons, assuming that they are triangles
|
// Enumerate over the polygons, assuming that they are triangles
|
||||||
// If the ramp is not split we will take the first three vertices of the corners as a valid triangle
|
// If the ramp is not split we will take the first three vertices of the corners as a valid triangle
|
||||||
WVec[] p = null;
|
int u;
|
||||||
var u = 0;
|
int v;
|
||||||
var v = 0;
|
ImmutableArray<WVec> p;
|
||||||
for (var i = 0; i < Polygons.Length; i++)
|
var i = 0;
|
||||||
|
do
|
||||||
{
|
{
|
||||||
p = Polygons[i];
|
p = Polygons[i];
|
||||||
u = ((p[1].Y - p[2].Y) * (dX - p[2].X) - (p[1].X - p[2].X) * (dY - p[2].Y)) / 1024;
|
u = ((p[1].Y - p[2].Y) * (dX - p[2].X) - (p[1].X - p[2].X) * (dY - p[2].Y)) / 1024;
|
||||||
@@ -96,7 +97,10 @@ namespace OpenRA
|
|||||||
// Point is within the triangle if 0 <= u,v <= 1024
|
// Point is within the triangle if 0 <= u,v <= 1024
|
||||||
if (u >= 0 && u <= 1024 && v >= 0 && v <= 1024)
|
if (u >= 0 && u <= 1024 && v >= 0 && v <= 1024)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
while (i < Polygons.Length);
|
||||||
|
|
||||||
// Calculate w from u,v and interpolate height
|
// Calculate w from u,v and interpolate height
|
||||||
return (u * p[0].Z + v * p[1].Z + (1024 - u - v) * p[2].Z) / 1024;
|
return (u * p[0].Z + v * p[1].Z + (1024 - u - v) * p[2].Z) / 1024;
|
||||||
@@ -106,7 +110,6 @@ namespace OpenRA
|
|||||||
public class MapGrid : IGlobalModData
|
public class MapGrid : IGlobalModData
|
||||||
{
|
{
|
||||||
public readonly MapGridType Type = MapGridType.Rectangular;
|
public readonly MapGridType Type = MapGridType.Rectangular;
|
||||||
public readonly Size TileSize = new(24, 24);
|
|
||||||
public readonly byte MaximumTerrainHeight = 0;
|
public readonly byte MaximumTerrainHeight = 0;
|
||||||
public readonly SubCell DefaultSubCell = (SubCell)byte.MaxValue;
|
public readonly SubCell DefaultSubCell = (SubCell)byte.MaxValue;
|
||||||
|
|
||||||
@@ -114,19 +117,19 @@ namespace OpenRA
|
|||||||
|
|
||||||
public readonly bool EnableDepthBuffer = false;
|
public readonly bool EnableDepthBuffer = false;
|
||||||
|
|
||||||
public readonly WVec[] SubCellOffsets =
|
public readonly ImmutableArray<WVec> SubCellOffsets =
|
||||||
{
|
[
|
||||||
new(0, 0, 0), // full cell - index 0
|
new(0, 0, 0), // full cell - index 0
|
||||||
new(-299, -256, 0), // top left - index 1
|
new(-299, -256, 0), // top left - index 1
|
||||||
new(256, -256, 0), // top right - index 2
|
new(256, -256, 0), // top right - index 2
|
||||||
new(0, 0, 0), // center - index 3
|
new(0, 0, 0), // center - index 3
|
||||||
new(-299, 256, 0), // bottom left - index 4
|
new(-299, 256, 0), // bottom left - index 4
|
||||||
new(256, 256, 0), // bottom right - index 5
|
new(256, 256, 0), // bottom right - index 5
|
||||||
};
|
];
|
||||||
|
|
||||||
public CellRamp[] Ramps { get; }
|
public ImmutableArray<CellRamp> Ramps { get; }
|
||||||
|
|
||||||
internal readonly CVec[][] TilesByDistance;
|
internal readonly ImmutableArray<ImmutableArray<CVec>> TilesByDistance;
|
||||||
|
|
||||||
public int TileScale { get; }
|
public int TileScale { get; }
|
||||||
|
|
||||||
@@ -159,8 +162,9 @@ namespace OpenRA
|
|||||||
var halfBackward = -halfForward;
|
var halfBackward = -halfForward;
|
||||||
|
|
||||||
// Slope types are hardcoded following the convention from the TS and RA2 map format
|
// Slope types are hardcoded following the convention from the TS and RA2 map format
|
||||||
Ramps = new[]
|
Ramps =
|
||||||
{
|
[
|
||||||
|
|
||||||
// Flat
|
// Flat
|
||||||
new CellRamp(Type, WRot.None),
|
new CellRamp(Type, WRot.None),
|
||||||
|
|
||||||
@@ -193,16 +197,16 @@ namespace OpenRA
|
|||||||
new CellRamp(Type, WRot.None, tl: RampCornerHeight.Half, br: RampCornerHeight.Half, split: RampSplit.Y),
|
new CellRamp(Type, WRot.None, tl: RampCornerHeight.Half, br: RampCornerHeight.Half, split: RampSplit.Y),
|
||||||
new CellRamp(Type, WRot.None, tr: RampCornerHeight.Half, bl: RampCornerHeight.Half, split: RampSplit.X),
|
new CellRamp(Type, WRot.None, tr: RampCornerHeight.Half, bl: RampCornerHeight.Half, split: RampSplit.X),
|
||||||
new CellRamp(Type, WRot.None, tl: RampCornerHeight.Half, br: RampCornerHeight.Half, split: RampSplit.X),
|
new CellRamp(Type, WRot.None, tl: RampCornerHeight.Half, br: RampCornerHeight.Half, split: RampSplit.X),
|
||||||
};
|
];
|
||||||
|
|
||||||
TilesByDistance = CreateTilesByDistance();
|
TilesByDistance = CreateTilesByDistance();
|
||||||
}
|
}
|
||||||
|
|
||||||
CVec[][] CreateTilesByDistance()
|
ImmutableArray<ImmutableArray<CVec>> CreateTilesByDistance()
|
||||||
{
|
{
|
||||||
var ts = new List<CVec>[MaximumTileSearchRange + 1];
|
var ts = new List<CVec>[MaximumTileSearchRange + 1];
|
||||||
for (var i = 0; i < MaximumTileSearchRange + 1; i++)
|
for (var i = 0; i < MaximumTileSearchRange + 1; i++)
|
||||||
ts[i] = new List<CVec>();
|
ts[i] = [];
|
||||||
|
|
||||||
for (var j = -MaximumTileSearchRange; j <= MaximumTileSearchRange; j++)
|
for (var j = -MaximumTileSearchRange; j <= MaximumTileSearchRange; j++)
|
||||||
for (var i = -MaximumTileSearchRange; i <= MaximumTileSearchRange; i++)
|
for (var i = -MaximumTileSearchRange; i <= MaximumTileSearchRange; i++)
|
||||||
@@ -234,7 +238,7 @@ namespace OpenRA
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return ts.Select(list => list.ToArray()).ToArray();
|
return ts.Select(list => list.ToImmutableArray()).ToImmutableArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public WVec OffsetOfSubCell(SubCell subCell)
|
public WVec OffsetOfSubCell(SubCell subCell)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenRA.Traits;
|
using OpenRA.Traits;
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ namespace OpenRA
|
|||||||
public readonly Dictionary<string, PlayerReference> Players;
|
public readonly Dictionary<string, PlayerReference> Players;
|
||||||
|
|
||||||
public MapPlayers()
|
public MapPlayers()
|
||||||
: this(new List<MiniYamlNode>()) { }
|
: this([]) { }
|
||||||
|
|
||||||
public MapPlayers(IEnumerable<MiniYamlNode> playerDefinitions)
|
public MapPlayers(IEnumerable<MiniYamlNode> playerDefinitions)
|
||||||
{
|
{
|
||||||
@@ -54,7 +55,7 @@ namespace OpenRA
|
|||||||
Name = "Creeps",
|
Name = "Creeps",
|
||||||
Faction = firstFaction,
|
Faction = firstFaction,
|
||||||
NonCombatant = true,
|
NonCombatant = true,
|
||||||
Enemies = Exts.MakeArray(playerCount, i => $"Multi{i}")
|
Enemies = Exts.MakeArray(playerCount, i => $"Multi{i}").ToImmutableArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -66,7 +67,7 @@ namespace OpenRA
|
|||||||
Name = $"Multi{index}",
|
Name = $"Multi{index}",
|
||||||
Faction = "Random",
|
Faction = "Random",
|
||||||
Playable = true,
|
Playable = true,
|
||||||
Enemies = new[] { "Creeps" }
|
Enemies = ["Creeps"]
|
||||||
};
|
};
|
||||||
Players.Add(p.Name, p);
|
Players.Add(p.Name, p);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -23,10 +24,11 @@ using OpenRA.FileSystem;
|
|||||||
using OpenRA.Graphics;
|
using OpenRA.Graphics;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
using OpenRA.Support;
|
using OpenRA.Support;
|
||||||
|
using OpenRA.Traits;
|
||||||
|
|
||||||
namespace OpenRA
|
namespace OpenRA
|
||||||
{
|
{
|
||||||
public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError }
|
public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError, Generatable, Generating }
|
||||||
|
|
||||||
// Used for grouping maps in the UI
|
// Used for grouping maps in the UI
|
||||||
[Flags]
|
[Flags]
|
||||||
@@ -35,7 +37,8 @@ namespace OpenRA
|
|||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
System = 1,
|
System = 1,
|
||||||
User = 2,
|
User = 2,
|
||||||
Remote = 4
|
Remote = 4,
|
||||||
|
Generated = 8
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressMessage("StyleCop.CSharp.NamingRules",
|
[SuppressMessage("StyleCop.CSharp.NamingRules",
|
||||||
@@ -48,10 +51,10 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
public readonly string title;
|
public readonly string title;
|
||||||
public readonly string author;
|
public readonly string author;
|
||||||
public readonly string[] categories;
|
public readonly ImmutableArray<string> categories;
|
||||||
public readonly int players;
|
public readonly int players;
|
||||||
public readonly Rectangle bounds;
|
public readonly Rectangle bounds;
|
||||||
public readonly short[] spawnpoints = Array.Empty<short>();
|
public readonly ImmutableArray<short> spawnpoints = [];
|
||||||
public readonly MapGridType map_grid_type;
|
public readonly MapGridType map_grid_type;
|
||||||
public readonly string minimap;
|
public readonly string minimap;
|
||||||
public readonly bool downloading;
|
public readonly bool downloading;
|
||||||
@@ -69,12 +72,12 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
public int MapFormat;
|
public int MapFormat;
|
||||||
public string Title;
|
public string Title;
|
||||||
public string[] Categories;
|
public ImmutableArray<string> Categories;
|
||||||
public string Author;
|
public string Author;
|
||||||
public string TileSet;
|
public string TileSet;
|
||||||
public MapPlayers Players;
|
public MapPlayers Players;
|
||||||
public int PlayerCount;
|
public int PlayerCount;
|
||||||
public CPos[] SpawnPoints;
|
public ImmutableArray<CPos> SpawnPoints;
|
||||||
public MapGridType GridType;
|
public MapGridType GridType;
|
||||||
public Rectangle Bounds;
|
public Rectangle Bounds;
|
||||||
public Png Preview;
|
public Png Preview;
|
||||||
@@ -82,6 +85,7 @@ namespace OpenRA
|
|||||||
public MapClassification Class;
|
public MapClassification Class;
|
||||||
public MapVisibility Visibility;
|
public MapVisibility Visibility;
|
||||||
public DateTime ModifiedDate;
|
public DateTime ModifiedDate;
|
||||||
|
public MapGenerationArgs GenerationArgs;
|
||||||
|
|
||||||
public MiniYaml RuleDefinitions;
|
public MiniYaml RuleDefinitions;
|
||||||
public MiniYaml WeaponDefinitions;
|
public MiniYaml WeaponDefinitions;
|
||||||
@@ -113,7 +117,7 @@ namespace OpenRA
|
|||||||
return key == "world" || key == "player";
|
return key == "world" || key == "player";
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary<string, MiniYaml> yaml, IEnumerable<List<MiniYamlNode>> modDataRules)
|
public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary<string, MiniYaml> yaml, MiniYamlNode[][] modDataRules)
|
||||||
{
|
{
|
||||||
RuleDefinitions = LoadRuleSection(yaml, "Rules");
|
RuleDefinitions = LoadRuleSection(yaml, "Rules");
|
||||||
WeaponDefinitions = LoadRuleSection(yaml, "Weapons");
|
WeaponDefinitions = LoadRuleSection(yaml, "Weapons");
|
||||||
@@ -128,9 +132,9 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
if (FluentMessageDefinitions != null)
|
if (FluentMessageDefinitions != null)
|
||||||
{
|
{
|
||||||
var files = Array.Empty<string>();
|
var files = ImmutableArray<string>.Empty;
|
||||||
if (FluentMessageDefinitions.Value != null)
|
if (FluentMessageDefinitions.Value != null)
|
||||||
files = FieldLoader.GetValue<string[]>("value", FluentMessageDefinitions.Value);
|
files = FieldLoader.GetValue<ImmutableArray<string>>("value", FluentMessageDefinitions.Value);
|
||||||
|
|
||||||
string text = null;
|
string text = null;
|
||||||
if (FluentMessageDefinitions.Nodes.Length > 0)
|
if (FluentMessageDefinitions.Nodes.Length > 0)
|
||||||
@@ -156,8 +160,8 @@ namespace OpenRA
|
|||||||
var files = Enumerable.Empty<string>();
|
var files = Enumerable.Empty<string>();
|
||||||
if (RuleDefinitions.Value != null)
|
if (RuleDefinitions.Value != null)
|
||||||
{
|
{
|
||||||
var mapFiles = FieldLoader.GetValue<string[]>("value", RuleDefinitions.Value);
|
var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", RuleDefinitions.Value);
|
||||||
files = files.Append(mapFiles);
|
files = files.Concat(mapFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||||
@@ -195,22 +199,26 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static readonly CPos[] NoSpawns = Array.Empty<CPos>();
|
readonly object syncRoot = new();
|
||||||
readonly MapCache cache;
|
readonly MapCache cache;
|
||||||
readonly ModData modData;
|
readonly ModData modData;
|
||||||
|
IReadOnlyPackage package;
|
||||||
|
|
||||||
public readonly string Uid;
|
public readonly string Uid;
|
||||||
public string PackageName { get; private set; }
|
|
||||||
IReadOnlyPackage package;
|
public string Path { get; private set; }
|
||||||
public IReadOnlyPackage Package
|
|
||||||
|
void LoadPackage()
|
||||||
{
|
{
|
||||||
get
|
if (package == null && parentPackage != null)
|
||||||
{
|
package = parentPackage.OpenPackage(Path, modData.ModFiles);
|
||||||
package ??= parentPackage.OpenPackage(PackageName, modData.ModFiles);
|
|
||||||
return package;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private set => package = value;
|
public Map ToMap()
|
||||||
|
{
|
||||||
|
LoadPackage();
|
||||||
|
using (new PerfTimer("Map"))
|
||||||
|
return new Map(modData, package);
|
||||||
}
|
}
|
||||||
|
|
||||||
IReadOnlyPackage parentPackage;
|
IReadOnlyPackage parentPackage;
|
||||||
@@ -219,12 +227,12 @@ namespace OpenRA
|
|||||||
|
|
||||||
public int MapFormat => innerData.MapFormat;
|
public int MapFormat => innerData.MapFormat;
|
||||||
public string Title => innerData.Title;
|
public string Title => innerData.Title;
|
||||||
public string[] Categories => innerData.Categories;
|
public ImmutableArray<string> Categories => innerData.Categories;
|
||||||
public string Author => innerData.Author;
|
public string Author => innerData.Author;
|
||||||
public string TileSet => innerData.TileSet;
|
public string TileSet => innerData.TileSet;
|
||||||
public MapPlayers Players => innerData.Players;
|
public MapPlayers Players => innerData.Players;
|
||||||
public int PlayerCount => innerData.PlayerCount;
|
public int PlayerCount => innerData.PlayerCount;
|
||||||
public CPos[] SpawnPoints => innerData.SpawnPoints;
|
public ImmutableArray<CPos> SpawnPoints => innerData.SpawnPoints;
|
||||||
public MapGridType GridType => innerData.GridType;
|
public MapGridType GridType => innerData.GridType;
|
||||||
public Rectangle Bounds => innerData.Bounds;
|
public Rectangle Bounds => innerData.Bounds;
|
||||||
public Png Preview => innerData.Preview;
|
public Png Preview => innerData.Preview;
|
||||||
@@ -240,6 +248,8 @@ namespace OpenRA
|
|||||||
public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo;
|
public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo;
|
||||||
public DateTime ModifiedDate => innerData.ModifiedDate;
|
public DateTime ModifiedDate => innerData.ModifiedDate;
|
||||||
|
|
||||||
|
public MapGenerationArgs GenerationArgs => innerData.GenerationArgs;
|
||||||
|
|
||||||
public long DownloadBytes { get; private set; }
|
public long DownloadBytes { get; private set; }
|
||||||
public int DownloadPercentage { get; private set; }
|
public int DownloadPercentage { get; private set; }
|
||||||
|
|
||||||
@@ -249,11 +259,26 @@ namespace OpenRA
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string GetMessage(string key, object[] args = null)
|
public string GetMessage(string key, object[] args = null)
|
||||||
{
|
{
|
||||||
// PERF: instead of loading mod level strings per each MapPreview, reuse the already loaded one in FluentProvider.
|
if (TryGetMessage(key, out var message, args))
|
||||||
if (FluentProvider.TryGetModMessage(key, out var message, args))
|
|
||||||
return message;
|
return message;
|
||||||
|
|
||||||
return innerData.FluentBundle?.GetMessage(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;
|
Sprite minimap;
|
||||||
@@ -302,12 +327,12 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
MapFormat = 0,
|
MapFormat = 0,
|
||||||
Title = "Unknown Map",
|
Title = "Unknown Map",
|
||||||
Categories = new[] { "Unknown" },
|
Categories = ["Unknown"],
|
||||||
Author = "Unknown Author",
|
Author = "Unknown Author",
|
||||||
TileSet = "unknown",
|
TileSet = "unknown",
|
||||||
Players = null,
|
Players = null,
|
||||||
PlayerCount = 0,
|
PlayerCount = 0,
|
||||||
SpawnPoints = NoSpawns,
|
SpawnPoints = [],
|
||||||
GridType = gridType,
|
GridType = gridType,
|
||||||
Bounds = Rectangle.Empty,
|
Bounds = Rectangle.Empty,
|
||||||
Preview = null,
|
Preview = null,
|
||||||
@@ -317,57 +342,28 @@ namespace OpenRA
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// For linting purposes only!
|
/// <summary>
|
||||||
public MapPreview(Map map, ModData modData)
|
/// Updates internal state from a map without taking ownership of its package.
|
||||||
|
/// A new copy of the map package will be opened lazily when needed.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateFromMapWithoutOwningPackage(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification,
|
||||||
|
MapGridType? gridType = null, MiniYamlNode[][] modDataRules = null)
|
||||||
{
|
{
|
||||||
this.modData = modData;
|
UpdateFromMap(p, classification, gridType, modDataRules);
|
||||||
cache = modData.MapCache;
|
parentPackage = parent;
|
||||||
|
package = null;
|
||||||
Uid = map.Uid;
|
|
||||||
PackageName = map.Package.Name;
|
|
||||||
|
|
||||||
var mapPlayers = new MapPlayers(map.PlayerDefinitions);
|
|
||||||
var spawns = new List<CPos>();
|
|
||||||
foreach (var kv in map.ActorDefinitions.Where(d => d.Value.Value == "mpspawn"))
|
|
||||||
{
|
|
||||||
var s = new ActorReference(kv.Value.Value, kv.Value.ToDictionary());
|
|
||||||
spawns.Add(s.Get<LocationInit>().Value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
innerData = new InnerData
|
/// <summary>
|
||||||
|
/// Updates internal state from a map and takes ownership of its package.
|
||||||
|
/// The package remains in memory and must not be disposed.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateFromMap(IReadOnlyPackage p, MapClassification classification,
|
||||||
|
MapGridType? gridType = null, MiniYamlNode[][] modDataRules = null)
|
||||||
{
|
{
|
||||||
MapFormat = map.MapFormat,
|
Path = p.Name;
|
||||||
Title = map.Title,
|
package = p;
|
||||||
Categories = map.Categories,
|
|
||||||
Author = map.Author,
|
|
||||||
TileSet = map.Tileset,
|
|
||||||
Players = mapPlayers,
|
|
||||||
PlayerCount = mapPlayers.Players.Count(x => x.Value.Playable),
|
|
||||||
SpawnPoints = spawns.ToArray(),
|
|
||||||
GridType = map.Grid.Type,
|
|
||||||
Bounds = map.Bounds,
|
|
||||||
Preview = null,
|
|
||||||
Status = MapStatus.Available,
|
|
||||||
Class = MapClassification.Unknown,
|
|
||||||
Visibility = map.Visibility,
|
|
||||||
};
|
|
||||||
|
|
||||||
innerData.SetCustomRules(modData, this, new Dictionary<string, MiniYaml>()
|
|
||||||
{
|
|
||||||
{ "Rules", map.RuleDefinitions },
|
|
||||||
{ "FluentMessages", map.FluentMessageDefinitions },
|
|
||||||
{ "Weapons", map.WeaponDefinitions },
|
|
||||||
{ "Voices", map.VoiceDefinitions },
|
|
||||||
{ "Music", map.MusicDefinitions },
|
|
||||||
{ "Notifications", map.NotificationDefinitions },
|
|
||||||
{ "Sequences", map.SequenceDefinitions },
|
|
||||||
{ "ModelSequences", map.ModelSequenceDefinitions }
|
|
||||||
}, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification,
|
|
||||||
string[] mapCompatibility, MapGridType gridType, IEnumerable<List<MiniYamlNode>> modDataRules)
|
|
||||||
{
|
|
||||||
Dictionary<string, MiniYaml> yaml;
|
Dictionary<string, MiniYaml> yaml;
|
||||||
using (var yamlStream = p.GetStream("map.yaml"))
|
using (var yamlStream = p.GetStream("map.yaml"))
|
||||||
{
|
{
|
||||||
@@ -377,12 +373,9 @@ namespace OpenRA
|
|||||||
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, $"{p.Name}:map.yaml", stringPool: cache.StringPool)).ToDictionary();
|
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, $"{p.Name}:map.yaml", stringPool: cache.StringPool)).ToDictionary();
|
||||||
}
|
}
|
||||||
|
|
||||||
PackageName = p.Name;
|
|
||||||
parentPackage = parent;
|
|
||||||
|
|
||||||
var newData = innerData.Clone();
|
var newData = innerData.Clone();
|
||||||
newData.GridType = gridType;
|
|
||||||
newData.Class = classification;
|
newData.Class = classification;
|
||||||
|
newData.GridType = gridType ?? modData.GetOrCreate<MapGrid>().Type;
|
||||||
|
|
||||||
if (yaml.TryGetValue("MapFormat", out var temp))
|
if (yaml.TryGetValue("MapFormat", out var temp))
|
||||||
{
|
{
|
||||||
@@ -395,7 +388,7 @@ namespace OpenRA
|
|||||||
newData.Title = temp.Value;
|
newData.Title = temp.Value;
|
||||||
|
|
||||||
if (yaml.TryGetValue("Categories", out temp))
|
if (yaml.TryGetValue("Categories", out temp))
|
||||||
newData.Categories = FieldLoader.GetValue<string[]>("Categories", temp.Value);
|
newData.Categories = FieldLoader.GetValue<ImmutableArray<string>>("Categories", temp.Value);
|
||||||
|
|
||||||
if (yaml.TryGetValue("Tileset", out temp))
|
if (yaml.TryGetValue("Tileset", out temp))
|
||||||
newData.TileSet = temp.Value;
|
newData.TileSet = temp.Value;
|
||||||
@@ -416,7 +409,7 @@ namespace OpenRA
|
|||||||
if (yaml.TryGetValue("MapFormat", out temp))
|
if (yaml.TryGetValue("MapFormat", out temp))
|
||||||
newData.MapFormat = FieldLoader.GetValue<int>("MapFormat", temp.Value);
|
newData.MapFormat = FieldLoader.GetValue<int>("MapFormat", temp.Value);
|
||||||
|
|
||||||
newData.Status = mapCompatibility == null || mapCompatibility.Contains(requiresMod) ?
|
newData.Status = modData.Manifest.MapCompatibility.Contains(requiresMod) ?
|
||||||
MapStatus.Available : MapStatus.Unavailable;
|
MapStatus.Available : MapStatus.Unavailable;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -427,18 +420,18 @@ namespace OpenRA
|
|||||||
var spawns = new List<CPos>();
|
var spawns = new List<CPos>();
|
||||||
foreach (var kv in actorDefinitions.Nodes.Where(d => d.Value.Value == "mpspawn"))
|
foreach (var kv in actorDefinitions.Nodes.Where(d => d.Value.Value == "mpspawn"))
|
||||||
{
|
{
|
||||||
var s = new ActorReference(kv.Value.Value, kv.Value.ToDictionary());
|
var s = new ActorReference(kv.Value.Value, kv.Value);
|
||||||
spawns.Add(s.Get<LocationInit>().Value);
|
spawns.Add(s.Get<LocationInit>().Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
newData.SpawnPoints = spawns.ToArray();
|
newData.SpawnPoints = spawns.ToImmutableArray();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
newData.SpawnPoints = Array.Empty<CPos>();
|
newData.SpawnPoints = [];
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
newData.SpawnPoints = Array.Empty<CPos>();
|
newData.SpawnPoints = [];
|
||||||
newData.Status = MapStatus.Unavailable;
|
newData.Status = MapStatus.Unavailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,31 +455,122 @@ namespace OpenRA
|
|||||||
using (var dataStream = p.GetStream("map.png"))
|
using (var dataStream = p.GetStream("map.png"))
|
||||||
newData.Preview = new Png(dataStream);
|
newData.Preview = new Png(dataStream);
|
||||||
|
|
||||||
newData.ModifiedDate = File.GetLastWriteTime(p.Name);
|
newData.ModifiedDate = p.Name != null ? File.GetLastWriteTime(p.Name) : DateTime.Now;
|
||||||
|
|
||||||
// Assign the new data atomically
|
// Assign the new data atomically
|
||||||
|
// Local maps have higher precedence than remote/generated maps,
|
||||||
|
// so should always replace their metadata
|
||||||
|
lock (syncRoot)
|
||||||
innerData = newData;
|
innerData = newData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, string[] mapCompatibility, Action<MapPreview> parseMetadata = null)
|
public void UpdateFromGenerationArgs(MapGenerationArgs args)
|
||||||
{
|
{
|
||||||
var newData = innerData.Clone();
|
var newData = innerData.Clone();
|
||||||
newData.Status = status;
|
newData.Class = MapClassification.Generated;
|
||||||
newData.Class = MapClassification.Remote;
|
newData.Status = MapStatus.Generatable;
|
||||||
|
newData.Title = args.Title;
|
||||||
|
newData.Author = args.Author;
|
||||||
|
newData.TileSet = args.Tileset;
|
||||||
|
newData.GenerationArgs = args;
|
||||||
|
newData.MapFormat = Map.CurrentMapFormat;
|
||||||
|
|
||||||
if (status == MapStatus.DownloadAvailable)
|
try
|
||||||
|
{
|
||||||
|
var generator = modData.DefaultRules.Actors[SystemActors.EditorWorld]
|
||||||
|
.TraitInfos<IMapGeneratorInfo>()
|
||||||
|
.FirstOrDefault(info => info.Type == args.Generator);
|
||||||
|
|
||||||
|
if (generator == null)
|
||||||
|
throw new Exception($"Unknown map generator type {args.Generator}");
|
||||||
|
|
||||||
|
if (!generator.TryGenerateMetadata(modData, args, out var players, out var ruleDefinitions))
|
||||||
|
throw new Exception("Failed to generate map metadata");
|
||||||
|
|
||||||
|
newData.Players = players;
|
||||||
|
newData.PlayerCount = newData.Players.Players.Count(x => x.Value.Playable);
|
||||||
|
newData.SetCustomRules(modData, this, ruleDefinitions, null);
|
||||||
|
|
||||||
|
// Placeholder to satisfy server-side lint checks
|
||||||
|
newData.SpawnPoints = Exts.MakeArray(newData.PlayerCount, i => new CPos(i, i)).ToImmutableArray();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Write("debug", "Map generation failed with error:");
|
||||||
|
Log.Write("debug", e);
|
||||||
|
|
||||||
|
newData.Status = MapStatus.Unavailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (syncRoot)
|
||||||
|
innerData = newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Generate()
|
||||||
|
{
|
||||||
|
if (Class != MapClassification.Generated || Status != MapStatus.Generatable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (syncRoot)
|
||||||
|
innerData.Status = MapStatus.Generating;
|
||||||
|
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var generator = modData.DefaultRules.Actors[SystemActors.EditorWorld]
|
||||||
|
.TraitInfos<IMapGeneratorInfo>()
|
||||||
|
.FirstOrDefault(info => info.Type == GenerationArgs.Generator);
|
||||||
|
|
||||||
|
if (generator == null)
|
||||||
|
throw new Exception($"Unknown map generator type {GenerationArgs.Generator}");
|
||||||
|
|
||||||
|
var map = generator.Generate(modData, GenerationArgs);
|
||||||
|
|
||||||
|
// Uid is generated when the map is saved
|
||||||
|
map.Save(new ZipFileLoader.ReadWriteZipFile());
|
||||||
|
|
||||||
|
if (map.Uid != GenerationArgs.Uid)
|
||||||
|
throw new InvalidOperationException("Map generation UID mismatch");
|
||||||
|
|
||||||
|
Game.RunAfterTick(() => UpdateFromMap(map.Package, MapClassification.Generated));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Write("debug", "Map generation failed with error:");
|
||||||
|
Log.Write("debug", e);
|
||||||
|
|
||||||
|
lock (syncRoot)
|
||||||
|
innerData.Status = MapStatus.Unavailable;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BeginRemoteSearch()
|
||||||
|
{
|
||||||
|
var newData = innerData.Clone();
|
||||||
|
newData.Class = MapClassification.Remote;
|
||||||
|
newData.Status = MapStatus.Searching;
|
||||||
|
|
||||||
|
// We may have been resolved to a local/generated map by another
|
||||||
|
// async task. Make sure we don't stomp over their state!
|
||||||
|
lock (syncRoot)
|
||||||
|
if (innerData.Class == MapClassification.Unknown || innerData.Class == MapClassification.Remote)
|
||||||
|
innerData = newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CompleteRemoteSearch(MiniYaml yaml, Action<MapPreview> parseMetadata = null)
|
||||||
|
{
|
||||||
|
var newData = innerData.Clone();
|
||||||
|
newData.Class = MapClassification.Remote;
|
||||||
|
newData.Status = MapStatus.Unavailable;
|
||||||
|
|
||||||
|
if (yaml != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var r = FieldLoader.Load<RemoteMapData>(yaml);
|
var r = FieldLoader.Load<RemoteMapData>(yaml);
|
||||||
|
newData.Status = r.downloading ? MapStatus.DownloadAvailable : MapStatus.Unavailable;
|
||||||
// Map download has been disabled server side
|
|
||||||
if (!r.downloading)
|
|
||||||
{
|
|
||||||
newData.Status = MapStatus.Unavailable;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
newData.Title = r.title;
|
newData.Title = r.title;
|
||||||
newData.Categories = r.categories;
|
newData.Categories = r.categories;
|
||||||
newData.Author = r.author;
|
newData.Author = r.author;
|
||||||
@@ -498,7 +582,7 @@ namespace OpenRA
|
|||||||
var spawns = new CPos[r.spawnpoints.Length / 2];
|
var spawns = new CPos[r.spawnpoints.Length / 2];
|
||||||
for (var j = 0; j < r.spawnpoints.Length; j += 2)
|
for (var j = 0; j < r.spawnpoints.Length; j += 2)
|
||||||
spawns[j / 2] = new CPos(r.spawnpoints[j], r.spawnpoints[j + 1]);
|
spawns[j / 2] = new CPos(r.spawnpoints[j], r.spawnpoints[j + 1]);
|
||||||
newData.SpawnPoints = spawns;
|
newData.SpawnPoints = spawns.ToImmutableArray();
|
||||||
newData.GridType = r.map_grid_type;
|
newData.GridType = r.map_grid_type;
|
||||||
if (cache.LoadPreviewImages)
|
if (cache.LoadPreviewImages)
|
||||||
{
|
{
|
||||||
@@ -526,29 +610,37 @@ namespace OpenRA
|
|||||||
// Map is for a different mod: update its information so it can be displayed
|
// 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
|
// 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.
|
// be selected in a server for the current mod.
|
||||||
if (!mapCompatibility.Contains(r.game_mod))
|
if (!modData.Manifest.MapCompatibility.Contains(r.game_mod))
|
||||||
newData.Status = MapStatus.Unavailable;
|
newData.Status = MapStatus.Unavailable;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Log.Write("debug", "Failed parsing mapserver response:");
|
Log.Write("debug", "Failed parsing mapserver response:");
|
||||||
Log.Write("debug", e);
|
Log.Write("debug", e);
|
||||||
|
newData.Status = MapStatus.Unavailable;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit updated data before running the callbacks
|
// We may have been resolved to a local/generated map by another
|
||||||
|
// async task. Make sure we don't stomp over their state!
|
||||||
|
MapClassification mapClassification;
|
||||||
|
lock (syncRoot)
|
||||||
|
{
|
||||||
|
mapClassification = innerData.Class;
|
||||||
|
if (mapClassification == MapClassification.Remote)
|
||||||
innerData = newData;
|
innerData = newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapClassification == MapClassification.Remote)
|
||||||
|
{
|
||||||
if (innerData.Preview != null)
|
if (innerData.Preview != null)
|
||||||
cache.CacheMinimap(this);
|
cache.CacheMinimap(this);
|
||||||
|
|
||||||
parseMetadata?.Invoke(this);
|
parseMetadata?.Invoke(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the status and class unconditionally
|
|
||||||
innerData = newData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Install(string mapRepositoryUrl, Action onSuccess)
|
public void Install(string mapRepositoryUrl)
|
||||||
{
|
{
|
||||||
if ((Status != MapStatus.DownloadError && Status != MapStatus.DownloadAvailable) || !Game.Settings.Game.AllowDownloading)
|
if ((Status != MapStatus.DownloadError && Status != MapStatus.DownloadAvailable) || !Game.Settings.Game.AllowDownloading)
|
||||||
return;
|
return;
|
||||||
@@ -601,14 +693,11 @@ namespace OpenRA
|
|||||||
mapInstallPackage.Update(mapFilename, fileStream.ToArray());
|
mapInstallPackage.Update(mapFilename, fileStream.ToArray());
|
||||||
Log.Write("debug", $"Downloaded map to '{mapFilename}'");
|
Log.Write("debug", $"Downloaded map to '{mapFilename}'");
|
||||||
|
|
||||||
var package = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles);
|
var p = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles);
|
||||||
if (package == null)
|
if (p == null)
|
||||||
innerData.Status = MapStatus.DownloadError;
|
innerData.Status = MapStatus.DownloadError;
|
||||||
else
|
else
|
||||||
{
|
UpdateFromMapWithoutOwningPackage(p, mapInstallPackage, MapClassification.User, GridType);
|
||||||
UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType, null);
|
|
||||||
Game.RunAfterTick(onSuccess);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -621,34 +710,31 @@ namespace OpenRA
|
|||||||
|
|
||||||
public void Invalidate()
|
public void Invalidate()
|
||||||
{
|
{
|
||||||
|
lock (syncRoot)
|
||||||
|
{
|
||||||
|
innerData.Class = MapClassification.Unknown;
|
||||||
innerData.Status = MapStatus.Unavailable;
|
innerData.Status = MapStatus.Unavailable;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
DisposePackage();
|
package?.Dispose();
|
||||||
}
|
|
||||||
|
|
||||||
public void DisposePackage()
|
|
||||||
{
|
|
||||||
if (package != null)
|
|
||||||
{
|
|
||||||
package.Dispose();
|
|
||||||
package = null;
|
package = null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void Delete()
|
public void Delete()
|
||||||
{
|
{
|
||||||
Invalidate();
|
Invalidate();
|
||||||
(parentPackage as IReadWritePackage)?.Delete(Package.Name);
|
(parentPackage as IReadWritePackage)?.Delete(Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream IReadOnlyFileSystem.Open(string filename)
|
Stream IReadOnlyFileSystem.Open(string filename)
|
||||||
{
|
{
|
||||||
// Explicit package paths never refer to a map
|
// Explicit package paths never refer to a map
|
||||||
if (!filename.Contains('|') && Package.Contains(filename))
|
LoadPackage();
|
||||||
return Package.GetStream(filename);
|
if (!filename.Contains('|') && package.Contains(filename))
|
||||||
|
return package.GetStream(filename);
|
||||||
|
|
||||||
return modData.DefaultFileSystem.Open(filename);
|
return modData.DefaultFileSystem.Open(filename);
|
||||||
}
|
}
|
||||||
@@ -664,7 +750,8 @@ namespace OpenRA
|
|||||||
// Explicit package paths never refer to a map
|
// Explicit package paths never refer to a map
|
||||||
if (!filename.Contains('|'))
|
if (!filename.Contains('|'))
|
||||||
{
|
{
|
||||||
s = Package.GetStream(filename);
|
LoadPackage();
|
||||||
|
s = package.GetStream(filename);
|
||||||
if (s != null)
|
if (s != null)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -675,7 +762,8 @@ namespace OpenRA
|
|||||||
bool IReadOnlyFileSystem.Exists(string filename)
|
bool IReadOnlyFileSystem.Exists(string filename)
|
||||||
{
|
{
|
||||||
// Explicit package paths never refer to a map
|
// Explicit package paths never refer to a map
|
||||||
if (!filename.Contains('|') && Package.Contains(filename))
|
LoadPackage();
|
||||||
|
if (!filename.Contains('|') && package.Contains(filename))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return modData.DefaultFileSystem.Exists(filename);
|
return modData.DefaultFileSystem.Exists(filename);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System.Collections.Immutable;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
|
|
||||||
namespace OpenRA
|
namespace OpenRA
|
||||||
@@ -31,7 +31,7 @@ namespace OpenRA
|
|||||||
public string Faction;
|
public string Faction;
|
||||||
|
|
||||||
public bool LockColor = false;
|
public bool LockColor = false;
|
||||||
public Color Color = Game.ModData.Manifest.Get<DefaultPlayer>().Color;
|
public Color Color = Game.ModData.GetOrCreate<DefaultPlayer>().Color;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the "Home" location, which can be used by traits and scripts to e.g. set the initial camera
|
/// Sets the "Home" location, which can be used by traits and scripts to e.g. set the initial camera
|
||||||
@@ -54,8 +54,8 @@ namespace OpenRA
|
|||||||
public bool LockHandicap = false;
|
public bool LockHandicap = false;
|
||||||
public int Handicap = 0;
|
public int Handicap = 0;
|
||||||
|
|
||||||
public string[] Allies = Array.Empty<string>();
|
public ImmutableArray<string> Allies = [];
|
||||||
public string[] Enemies = Array.Empty<string>();
|
public ImmutableArray<string> Enemies = [];
|
||||||
|
|
||||||
public PlayerReference() { }
|
public PlayerReference() { }
|
||||||
public PlayerReference(MiniYaml my) { FieldLoader.Load(this, my); }
|
public PlayerReference(MiniYaml my) { FieldLoader.Load(this, my); }
|
||||||
|
|||||||
@@ -55,14 +55,9 @@ namespace OpenRA
|
|||||||
return Bounds.Contains(uv.U, uv.V);
|
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)
|
public void SetAll(T value)
|
||||||
{
|
{
|
||||||
Array.Fill(Entries, value);
|
Entries.AsSpan().Fill(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using OpenRA.FileSystem;
|
using OpenRA.FileSystem;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
using OpenRA.Support;
|
using OpenRA.Support;
|
||||||
@@ -25,14 +26,16 @@ namespace OpenRA
|
|||||||
public interface ITerrainInfo
|
public interface ITerrainInfo
|
||||||
{
|
{
|
||||||
string Id { get; }
|
string Id { get; }
|
||||||
TerrainTypeInfo[] TerrainTypes { get; }
|
string Name { get; }
|
||||||
|
Size TileSize { get; }
|
||||||
|
ImmutableArray<TerrainTypeInfo> TerrainTypes { get; }
|
||||||
TerrainTileInfo GetTerrainInfo(TerrainTile r);
|
TerrainTileInfo GetTerrainInfo(TerrainTile r);
|
||||||
bool TryGetTerrainInfo(TerrainTile r, out TerrainTileInfo info);
|
bool TryGetTerrainInfo(TerrainTile r, out TerrainTileInfo info);
|
||||||
byte GetTerrainIndex(string type);
|
byte GetTerrainIndex(string type);
|
||||||
byte GetTerrainIndex(TerrainTile r);
|
byte GetTerrainIndex(TerrainTile r);
|
||||||
TerrainTile DefaultTerrainTile { get; }
|
TerrainTile DefaultTerrainTile { get; }
|
||||||
|
|
||||||
Color[] HeightDebugColors { get; }
|
ImmutableArray<Color> HeightDebugColors { get; }
|
||||||
IEnumerable<Color> RestrictedPlayerColors { get; }
|
IEnumerable<Color> RestrictedPlayerColors { get; }
|
||||||
float MinHeightColorBrightness { get; }
|
float MinHeightColorBrightness { get; }
|
||||||
float MaxHeightColorBrightness { get; }
|
float MaxHeightColorBrightness { get; }
|
||||||
@@ -60,7 +63,7 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
public readonly string Type;
|
public readonly string Type;
|
||||||
public readonly BitSet<TargetableType> TargetTypes;
|
public readonly BitSet<TargetableType> TargetTypes;
|
||||||
public readonly HashSet<string> AcceptsSmudgeType = new();
|
public readonly ImmutableArray<string> AcceptsSmudgeType = [];
|
||||||
public readonly Color Color;
|
public readonly Color Color;
|
||||||
public readonly bool RestrictPlayerColor = false;
|
public readonly bool RestrictPlayerColor = false;
|
||||||
|
|
||||||
|
|||||||
@@ -11,32 +11,35 @@
|
|||||||
|
|
||||||
namespace OpenRA
|
namespace OpenRA
|
||||||
{
|
{
|
||||||
public readonly struct TerrainTile
|
public readonly struct TerrainTile(ushort type, byte index)
|
||||||
{
|
{
|
||||||
public readonly ushort Type;
|
public readonly ushort Type = type;
|
||||||
public readonly byte Index;
|
public readonly byte Index = index;
|
||||||
|
|
||||||
public TerrainTile(ushort type, byte index)
|
|
||||||
{
|
|
||||||
Type = type;
|
|
||||||
Index = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode() { return Type.GetHashCode() ^ Index.GetHashCode(); }
|
public override int GetHashCode() { return Type.GetHashCode() ^ Index.GetHashCode(); }
|
||||||
|
|
||||||
public override string ToString() { return Type + "," + Index; }
|
public override string ToString() { return Type + "," + Index; }
|
||||||
|
|
||||||
|
public static bool TryParse(string s, out TerrainTile tt)
|
||||||
|
{
|
||||||
|
var split = s.Split(',');
|
||||||
|
if (split.Length == 2 &&
|
||||||
|
Exts.TryParseUshortInvariant(split[0], out var type) &&
|
||||||
|
Exts.TryParseByteInvariant(split[1], out var index))
|
||||||
|
{
|
||||||
|
tt = new TerrainTile(type, index);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly struct ResourceTile
|
tt = default;
|
||||||
{
|
return false;
|
||||||
public readonly byte Type;
|
|
||||||
public readonly byte Index;
|
|
||||||
|
|
||||||
public ResourceTile(byte type, byte index)
|
|
||||||
{
|
|
||||||
Type = type;
|
|
||||||
Index = index;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly struct ResourceTile(byte type, byte index)
|
||||||
|
{
|
||||||
|
public readonly byte Type = type;
|
||||||
|
public readonly byte Index = index;
|
||||||
|
|
||||||
public override int GetHashCode() { return Type.GetHashCode() ^ Index.GetHashCode(); }
|
public override int GetHashCode() { return Type.GetHashCode() ^ Index.GetHashCode(); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,16 +59,10 @@ namespace OpenRA
|
|||||||
|
|
||||||
public sealed class MiniYamlNode
|
public sealed class MiniYamlNode
|
||||||
{
|
{
|
||||||
public readonly struct SourceLocation
|
public readonly struct SourceLocation(string name, int line)
|
||||||
{
|
{
|
||||||
public readonly string Name;
|
public readonly string Name = name;
|
||||||
public readonly int Line;
|
public readonly int Line = line;
|
||||||
|
|
||||||
public SourceLocation(string name, int line)
|
|
||||||
{
|
|
||||||
Name = name;
|
|
||||||
Line = line;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() { return $"{Name}:{Line}"; }
|
public override string ToString() { return $"{Name}:{Line}"; }
|
||||||
}
|
}
|
||||||
@@ -99,7 +93,7 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
|
|
||||||
public MiniYamlNode(string k, string v, string c = null)
|
public MiniYamlNode(string k, string v, string c = null)
|
||||||
: this(k, new MiniYaml(v, Enumerable.Empty<MiniYamlNode>()), c) { }
|
: this(k, new MiniYaml(v, []), c) { }
|
||||||
|
|
||||||
public MiniYamlNode(string k, string v, IEnumerable<MiniYamlNode> n)
|
public MiniYamlNode(string k, string v, IEnumerable<MiniYamlNode> n)
|
||||||
: this(k, new MiniYaml(v, n), null) { }
|
: this(k, new MiniYaml(v, n), null) { }
|
||||||
@@ -115,7 +109,7 @@ namespace OpenRA
|
|||||||
const int SpacesPerLevel = 4;
|
const int SpacesPerLevel = 4;
|
||||||
static readonly Func<string, string> StringIdentity = s => s;
|
static readonly Func<string, string> StringIdentity = s => s;
|
||||||
static readonly Func<MiniYaml, MiniYaml> MiniYamlIdentity = my => my;
|
static readonly Func<MiniYaml, MiniYaml> MiniYamlIdentity = my => my;
|
||||||
static readonly Dictionary<string, MiniYamlNode> ConflictScratch = new();
|
static readonly Dictionary<string, MiniYamlNode> ConflictScratch = [];
|
||||||
|
|
||||||
public readonly string Value;
|
public readonly string Value;
|
||||||
public readonly ImmutableArray<MiniYamlNode> Nodes;
|
public readonly ImmutableArray<MiniYamlNode> Nodes;
|
||||||
@@ -196,21 +190,22 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
|
|
||||||
public MiniYaml(string value)
|
public MiniYaml(string value)
|
||||||
: this(value, Enumerable.Empty<MiniYamlNode>()) { }
|
: this(value, []) { }
|
||||||
|
|
||||||
public MiniYaml(string value, IEnumerable<MiniYamlNode> nodes)
|
public MiniYaml(string value, IEnumerable<MiniYamlNode> nodes)
|
||||||
{
|
{
|
||||||
Value = value;
|
Value = value;
|
||||||
Nodes = ImmutableArray.CreateRange(nodes);
|
Nodes = nodes.ToImmutableArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string name, bool discardCommentsAndWhitespace, HashSet<string> stringPool)
|
static IEnumerable<MiniYamlNode> FromLines(
|
||||||
|
IEnumerable<ReadOnlyMemory<char>> lines, string name, bool discardCommentsAndWhitespace, HashSet<string> stringPool)
|
||||||
{
|
{
|
||||||
// YAML config often contains repeated strings for key, values, comments.
|
// YAML config often contains repeated strings for key, values, comments.
|
||||||
// Pool these strings so we only need one copy of each unique string.
|
// 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.
|
// 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.
|
// A caller can also provide a pool as input, allowing de-duplication across multiple parses.
|
||||||
stringPool ??= new HashSet<string>();
|
stringPool ??= [];
|
||||||
|
|
||||||
var result = new List<List<MiniYamlNode>>
|
var result = new List<List<MiniYamlNode>>
|
||||||
{
|
{
|
||||||
@@ -341,18 +336,25 @@ namespace OpenRA
|
|||||||
|
|
||||||
parsedLines.Add((level, keyString, valueString, commentString, location));
|
parsedLines.Add((level, keyString, valueString, commentString, location));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var topLevelNode in result[0])
|
||||||
|
yield return topLevelNode;
|
||||||
|
result[0].Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedLines.Count > 0)
|
if (parsedLines.Count > 0)
|
||||||
|
{
|
||||||
BuildCompletedSubNode(0);
|
BuildCompletedSubNode(0);
|
||||||
|
foreach (var topLevelNode in result[0])
|
||||||
return result[0];
|
yield return topLevelNode;
|
||||||
|
result[0].Clear();
|
||||||
|
}
|
||||||
|
|
||||||
void BuildCompletedSubNode(int level)
|
void BuildCompletedSubNode(int level)
|
||||||
{
|
{
|
||||||
var lastLevel = parsedLines[^1].Level;
|
var lastLevel = parsedLines[^1].Level;
|
||||||
while (lastLevel >= result.Count)
|
while (lastLevel >= result.Count)
|
||||||
result.Add(new List<MiniYamlNode>());
|
result.Add([]);
|
||||||
|
|
||||||
while (parsedLines.Count > 0 && parsedLines[^1].Level >= level)
|
while (parsedLines.Count > 0 && parsedLines[^1].Level >= level)
|
||||||
{
|
{
|
||||||
@@ -381,29 +383,30 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
|
public static IEnumerable<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
|
||||||
{
|
{
|
||||||
return FromStream(File.OpenRead(path), path, discardCommentsAndWhitespace, stringPool);
|
return FromStream(File.OpenRead(path), path, discardCommentsAndWhitespace, stringPool);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<MiniYamlNode> FromStream(Stream s, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
|
public static IEnumerable<MiniYamlNode> FromStream(Stream s, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
|
||||||
{
|
{
|
||||||
return FromLines(s.ReadAllLinesAsMemory(), name, discardCommentsAndWhitespace, stringPool);
|
return FromLines(s.ReadAllLinesAsMemory(), name, discardCommentsAndWhitespace, stringPool);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<MiniYamlNode> FromString(string text, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
|
public static IEnumerable<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()), name, discardCommentsAndWhitespace, stringPool);
|
return FromLines(text.Split(["\r\n", "\n"], StringSplitOptions.None).Select(s => s.AsMemory()), name, discardCommentsAndWhitespace, stringPool);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<MiniYamlNode> Merge(IEnumerable<IReadOnlyCollection<MiniYamlNode>> sources)
|
public static List<MiniYamlNode> Merge(IEnumerable<IEnumerable<MiniYamlNode>> sources)
|
||||||
{
|
{
|
||||||
var sourcesList = sources.ToList();
|
var sourcesList = sources.ToList();
|
||||||
if (sourcesList.Count == 0)
|
if (sourcesList.Count == 0)
|
||||||
return new List<MiniYamlNode>();
|
return [];
|
||||||
|
|
||||||
var tree = sourcesList
|
var tree = sourcesList
|
||||||
.Where(s => s != null)
|
.Where(s => s != null)
|
||||||
|
.Select(s => s as IReadOnlyCollection<MiniYamlNode> ?? s.ToList())
|
||||||
.Select(MergeSelfPartial)
|
.Select(MergeSelfPartial)
|
||||||
.Aggregate(MergePartial)
|
.Aggregate(MergePartial)
|
||||||
.Where(n => n.Key != null)
|
.Where(n => n.Key != null)
|
||||||
@@ -503,6 +506,9 @@ namespace OpenRA
|
|||||||
var ret = new List<MiniYamlNode>(existingNodes.Count);
|
var ret = new List<MiniYamlNode>(existingNodes.Count);
|
||||||
foreach (var n in existingNodes)
|
foreach (var n in existingNodes)
|
||||||
{
|
{
|
||||||
|
if (n.Key == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
if (keys.Add(n.Key))
|
if (keys.Add(n.Key))
|
||||||
ret.Add(n);
|
ret.Add(n);
|
||||||
else
|
else
|
||||||
@@ -517,17 +523,60 @@ namespace OpenRA
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static IReadOnlyList<MiniYamlNode> WeakResolveRemovals(IReadOnlyList<MiniYamlNode> nodes)
|
||||||
|
{
|
||||||
|
if (nodes == null || nodes.Count == 0)
|
||||||
|
return nodes;
|
||||||
|
|
||||||
|
List<MiniYamlNode> ret = null;
|
||||||
|
for (var i = 0; i < nodes.Count; i++)
|
||||||
|
{
|
||||||
|
var node = nodes[i];
|
||||||
|
if (node.Key == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (node.Key.StartsWith('-'))
|
||||||
|
{
|
||||||
|
if (ret == null)
|
||||||
|
{
|
||||||
|
ret ??= new List<MiniYamlNode>(nodes.Count);
|
||||||
|
ret.AddRange(nodes.Take(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the removal node - but "weakly" - don't throw if there is no prior node to remove.
|
||||||
|
var removed = node.Key[1..];
|
||||||
|
ret.RemoveAll(r => r.Key == removed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ret?.Add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret ?? nodes;
|
||||||
|
}
|
||||||
|
|
||||||
static MiniYaml MergePartial(MiniYaml existingNodes, MiniYaml overrideNodes)
|
static MiniYaml MergePartial(MiniYaml existingNodes, MiniYaml overrideNodes)
|
||||||
{
|
{
|
||||||
|
var resolvedExistingNodes = WeakResolveRemovals(existingNodes?.Nodes);
|
||||||
|
var resolvedOverrideNodes = WeakResolveRemovals(overrideNodes?.Nodes);
|
||||||
|
|
||||||
lock (ConflictScratch)
|
lock (ConflictScratch)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
// PERF: Reuse ConflictScratch for all conflict checks to avoid allocations.
|
// PERF: Reuse ConflictScratch for all conflict checks to avoid allocations.
|
||||||
existingNodes?.Nodes.IntoDictionaryWithConflictLog(
|
resolvedExistingNodes?.IntoDictionaryWithConflictLog(
|
||||||
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
|
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
|
||||||
overrideNodes?.Nodes.IntoDictionaryWithConflictLog(
|
resolvedOverrideNodes?.IntoDictionaryWithConflictLog(
|
||||||
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
|
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
|
||||||
ConflictScratch.Clear();
|
ConflictScratch.Clear();
|
||||||
}
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
throw new YamlException(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (existingNodes == null)
|
if (existingNodes == null)
|
||||||
return overrideNodes;
|
return overrideNodes;
|
||||||
@@ -556,6 +605,9 @@ namespace OpenRA
|
|||||||
|
|
||||||
void MergeNode(MiniYamlNode node)
|
void MergeNode(MiniYamlNode node)
|
||||||
{
|
{
|
||||||
|
if (node.Key == null)
|
||||||
|
return;
|
||||||
|
|
||||||
// Append Removal nodes to the result.
|
// Append Removal nodes to the result.
|
||||||
// Therefore: we know the remainder of the method deals with a plain node.
|
// Therefore: we know the remainder of the method deals with a plain node.
|
||||||
if (node.Key.StartsWith('-'))
|
if (node.Key.StartsWith('-'))
|
||||||
@@ -626,12 +678,12 @@ namespace OpenRA
|
|||||||
{
|
{
|
||||||
if (mapRules != null && mapRules.Value != null)
|
if (mapRules != null && mapRules.Value != null)
|
||||||
{
|
{
|
||||||
var mapFiles = FieldLoader.GetValue<string[]>("value", mapRules.Value);
|
var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", mapRules.Value);
|
||||||
files = files.Append(mapFiles);
|
files = files.Concat(mapFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||||
IEnumerable<IReadOnlyCollection<MiniYamlNode>> yaml = files.Select(s => FromStream(fileSystem.Open(s), s, stringPool: stringPool));
|
var yaml = files.Select(s => FromStream(fileSystem.Open(s), s, stringPool: stringPool));
|
||||||
if (mapRules != null && mapRules.Nodes.Length > 0)
|
if (mapRules != null && mapRules.Nodes.Length > 0)
|
||||||
yaml = yaml.Append(mapRules.Nodes);
|
yaml = yaml.Append(mapRules.Nodes);
|
||||||
|
|
||||||
@@ -696,7 +748,7 @@ namespace OpenRA
|
|||||||
public MiniYamlBuilder(string value, List<MiniYamlNode> nodes)
|
public MiniYamlBuilder(string value, List<MiniYamlNode> nodes)
|
||||||
{
|
{
|
||||||
Value = value;
|
Value = value;
|
||||||
Nodes = nodes == null ? new List<MiniYamlNodeBuilder>() : nodes.ConvertAll(x => new MiniYamlNodeBuilder(x));
|
Nodes = nodes == null ? [] : nodes.ConvertAll(x => new MiniYamlNodeBuilder(x));
|
||||||
}
|
}
|
||||||
|
|
||||||
public MiniYaml Build()
|
public MiniYaml Build()
|
||||||
@@ -724,7 +776,6 @@ namespace OpenRA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class YamlException : Exception
|
public class YamlException : Exception
|
||||||
{
|
{
|
||||||
public YamlException(string s)
|
public YamlException(string s)
|
||||||
|
|||||||
@@ -10,18 +10,22 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using OpenRA.FileSystem;
|
using OpenRA.FileSystem;
|
||||||
using OpenRA.Graphics;
|
using OpenRA.Graphics;
|
||||||
|
using OpenRA.Primitives;
|
||||||
using OpenRA.Video;
|
using OpenRA.Video;
|
||||||
using OpenRA.Widgets;
|
using OpenRA.Widgets;
|
||||||
using FS = OpenRA.FileSystem.FileSystem;
|
using FS = OpenRA.FileSystem.FileSystem;
|
||||||
|
|
||||||
namespace OpenRA
|
namespace OpenRA
|
||||||
{
|
{
|
||||||
|
public interface IGlobalModData { }
|
||||||
|
|
||||||
public sealed class ModData : IDisposable
|
public sealed class ModData : IDisposable
|
||||||
{
|
{
|
||||||
public readonly Manifest Manifest;
|
public readonly Manifest Manifest;
|
||||||
@@ -31,14 +35,13 @@ namespace OpenRA
|
|||||||
public readonly IPackageLoader[] PackageLoaders;
|
public readonly IPackageLoader[] PackageLoaders;
|
||||||
public readonly ISoundLoader[] SoundLoaders;
|
public readonly ISoundLoader[] SoundLoaders;
|
||||||
public readonly ISpriteLoader[] SpriteLoaders;
|
public readonly ISpriteLoader[] SpriteLoaders;
|
||||||
public readonly ITerrainLoader TerrainLoader;
|
|
||||||
public readonly ISpriteSequenceLoader SpriteSequenceLoader;
|
public readonly ISpriteSequenceLoader SpriteSequenceLoader;
|
||||||
public readonly IVideoLoader[] VideoLoaders;
|
public readonly IVideoLoader[] VideoLoaders;
|
||||||
public readonly HotkeyManager Hotkeys;
|
public readonly HotkeyManager Hotkeys;
|
||||||
public readonly IFileSystemLoader FileSystemLoader;
|
public readonly IFileSystemLoader FileSystemLoader;
|
||||||
|
public readonly FrozenDictionary<string, CursorSequence> Cursors;
|
||||||
|
|
||||||
public ILoadScreen LoadScreen { get; }
|
public ILoadScreen LoadScreen { get; }
|
||||||
public CursorProvider CursorProvider { get; private set; }
|
|
||||||
public FS ModFiles;
|
public FS ModFiles;
|
||||||
public IReadOnlyFileSystem DefaultFileSystem => ModFiles;
|
public IReadOnlyFileSystem DefaultFileSystem => ModFiles;
|
||||||
|
|
||||||
@@ -48,9 +51,11 @@ namespace OpenRA
|
|||||||
readonly Lazy<IReadOnlyDictionary<string, ITerrainInfo>> defaultTerrainInfo;
|
readonly Lazy<IReadOnlyDictionary<string, ITerrainInfo>> defaultTerrainInfo;
|
||||||
public IReadOnlyDictionary<string, ITerrainInfo> DefaultTerrainInfo => defaultTerrainInfo.Value;
|
public IReadOnlyDictionary<string, ITerrainInfo> DefaultTerrainInfo => defaultTerrainInfo.Value;
|
||||||
|
|
||||||
|
readonly TypeDictionary modules = [];
|
||||||
|
|
||||||
public ModData(Manifest mod, InstalledMods mods, bool useLoadScreen = false)
|
public ModData(Manifest mod, InstalledMods mods, bool useLoadScreen = false)
|
||||||
{
|
{
|
||||||
Languages = Array.Empty<string>();
|
Languages = [];
|
||||||
|
|
||||||
// Take a local copy of the manifest
|
// Take a local copy of the manifest
|
||||||
Manifest = new Manifest(mod.Id, mod.Package);
|
Manifest = new Manifest(mod.Id, mod.Package);
|
||||||
@@ -60,53 +65,64 @@ namespace OpenRA
|
|||||||
|
|
||||||
FileSystemLoader = ObjectCreator.GetLoader<IFileSystemLoader>(Manifest.FileSystem.Value, "filesystem");
|
FileSystemLoader = ObjectCreator.GetLoader<IFileSystemLoader>(Manifest.FileSystem.Value, "filesystem");
|
||||||
FieldLoader.Load(FileSystemLoader, Manifest.FileSystem);
|
FieldLoader.Load(FileSystemLoader, Manifest.FileSystem);
|
||||||
FileSystemLoader.Mount(ModFiles, ObjectCreator);
|
FileSystemLoader.Mount(Manifest, ModFiles, ObjectCreator);
|
||||||
ModFiles.TrimExcess();
|
ModFiles.TrimExcess();
|
||||||
|
|
||||||
Manifest.LoadCustomData(ObjectCreator);
|
foreach (var kv in Manifest.GlobalModData)
|
||||||
|
{
|
||||||
|
var t = ObjectCreator.FindType(kv.Key);
|
||||||
|
if (t == null || !typeof(IGlobalModData).IsAssignableFrom(t))
|
||||||
|
throw new InvalidDataException($"`{kv.Key}` is not a valid mod manifest entry.");
|
||||||
|
|
||||||
FluentProvider.Initialize(this, DefaultFileSystem);
|
IGlobalModData module;
|
||||||
|
var ctor = t.GetConstructor([typeof(MiniYaml)]);
|
||||||
|
if (ctor != null)
|
||||||
|
{
|
||||||
|
// Class has opted-in to DIY initialization
|
||||||
|
module = (IGlobalModData)ctor.Invoke([kv.Value]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Automatically load the child nodes using FieldLoader
|
||||||
|
module = ObjectCreator.CreateObject<IGlobalModData>(kv.Key);
|
||||||
|
FieldLoader.Load(module, kv.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
modules.Add(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
FluentProvider.Initialize(Manifest, DefaultFileSystem);
|
||||||
|
|
||||||
if (useLoadScreen)
|
if (useLoadScreen)
|
||||||
{
|
{
|
||||||
LoadScreen = ObjectCreator.CreateObject<ILoadScreen>(Manifest.LoadScreen.Value);
|
LoadScreen = ObjectCreator.CreateObject<ILoadScreen>(Manifest.LoadScreen.Value);
|
||||||
LoadScreen.Init(this, Manifest.LoadScreen.ToDictionary(my => my.Value));
|
LoadScreen.Init(Manifest, DefaultFileSystem);
|
||||||
LoadScreen.Display();
|
LoadScreen.Display();
|
||||||
}
|
}
|
||||||
|
|
||||||
WidgetLoader = new WidgetLoader(this);
|
WidgetLoader = new WidgetLoader(Manifest, DefaultFileSystem);
|
||||||
MapCache = new MapCache(this);
|
MapCache = new MapCache(Manifest, ModFiles);
|
||||||
|
|
||||||
SoundLoaders = ObjectCreator.GetLoaders<ISoundLoader>(Manifest.SoundFormats, "sound");
|
SoundLoaders = ObjectCreator.GetLoaders<ISoundLoader>(Manifest.SoundFormats, "sound");
|
||||||
SpriteLoaders = ObjectCreator.GetLoaders<ISpriteLoader>(Manifest.SpriteFormats, "sprite");
|
SpriteLoaders = ObjectCreator.GetLoaders<ISpriteLoader>(Manifest.SpriteFormats, "sprite");
|
||||||
VideoLoaders = ObjectCreator.GetLoaders<IVideoLoader>(Manifest.VideoFormats, "video");
|
VideoLoaders = ObjectCreator.GetLoaders<IVideoLoader>(Manifest.VideoFormats, "video");
|
||||||
|
SpriteSequenceLoader = ObjectCreator.GetLoader<ISpriteSequenceLoader>(Manifest.SpriteSequenceFormat, "sequence");
|
||||||
var terrainFormat = Manifest.Get<TerrainFormat>();
|
Hotkeys = new HotkeyManager(ModFiles, ObjectCreator, Manifest);
|
||||||
var terrainLoader = ObjectCreator.FindType(terrainFormat.Type + "Loader");
|
Cursors = ParseCursors(Manifest, DefaultFileSystem);
|
||||||
var terrainCtor = terrainLoader?.GetConstructor(new[] { typeof(ModData) });
|
|
||||||
if (terrainLoader == null || !terrainLoader.GetInterfaces().Contains(typeof(ITerrainLoader)) || terrainCtor == null)
|
|
||||||
throw new InvalidOperationException($"Unable to find a terrain loader for type '{terrainFormat.Type}'.");
|
|
||||||
|
|
||||||
TerrainLoader = (ITerrainLoader)terrainCtor.Invoke(new[] { this });
|
|
||||||
|
|
||||||
var sequenceFormat = Manifest.Get<SpriteSequenceFormat>();
|
|
||||||
var sequenceLoader = ObjectCreator.FindType(sequenceFormat.Type + "Loader");
|
|
||||||
var sequenceCtor = sequenceLoader?.GetConstructor(new[] { typeof(ModData) });
|
|
||||||
if (sequenceLoader == null || !sequenceLoader.GetInterfaces().Contains(typeof(ISpriteSequenceLoader)) || sequenceCtor == null)
|
|
||||||
throw new InvalidOperationException($"Unable to find a sequence loader for type '{sequenceFormat.Type}'.");
|
|
||||||
|
|
||||||
SpriteSequenceLoader = (ISpriteSequenceLoader)sequenceCtor.Invoke(new[] { this });
|
|
||||||
|
|
||||||
Hotkeys = new HotkeyManager(ModFiles, Game.Settings.Keys, Manifest);
|
|
||||||
|
|
||||||
defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this));
|
defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this));
|
||||||
defaultTerrainInfo = Exts.Lazy(() =>
|
defaultTerrainInfo = Exts.Lazy(() =>
|
||||||
{
|
{
|
||||||
var items = new Dictionary<string, ITerrainInfo>();
|
var terrainType = ObjectCreator.FindType(Manifest.TerrainFormat + "Loader");
|
||||||
|
var terrainCtor = terrainType?.GetConstructor([typeof(ModData)]);
|
||||||
|
if (terrainType == null || !terrainType.GetInterfaces().Contains(typeof(ITerrainLoader)) || terrainCtor == null)
|
||||||
|
throw new InvalidOperationException($"Unable to find a terrain loader for type '{Manifest.TerrainFormat}'.");
|
||||||
|
|
||||||
|
var items = new Dictionary<string, ITerrainInfo>();
|
||||||
|
var terrainLoader = (ITerrainLoader)terrainCtor.Invoke([this]);
|
||||||
foreach (var file in Manifest.TileSets)
|
foreach (var file in Manifest.TileSets)
|
||||||
{
|
{
|
||||||
var t = TerrainLoader.ParseTerrain(DefaultFileSystem, file);
|
var t = terrainLoader.ParseTerrain(DefaultFileSystem, file);
|
||||||
items.Add(t.Id, t);
|
items.Add(t.Id, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +132,23 @@ namespace OpenRA
|
|||||||
initialThreadId = Environment.CurrentManagedThreadId;
|
initialThreadId = Environment.CurrentManagedThreadId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static FrozenDictionary<string, CursorSequence> ParseCursors(Manifest manifest, IReadOnlyFileSystem fileSystem)
|
||||||
|
{
|
||||||
|
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||||
|
var sequenceYaml = MiniYaml.Merge(manifest.Cursors.Select(
|
||||||
|
s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool)));
|
||||||
|
|
||||||
|
var cursors = new Dictionary<string, CursorSequence>();
|
||||||
|
foreach (var node in sequenceYaml)
|
||||||
|
if (node.Key == "Cursors")
|
||||||
|
foreach (var fileNode in node.Value.Nodes)
|
||||||
|
foreach (var sequenceNode in fileNode.Value.Nodes)
|
||||||
|
cursors.Add(sequenceNode.Key, new CursorSequence(
|
||||||
|
sequenceNode.Key, fileNode.Key, fileNode.Value.Value, sequenceNode.Value));
|
||||||
|
|
||||||
|
return cursors.ToFrozenDictionary();
|
||||||
|
}
|
||||||
|
|
||||||
// HACK: Only update the loading screen if we're in the main thread.
|
// HACK: Only update the loading screen if we're in the main thread.
|
||||||
readonly int initialThreadId;
|
readonly int initialThreadId;
|
||||||
internal void HandleLoadingProgress()
|
internal void HandleLoadingProgress()
|
||||||
@@ -132,26 +165,18 @@ namespace OpenRA
|
|||||||
// horribly when you use ModData in unexpected ways.
|
// horribly when you use ModData in unexpected ways.
|
||||||
ChromeMetrics.Initialize(this);
|
ChromeMetrics.Initialize(this);
|
||||||
ChromeProvider.Initialize(this);
|
ChromeProvider.Initialize(this);
|
||||||
FluentProvider.Initialize(this, fileSystem);
|
Ui.Initialize(this);
|
||||||
|
FluentProvider.Initialize(Manifest, fileSystem);
|
||||||
|
|
||||||
Game.Sound.Initialize(SoundLoaders, fileSystem);
|
Game.Sound.Initialize(SoundLoaders, fileSystem);
|
||||||
|
|
||||||
CursorProvider = new CursorProvider(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> Languages { get; }
|
public IEnumerable<string> Languages { get; }
|
||||||
|
|
||||||
public Map PrepareMap(string uid)
|
public void PrepareMap(Map map)
|
||||||
{
|
{
|
||||||
LoadScreen?.Display();
|
LoadScreen?.Display();
|
||||||
|
|
||||||
if (MapCache[uid].Status != MapStatus.Available)
|
|
||||||
throw new InvalidDataException($"Invalid map uid: {uid}");
|
|
||||||
|
|
||||||
Map map;
|
|
||||||
using (new Support.PerfTimer("Map"))
|
|
||||||
map = new Map(this, MapCache[uid].Package);
|
|
||||||
|
|
||||||
// Reinitialize all our assets
|
// Reinitialize all our assets
|
||||||
InitializeLoaders(map);
|
InitializeLoaders(map);
|
||||||
map.Sequences.LoadSprites();
|
map.Sequences.LoadSprites();
|
||||||
@@ -160,14 +185,37 @@ namespace OpenRA
|
|||||||
using (new Support.PerfTimer("Map.Music"))
|
using (new Support.PerfTimer("Map.Music"))
|
||||||
foreach (var entry in map.Rules.Music)
|
foreach (var entry in map.Rules.Music)
|
||||||
entry.Value.Load(map);
|
entry.Value.Load(map);
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MiniYamlNode>[] GetRulesYaml()
|
public MiniYamlNode[][] GetRulesYaml()
|
||||||
{
|
{
|
||||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||||
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s, stringPool: stringPool)).ToArray();
|
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s, stringPool: stringPool).ToArray()).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Load a cached IGlobalModData instance.</summary>
|
||||||
|
public T GetOrCreate<T>() where T : IGlobalModData
|
||||||
|
{
|
||||||
|
var module = modules.GetOrDefault<T>();
|
||||||
|
|
||||||
|
// Lazily create the default values if not explicitly defined.
|
||||||
|
if (module == null)
|
||||||
|
{
|
||||||
|
module = (T)ObjectCreator.CreateBasic(typeof(T));
|
||||||
|
modules.Add(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T GetOrNull<T>() where T : IGlobalModData
|
||||||
|
{
|
||||||
|
return modules.GetOrDefault<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T GetSettings<T>() where T : SettingsModule
|
||||||
|
{
|
||||||
|
return Game.Settings.GetOrCreate<T>(ObjectCreator, Manifest.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -177,14 +225,18 @@ namespace OpenRA
|
|||||||
|
|
||||||
ObjectCreator?.Dispose();
|
ObjectCreator?.Dispose();
|
||||||
|
|
||||||
Manifest.Dispose();
|
foreach (var module in modules)
|
||||||
|
{
|
||||||
|
var disposableModule = module as IDisposable;
|
||||||
|
disposableModule?.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface ILoadScreen : IDisposable
|
public interface ILoadScreen : IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>Initializes the loadscreen with yaml data from the LoadScreen block in mod.yaml.</summary>
|
/// <summary>Initializes the loadscreen with yaml data from the LoadScreen block in mod.yaml.</summary>
|
||||||
void Init(ModData m, Dictionary<string, string> info);
|
void Init(Manifest manifest, IReadOnlyFileSystem fileSystem);
|
||||||
|
|
||||||
/// <summary>Called at arbitrary times during mod load to rerender the loadscreen.</summary>
|
/// <summary>Called at arbitrary times during mod load to rerender the loadscreen.</summary>
|
||||||
void Display();
|
void Display();
|
||||||
@@ -193,7 +245,7 @@ namespace OpenRA
|
|||||||
/// Called before loading the mod assets.
|
/// Called before loading the mod assets.
|
||||||
/// Returns false if mod loading should be aborted (e.g. switching to another mod instead).
|
/// Returns false if mod loading should be aborted (e.g. switching to another mod instead).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool BeforeLoad();
|
bool BeforeLoad(ModData modData);
|
||||||
|
|
||||||
/// <summary>Called when the engine expects to connect to a server/replay or load the shellmap.</summary>
|
/// <summary>Called when the engine expects to connect to a server/replay or load the shellmap.</summary>
|
||||||
void StartGame(Arguments args);
|
void StartGame(Arguments args);
|
||||||
@@ -201,6 +253,6 @@ namespace OpenRA
|
|||||||
|
|
||||||
public interface IFileSystemLoader
|
public interface IFileSystemLoader
|
||||||
{
|
{
|
||||||
void Mount(FS fileSystem, ObjectCreator objectCreator);
|
void Mount(Manifest manifest, FS fileSystem, ObjectCreator objectCreator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ namespace OpenRA.Network
|
|||||||
public sealed class EchoConnection : IConnection
|
public sealed class EchoConnection : IConnection
|
||||||
{
|
{
|
||||||
const int LocalClientId = 1;
|
const int LocalClientId = 1;
|
||||||
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sync = new();
|
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sync = [];
|
||||||
readonly Queue<(int Frame, OrderPacket Orders)> orders = new();
|
readonly Queue<(int Frame, OrderPacket Orders)> orders = [];
|
||||||
readonly Queue<OrderPacket> immediateOrders = new();
|
readonly Queue<OrderPacket> immediateOrders = [];
|
||||||
bool disposed;
|
bool disposed;
|
||||||
|
|
||||||
int IConnection.LocalClientId => LocalClientId;
|
int IConnection.LocalClientId => LocalClientId;
|
||||||
@@ -52,7 +52,7 @@ namespace OpenRA.Network
|
|||||||
void IConnection.StartGame()
|
void IConnection.StartGame()
|
||||||
{
|
{
|
||||||
// Inject an empty frame to fill the gap we are making by projecting forward orders
|
// Inject an empty frame to fill the gap we are making by projecting forward orders
|
||||||
orders.Enqueue((0, new OrderPacket(Array.Empty<Order>())));
|
orders.Enqueue((0, new OrderPacket([])));
|
||||||
}
|
}
|
||||||
|
|
||||||
void IConnection.Send(int frame, IEnumerable<Order> o)
|
void IConnection.Send(int frame, IEnumerable<Order> o)
|
||||||
@@ -100,12 +100,12 @@ namespace OpenRA.Network
|
|||||||
{
|
{
|
||||||
public readonly ConnectionTarget Target;
|
public readonly ConnectionTarget Target;
|
||||||
internal ReplayRecorder Recorder { get; private set; }
|
internal ReplayRecorder Recorder { get; private set; }
|
||||||
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sentSync = new();
|
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sentSync = [];
|
||||||
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> queuedSyncPackets = new();
|
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> queuedSyncPackets = [];
|
||||||
|
|
||||||
readonly Queue<(int Frame, OrderPacket Orders)> sentOrders = new();
|
readonly Queue<(int Frame, OrderPacket Orders)> sentOrders = [];
|
||||||
readonly Queue<OrderPacket> sentImmediateOrders = new();
|
readonly Queue<OrderPacket> sentImmediateOrders = [];
|
||||||
readonly ConcurrentQueue<(int FromClient, byte[] Data)> receivedPackets = new();
|
readonly ConcurrentQueue<(int FromClient, byte[] Data)> receivedPackets = [];
|
||||||
TcpClient tcp;
|
TcpClient tcp;
|
||||||
volatile ConnectionState connectionState = ConnectionState.Connecting;
|
volatile ConnectionState connectionState = ConnectionState.Connecting;
|
||||||
volatile int clientId;
|
volatile int clientId;
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ namespace OpenRA.Network
|
|||||||
|
|
||||||
public ConnectionTarget()
|
public ConnectionTarget()
|
||||||
{
|
{
|
||||||
endpoints = new[] { new DnsEndPoint("invalid", 0) };
|
endpoints = [new DnsEndPoint("invalid", 0)];
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConnectionTarget(string host, int port)
|
public ConnectionTarget(string host, int port)
|
||||||
{
|
{
|
||||||
endpoints = new[] { new DnsEndPoint(host, port) };
|
endpoints = [new DnsEndPoint(host, port)];
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConnectionTarget(IEnumerable<DnsEndPoint> endpoints)
|
public ConnectionTarget(IEnumerable<DnsEndPoint> endpoints)
|
||||||
@@ -48,7 +48,7 @@ namespace OpenRA.Network
|
|||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
return Enumerable.Empty<IPEndPoint>();
|
return [];
|
||||||
}
|
}
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using Linguini.Shared.Types.Bundle;
|
using Linguini.Shared.Types.Bundle;
|
||||||
|
|
||||||
namespace OpenRA.Network
|
namespace OpenRA.Network
|
||||||
@@ -55,14 +56,16 @@ namespace OpenRA.Network
|
|||||||
public readonly string Key = string.Empty;
|
public readonly string Key = string.Empty;
|
||||||
|
|
||||||
[FieldLoader.LoadUsing(nameof(LoadArguments))]
|
[FieldLoader.LoadUsing(nameof(LoadArguments))]
|
||||||
public readonly object[] Arguments;
|
public readonly ImmutableArray<object> Arguments;
|
||||||
|
|
||||||
static object LoadArguments(MiniYaml yaml)
|
static object LoadArguments(MiniYaml yaml)
|
||||||
{
|
{
|
||||||
var arguments = new List<object>();
|
|
||||||
var argumentsNode = yaml.NodeWithKeyOrDefault("Arguments");
|
var argumentsNode = yaml.NodeWithKeyOrDefault("Arguments");
|
||||||
if (argumentsNode != null)
|
|
||||||
{
|
if (argumentsNode == null)
|
||||||
|
return ImmutableArray<object>.Empty;
|
||||||
|
|
||||||
|
var arguments = new List<object>(argumentsNode.Value.Nodes.Length * 2);
|
||||||
foreach (var argumentNode in argumentsNode.Value.Nodes)
|
foreach (var argumentNode in argumentsNode.Value.Nodes)
|
||||||
{
|
{
|
||||||
var argument = FieldLoader.Load<FluentArgument>(argumentNode.Value);
|
var argument = FieldLoader.Load<FluentArgument>(argumentNode.Value);
|
||||||
@@ -77,9 +80,8 @@ namespace OpenRA.Network
|
|||||||
else
|
else
|
||||||
arguments.Add(argument.Value);
|
arguments.Add(argument.Value);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return arguments.ToArray();
|
return arguments.ToImmutableArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public FluentMessage(MiniYaml yaml)
|
public FluentMessage(MiniYaml yaml)
|
||||||
|
|||||||
@@ -86,22 +86,23 @@ namespace OpenRA.Network
|
|||||||
// Loaded from file and updated during gameplay
|
// Loaded from file and updated during gameplay
|
||||||
public int LastOrdersFrame { get; private set; }
|
public int LastOrdersFrame { get; private set; }
|
||||||
public int LastSyncFrame { get; private set; }
|
public int LastSyncFrame { get; private set; }
|
||||||
byte[] lastSyncPacket = Array.Empty<byte>();
|
byte[] lastSyncPacket = [];
|
||||||
|
|
||||||
// Loaded from file or set on game start
|
// Loaded from file or set on game start
|
||||||
public Session.Global GlobalSettings { get; private set; }
|
public Session.Global GlobalSettings { get; private set; }
|
||||||
public Dictionary<string, Session.Slot> Slots { get; private set; }
|
public Dictionary<string, Session.Slot> Slots { get; private set; }
|
||||||
public Dictionary<string, SlotClient> SlotClients { get; private set; }
|
public Dictionary<string, SlotClient> SlotClients { get; private set; }
|
||||||
public Dictionary<int, MiniYaml> TraitData = new();
|
public Dictionary<int, MiniYaml> TraitData = [];
|
||||||
|
public MapGenerationArgs MapGenerationArgs;
|
||||||
|
|
||||||
// Set on game start
|
// Set on game start
|
||||||
int[] clientsBySlotIndex = Array.Empty<int>();
|
int[] clientsBySlotIndex = [];
|
||||||
int firstBotSlotIndex = -1;
|
int firstBotSlotIndex = -1;
|
||||||
|
|
||||||
public GameSave()
|
public GameSave()
|
||||||
{
|
{
|
||||||
LastOrdersFrame = -1;
|
LastOrdersFrame = -1;
|
||||||
Slots = new Dictionary<string, Session.Slot>();
|
Slots = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameSave(string filepath)
|
public GameSave(string filepath)
|
||||||
@@ -123,10 +124,10 @@ namespace OpenRA.Network
|
|||||||
lastSyncPacket = rs.ReadBytes(Order.SyncHashOrderLength);
|
lastSyncPacket = rs.ReadBytes(Order.SyncHashOrderLength);
|
||||||
|
|
||||||
var globalSettings = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:globalSettings");
|
var globalSettings = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:globalSettings");
|
||||||
GlobalSettings = Session.Global.Deserialize(globalSettings[0].Value);
|
GlobalSettings = Session.Global.Deserialize(globalSettings.First().Value);
|
||||||
|
|
||||||
var slots = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:slots");
|
var slots = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:slots");
|
||||||
Slots = new Dictionary<string, Session.Slot>();
|
Slots = [];
|
||||||
foreach (var s in slots)
|
foreach (var s in slots)
|
||||||
{
|
{
|
||||||
var slot = Session.Slot.Deserialize(s.Value);
|
var slot = Session.Slot.Deserialize(s.Value);
|
||||||
@@ -134,13 +135,17 @@ namespace OpenRA.Network
|
|||||||
}
|
}
|
||||||
|
|
||||||
var slotClients = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:slotClients");
|
var slotClients = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:slotClients");
|
||||||
SlotClients = new Dictionary<string, SlotClient>();
|
SlotClients = [];
|
||||||
foreach (var s in slotClients)
|
foreach (var s in slotClients)
|
||||||
{
|
{
|
||||||
var slotClient = SlotClient.Deserialize(s.Value);
|
var slotClient = SlotClient.Deserialize(s.Value);
|
||||||
SlotClients.Add(slotClient.Slot, slotClient);
|
SlotClients.Add(slotClient.Slot, slotClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mapGenerationArgs = rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength);
|
||||||
|
if (!string.IsNullOrEmpty(mapGenerationArgs))
|
||||||
|
MapGenerationArgs = FieldLoader.Load<MapGenerationArgs>(new MiniYaml("", MiniYaml.FromString(mapGenerationArgs, $"{filepath}:mapGenerationArgs")));
|
||||||
|
|
||||||
if (rs.Position != traitDataOffset || rs.ReadInt32() != TraitDataMarker)
|
if (rs.Position != traitDataOffset || rs.ReadInt32() != TraitDataMarker)
|
||||||
throw new InvalidDataException("Invalid orasav file");
|
throw new InvalidDataException("Invalid orasav file");
|
||||||
|
|
||||||
@@ -155,6 +160,9 @@ namespace OpenRA.Network
|
|||||||
|
|
||||||
public void StartGame(Session lobbyInfo, MapPreview map)
|
public void StartGame(Session lobbyInfo, MapPreview map)
|
||||||
{
|
{
|
||||||
|
if (map.Class == MapClassification.Generated)
|
||||||
|
MapGenerationArgs = map.GenerationArgs;
|
||||||
|
|
||||||
// Game orders are mapped from a client index to the slot that they occupy
|
// Game orders are mapped from a client index to the slot that they occupy
|
||||||
// Orders from spectators are ignored, which is not a problem in practice
|
// Orders from spectators are ignored, which is not a problem in practice
|
||||||
// because all immediate orders are also ignored
|
// because all immediate orders are also ignored
|
||||||
@@ -166,8 +174,8 @@ namespace OpenRA.Network
|
|||||||
|
|
||||||
// Perform a deep clone by round-tripping the data
|
// Perform a deep clone by round-tripping the data
|
||||||
GlobalSettings = Session.Global.Deserialize(lobbyInfo.GlobalSettings.Serialize().Value);
|
GlobalSettings = Session.Global.Deserialize(lobbyInfo.GlobalSettings.Serialize().Value);
|
||||||
Slots = new Dictionary<string, Session.Slot>();
|
Slots = [];
|
||||||
SlotClients = new Dictionary<string, SlotClient>();
|
SlotClients = [];
|
||||||
foreach (var s in lobbyInfo.Slots)
|
foreach (var s in lobbyInfo.Slots)
|
||||||
{
|
{
|
||||||
Slots[s.Key] = Session.Slot.Deserialize(s.Value.Serialize().Value);
|
Slots[s.Key] = Session.Slot.Deserialize(s.Value.Serialize().Value);
|
||||||
@@ -306,6 +314,8 @@ namespace OpenRA.Network
|
|||||||
.ToList();
|
.ToList();
|
||||||
file.WriteLengthPrefixedString(Encoding.UTF8, slotClientNodes.WriteToString());
|
file.WriteLengthPrefixedString(Encoding.UTF8, slotClientNodes.WriteToString());
|
||||||
|
|
||||||
|
file.WriteLengthPrefixedString(Encoding.UTF8, MapGenerationArgs?.Serialize().WriteToString() ?? string.Empty);
|
||||||
|
|
||||||
var traitDataOffset = file.Length;
|
var traitDataOffset = file.Length;
|
||||||
file.Write(TraitDataMarker);
|
file.Write(TraitDataMarker);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using OpenRA.Primitives;
|
using OpenRA.Primitives;
|
||||||
@@ -47,8 +49,9 @@ namespace OpenRA.Network
|
|||||||
|
|
||||||
public class GameServer
|
public class GameServer
|
||||||
{
|
{
|
||||||
static readonly string[] SerializeFields =
|
static readonly ImmutableArray<string> SerializeFields =
|
||||||
{
|
[
|
||||||
|
|
||||||
// Server information
|
// Server information
|
||||||
"Name", "Address",
|
"Name", "Address",
|
||||||
|
|
||||||
@@ -57,7 +60,7 @@ namespace OpenRA.Network
|
|||||||
|
|
||||||
// Current server state
|
// Current server state
|
||||||
"Map", "State", "MaxPlayers", "Protected", "Authentication", "DisabledSpawnPoints"
|
"Map", "State", "MaxPlayers", "Protected", "Authentication", "DisabledSpawnPoints"
|
||||||
};
|
];
|
||||||
|
|
||||||
public const int ProtocolVersion = 2;
|
public const int ProtocolVersion = 2;
|
||||||
|
|
||||||
@@ -130,10 +133,10 @@ namespace OpenRA.Network
|
|||||||
public readonly bool IsJoinable = false;
|
public readonly bool IsJoinable = false;
|
||||||
|
|
||||||
[FieldLoader.LoadUsing(nameof(LoadClients))]
|
[FieldLoader.LoadUsing(nameof(LoadClients))]
|
||||||
public readonly GameClient[] Clients;
|
public readonly ImmutableArray<GameClient> Clients;
|
||||||
|
|
||||||
/// <summary>The list of spawnpoints that are disabled for this game.</summary>
|
/// <summary>The list of spawnpoints that are disabled for this game.</summary>
|
||||||
public readonly int[] DisabledSpawnPoints = Array.Empty<int>();
|
public readonly FrozenSet<int> DisabledSpawnPoints = FrozenSet<int>.Empty;
|
||||||
|
|
||||||
public string ModLabel => $"{ModTitle} ({Version})";
|
public string ModLabel => $"{ModTitle} ({Version})";
|
||||||
|
|
||||||
@@ -149,7 +152,7 @@ namespace OpenRA.Network
|
|||||||
clients.Add(FieldLoader.Load<GameClient>(client.Value));
|
clients.Add(FieldLoader.Load<GameClient>(client.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
return clients.ToArray();
|
return clients.ToImmutableArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameServer(MiniYaml yaml)
|
public GameServer(MiniYaml yaml)
|
||||||
@@ -226,9 +229,9 @@ namespace OpenRA.Network
|
|||||||
ModWebsite = manifest.Metadata.Website;
|
ModWebsite = manifest.Metadata.Website;
|
||||||
ModIcon32 = manifest.Metadata.WebIcon32;
|
ModIcon32 = manifest.Metadata.WebIcon32;
|
||||||
Protected = !string.IsNullOrEmpty(server.Settings.Password);
|
Protected = !string.IsNullOrEmpty(server.Settings.Password);
|
||||||
Authentication = server.Settings.RequireAuthentication || server.Settings.ProfileIDWhitelist.Length > 0;
|
Authentication = server.Settings.RequireAuthentication || server.Settings.ProfileIDWhitelist.Count > 0;
|
||||||
Clients = server.LobbyInfo.Clients.Select(c => new GameClient(c)).ToArray();
|
Clients = server.LobbyInfo.Clients.Select(c => new GameClient(c)).ToImmutableArray();
|
||||||
DisabledSpawnPoints = server.LobbyInfo.DisabledSpawnPoints?.ToArray() ?? Array.Empty<int>();
|
DisabledSpawnPoints = server.LobbyInfo.DisabledSpawnPoints?.ToFrozenSet() ?? FrozenSet<int>.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ToPOSTData(bool lanGame)
|
public string ToPOSTData(bool lanGame)
|
||||||
|
|||||||
@@ -269,6 +269,11 @@ namespace OpenRA
|
|||||||
return new Order(order, null, false) { IsImmediate = isImmediate, TargetString = targetString };
|
return new Order(order, null, false) { IsImmediate = isImmediate, TargetString = targetString };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Order FromTargetString(string order, string targetString, bool isImmediate, uint extraData)
|
||||||
|
{
|
||||||
|
return new Order(order, null, false) { IsImmediate = isImmediate, TargetString = targetString, ExtraData = extraData };
|
||||||
|
}
|
||||||
|
|
||||||
public static Order FromGroupedOrder(Order grouped, Actor subject)
|
public static Order FromGroupedOrder(Order grouped, Actor subject)
|
||||||
{
|
{
|
||||||
return new Order(
|
return new Order(
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ namespace OpenRA.Network
|
|||||||
|
|
||||||
public static class OrderIO
|
public static class OrderIO
|
||||||
{
|
{
|
||||||
static readonly OrderPacket NoOrders = new(Array.Empty<Order>());
|
static readonly OrderPacket NoOrders = new([]);
|
||||||
|
|
||||||
public static byte[] SerializeSync((int Frame, int SyncHash, ulong DefeatState) data)
|
public static byte[] SerializeSync((int Frame, int SyncHash, ulong DefeatState) data)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Frozen;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -26,8 +27,8 @@ namespace OpenRA.Network
|
|||||||
const string DesyncCompareLogs = "notification-desync-compare-logs";
|
const string DesyncCompareLogs = "notification-desync-compare-logs";
|
||||||
|
|
||||||
readonly SyncReport syncReport;
|
readonly SyncReport syncReport;
|
||||||
readonly Dictionary<int, Queue<(int Frame, OrderPacket Orders)>> pendingOrders = new();
|
readonly Dictionary<int, Queue<(int Frame, OrderPacket Orders)>> pendingOrders = [];
|
||||||
readonly Dictionary<int, (int SyncHash, ulong DefeatState)> syncForFrame = new();
|
readonly Dictionary<int, (int SyncHash, ulong DefeatState)> syncForFrame = [];
|
||||||
|
|
||||||
public Session LobbyInfo = new();
|
public Session LobbyInfo = new();
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ namespace OpenRA.Network
|
|||||||
public bool AuthenticationFailed = false;
|
public bool AuthenticationFailed = false;
|
||||||
|
|
||||||
// The default null means "no map restriction" while an empty set means "all maps restricted"
|
// The default null means "no map restriction" while an empty set means "all maps restricted"
|
||||||
public HashSet<string> ServerMapPool = null;
|
public FrozenSet<string> ServerMapPool = null;
|
||||||
|
|
||||||
public int NetFrameNumber { get; private set; }
|
public int NetFrameNumber { get; private set; }
|
||||||
public int LocalFrameNumber;
|
public int LocalFrameNumber;
|
||||||
@@ -53,11 +54,11 @@ namespace OpenRA.Network
|
|||||||
internal int GameSaveLastFrame = -1;
|
internal int GameSaveLastFrame = -1;
|
||||||
internal int GameSaveLastSyncFrame = -1;
|
internal int GameSaveLastSyncFrame = -1;
|
||||||
|
|
||||||
readonly List<Order> localOrders = new();
|
readonly List<Order> localOrders = [];
|
||||||
readonly List<Order> localImmediateOrders = new();
|
readonly List<Order> localImmediateOrders = [];
|
||||||
|
|
||||||
readonly List<ClientOrder> processClientOrders = new();
|
readonly List<ClientOrder> processClientOrders = [];
|
||||||
readonly List<int> processClientsToRemove = new();
|
readonly List<int> processClientsToRemove = [];
|
||||||
|
|
||||||
bool disposed;
|
bool disposed;
|
||||||
bool generateSyncReport = false;
|
bool generateSyncReport = false;
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ namespace OpenRA.Network
|
|||||||
public (int ClientId, byte[] Packet)[] Packets;
|
public (int ClientId, byte[] Packet)[] Packets;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly Queue<Chunk> chunks = new();
|
readonly Queue<Chunk> chunks = [];
|
||||||
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sync = new();
|
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sync = [];
|
||||||
readonly int orderLatency;
|
readonly int orderLatency;
|
||||||
|
|
||||||
public readonly int TickCount;
|
public readonly int TickCount;
|
||||||
@@ -87,7 +87,7 @@ namespace OpenRA.Network
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var gameSpeeds = Game.ModData.Manifest.Get<GameSpeeds>();
|
var gameSpeeds = Game.ModData.GetOrCreate<GameSpeeds>();
|
||||||
var gameSpeedName = LobbyInfo.GlobalSettings.OptionOrDefault("gamespeed", gameSpeeds.DefaultSpeed);
|
var gameSpeedName = LobbyInfo.GlobalSettings.OptionOrDefault("gamespeed", gameSpeeds.DefaultSpeed);
|
||||||
orderLatency = gameSpeeds.Speeds[gameSpeedName].OrderLatency;
|
orderLatency = gameSpeeds.Speeds[gameSpeedName].OrderLatency;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ namespace OpenRA.Network
|
|||||||
{
|
{
|
||||||
public class Session
|
public class Session
|
||||||
{
|
{
|
||||||
public List<Client> Clients = new();
|
public List<Client> Clients = [];
|
||||||
|
|
||||||
// Keyed by the PlayerReference id that the slot corresponds to
|
// Keyed by the PlayerReference id that the slot corresponds to
|
||||||
public Dictionary<string, Slot> Slots = new();
|
public Dictionary<string, Slot> Slots = [];
|
||||||
|
|
||||||
public HashSet<int> DisabledSpawnPoints = new();
|
public HashSet<int> DisabledSpawnPoints = [];
|
||||||
|
|
||||||
public Global GlobalSettings = new();
|
public Global GlobalSettings = new();
|
||||||
|
|
||||||
@@ -213,15 +213,16 @@ namespace OpenRA.Network
|
|||||||
public bool AllowSpectators = true;
|
public bool AllowSpectators = true;
|
||||||
public string GameUid;
|
public string GameUid;
|
||||||
public bool EnableSingleplayer;
|
public bool EnableSingleplayer;
|
||||||
|
public bool EnableMapGeneration;
|
||||||
|
public bool EnableGameSaves;
|
||||||
public bool EnableSyncReports;
|
public bool EnableSyncReports;
|
||||||
public bool Dedicated;
|
public bool Dedicated;
|
||||||
public bool GameSavesEnabled;
|
|
||||||
|
|
||||||
// 120ms network frame interval for 40ms local tick
|
// 120ms network frame interval for 40ms local tick
|
||||||
public int NetFrameInterval = 3;
|
public int NetFrameInterval = 3;
|
||||||
|
|
||||||
[FieldLoader.Ignore]
|
[FieldLoader.Ignore]
|
||||||
public Dictionary<string, LobbyOptionState> LobbyOptions = new();
|
public Dictionary<string, LobbyOptionState> LobbyOptions = [];
|
||||||
|
|
||||||
public static Global Deserialize(MiniYaml data)
|
public static Global Deserialize(MiniYaml data)
|
||||||
{
|
{
|
||||||
@@ -240,7 +241,7 @@ namespace OpenRA.Network
|
|||||||
var data = new MiniYamlNode("GlobalSettings", FieldSaver.Save(this));
|
var data = new MiniYamlNode("GlobalSettings", FieldSaver.Save(this));
|
||||||
var options = LobbyOptions.Select(kv => new MiniYamlNode(kv.Key, FieldSaver.Save(kv.Value)));
|
var options = LobbyOptions.Select(kv => new MiniYamlNode(kv.Key, FieldSaver.Save(kv.Value)));
|
||||||
data = data.WithValue(data.Value.WithNodesAppended(
|
data = data.WithValue(data.Value.WithNodesAppended(
|
||||||
new[] { new MiniYamlNode("Options", new MiniYaml(null, options)) }));
|
[new MiniYamlNode("Options", new MiniYaml(null, options))]));
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,9 +166,9 @@ namespace OpenRA.Network
|
|||||||
public int Frame;
|
public int Frame;
|
||||||
public int SyncedRandom;
|
public int SyncedRandom;
|
||||||
public int TotalCount;
|
public int TotalCount;
|
||||||
public readonly List<TraitReport> Traits = new();
|
public readonly List<TraitReport> Traits = [];
|
||||||
public readonly List<EffectReport> Effects = new();
|
public readonly List<EffectReport> Effects = [];
|
||||||
public readonly List<OrderManager.ClientOrder> Orders = new();
|
public readonly List<OrderManager.ClientOrder> Orders = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TraitReport
|
struct TraitReport
|
||||||
@@ -202,10 +202,10 @@ namespace OpenRA.Network
|
|||||||
{
|
{
|
||||||
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||||
var fields = type.GetFields(Flags)
|
var fields = type.GetFields(Flags)
|
||||||
.Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>())
|
.Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<VerifySyncAttribute>())
|
||||||
.ToList();
|
.ToList();
|
||||||
var properties = type.GetProperties(Flags)
|
var properties = type.GetProperties(Flags)
|
||||||
.Where(pi => pi.HasAttribute<SyncAttribute>())
|
.Where(pi => pi.HasAttribute<VerifySyncAttribute>())
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var prop in properties)
|
foreach (var prop in properties)
|
||||||
@@ -238,11 +238,11 @@ namespace OpenRA.Network
|
|||||||
// PERF: If the member is a Boolean, we can also avoid the allocation caused by boxing it.
|
// PERF: If the member is a Boolean, we can also avoid the allocation caused by boxing it.
|
||||||
// Instead, we can just return the resulting strings directly.
|
// Instead, we can just return the resulting strings directly.
|
||||||
var getBoolString = Expression.Condition(getMember, TrueString, FalseString);
|
var getBoolString = Expression.Condition(getMember, TrueString, FalseString);
|
||||||
return Expression.Lambda<Func<ISync, string>>(getBoolString, name, new[] { SyncParam }).Compile();
|
return Expression.Lambda<Func<ISync, string>>(getBoolString, name, [SyncParam]).Compile();
|
||||||
}
|
}
|
||||||
|
|
||||||
var boxedCopy = Expression.Convert(getMember, typeof(object));
|
var boxedCopy = Expression.Convert(getMember, typeof(object));
|
||||||
return Expression.Lambda<Func<ISync, object>>(boxedCopy, name, new[] { SyncParam }).Compile();
|
return Expression.Lambda<Func<ISync, object>>(boxedCopy, name, [SyncParam]).Compile();
|
||||||
}
|
}
|
||||||
|
|
||||||
// For reference types, we have to call ToString right away to get a snapshot of the value. We cannot
|
// For reference types, we have to call ToString right away to get a snapshot of the value. We cannot
|
||||||
@@ -266,13 +266,13 @@ namespace OpenRA.Network
|
|||||||
// (ISync sync) => { var foo = ((TSync)sync).Foo; return foo == null ? null : foo.ToString(); }
|
// (ISync sync) => { var foo = ((TSync)sync).Foo; return foo == null ? null : foo.ToString(); }
|
||||||
var memberVariable = Expression.Variable(memberType, getMember.Member.Name);
|
var memberVariable = Expression.Variable(memberType, getMember.Member.Name);
|
||||||
var assignMemberVariable = Expression.Assign(memberVariable, getMember);
|
var assignMemberVariable = Expression.Assign(memberVariable, getMember);
|
||||||
var member = Expression.Block(new[] { memberVariable }, assignMemberVariable);
|
var member = Expression.Block([memberVariable], assignMemberVariable);
|
||||||
getString = Expression.Call(member, toString);
|
getString = Expression.Call(member, toString);
|
||||||
var nullMember = Expression.Constant(null, memberType);
|
var nullMember = Expression.Constant(null, memberType);
|
||||||
getString = Expression.Condition(Expression.Equal(member, nullMember), NullString, getString);
|
getString = Expression.Condition(Expression.Equal(member, nullMember), NullString, getString);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Expression.Lambda<Func<ISync, string>>(getString, name, new[] { SyncParam }).Compile();
|
return Expression.Lambda<Func<ISync, string>>(getString, name, [SyncParam]).Compile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user