Compare commits
431 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a65e3c7b8 | ||
|
|
970c9c7a8f | ||
|
|
150e28a672 | ||
|
|
cf21c8e906 | ||
|
|
7859b913bc | ||
|
|
2481bddf58 | ||
|
|
c547f3f26d | ||
|
|
a4bb58007f | ||
|
|
ed5c7bb836 | ||
|
|
af81dca3ff | ||
|
|
188f0e2451 | ||
|
|
0c43801a2c | ||
|
|
8f985118cd | ||
|
|
ec09e618ff | ||
|
|
799c4c9e3c | ||
|
|
25a6b4b6b9 | ||
|
|
714f2c6dc2 | ||
|
|
e04439ea14 | ||
|
|
24093fd0c5 | ||
|
|
c18d10c846 | ||
|
|
4fca85f63d | ||
|
|
dc0f26a1cd | ||
|
|
d4457a4028 | ||
|
|
6c032bb8f7 | ||
|
|
a3d0a50f4d | ||
|
|
519db10f61 | ||
|
|
00a23e6c11 | ||
|
|
6e89bef657 | ||
|
|
5f97e2de5a | ||
|
|
7b82d85b27 | ||
|
|
ac610c54eb | ||
|
|
63247d2d11 | ||
|
|
d3c44de5d2 | ||
|
|
ade07607a5 | ||
|
|
3760b14235 | ||
|
|
a054d2115d | ||
|
|
9d29303142 | ||
|
|
12e1d327ef | ||
|
|
09834d3954 | ||
|
|
8fda46e241 | ||
|
|
8993901641 | ||
|
|
2fe13fe442 | ||
|
|
1a4f366e4b | ||
|
|
2d332d0a13 | ||
|
|
d630a6ef7d | ||
|
|
0c22499534 | ||
|
|
4077f28285 | ||
|
|
4e031a6ea5 | ||
|
|
311d55ff45 | ||
|
|
64cdfcbeab | ||
|
|
6026d088c8 | ||
|
|
53e4d0dd87 | ||
|
|
2ced4abc24 | ||
|
|
b58c1ea5bc | ||
|
|
ca6aa5ebf1 | ||
|
|
f979e6da0f | ||
|
|
32121a38f4 | ||
|
|
2fde98a0d1 | ||
|
|
ead78bc3a3 | ||
|
|
00857df990 | ||
|
|
1a037c06bf | ||
|
|
0741439dd6 | ||
|
|
0e5447d6d2 | ||
|
|
680144b24f | ||
|
|
9a1823d805 | ||
|
|
e96865b55e | ||
|
|
8ba144f43a | ||
|
|
1f10dafbea | ||
|
|
da638a495c | ||
|
|
d83a871520 | ||
|
|
a0b1bdd154 | ||
|
|
34ff23d030 | ||
|
|
f7f304a2e0 | ||
|
|
32ad81d0ff | ||
|
|
ea3a62927d | ||
|
|
ba951b6470 | ||
|
|
6c56ea4c55 | ||
|
|
6a86a99fce | ||
|
|
f270cb3bde | ||
|
|
aa5b193746 | ||
|
|
adf515d50b | ||
|
|
da507b2eed | ||
|
|
adf6a81862 | ||
|
|
264564d006 | ||
|
|
02d31a2f2c | ||
|
|
59473bdf9f | ||
|
|
dd7441e0b4 | ||
|
|
ad833a6fbb | ||
|
|
9f196f2693 | ||
|
|
ac6934405e | ||
|
|
d8100cb9f2 | ||
|
|
2733ed4b1c | ||
|
|
018777472a | ||
|
|
20f6e01afe | ||
|
|
3904576574 | ||
|
|
c4acd8b361 | ||
|
|
8529512edb | ||
|
|
c20cffad5c | ||
|
|
07ed6a889e | ||
|
|
d67e0a4eef | ||
|
|
8e7fa26709 | ||
|
|
deacc7ad65 | ||
|
|
65361ed8dc | ||
|
|
bb1e830264 | ||
|
|
ca6b87d05e | ||
|
|
855568cab7 | ||
|
|
6b0db6699d | ||
|
|
ab9b393238 | ||
|
|
61b124ddf5 | ||
|
|
678b238c1c | ||
|
|
304fc458eb | ||
|
|
caad8ba44b | ||
|
|
db8a28f2c0 | ||
|
|
0f5b78442b | ||
|
|
a5e472dfe6 | ||
|
|
4b7036be0f | ||
|
|
6386e96134 | ||
|
|
b267374d20 | ||
|
|
342fc5b0e9 | ||
|
|
ff49411bc1 | ||
|
|
c80020f451 | ||
|
|
54c2f7d2b4 | ||
|
|
59ad9e3cd7 | ||
|
|
ead6bdecb6 | ||
|
|
104801cdca | ||
|
|
e6914f707a | ||
|
|
acca837142 | ||
|
|
330ca92045 | ||
|
|
499efa1d0a | ||
|
|
89e1d71aec | ||
|
|
2faae285db | ||
|
|
bdef619803 | ||
|
|
3f4f9e7354 | ||
|
|
3b67e425ed | ||
|
|
c8efc5fdd7 | ||
|
|
c2568ebd1f | ||
|
|
2996a1ddde | ||
|
|
2ea2106eca | ||
|
|
9f526610dd | ||
|
|
3259737774 | ||
|
|
360f24f609 | ||
|
|
60cbf79c9b | ||
|
|
d98017c140 | ||
|
|
c0ae7ea497 | ||
|
|
ac53b89421 | ||
|
|
46ba8ef5dd | ||
|
|
cc0f116194 | ||
|
|
8dc255f401 | ||
|
|
dd4bbc3546 | ||
|
|
db0aabcb88 | ||
|
|
a086fdaa5b | ||
|
|
ee6f8ae45d | ||
|
|
91802e6f10 | ||
|
|
d1797a021f | ||
|
|
3ae617c55b | ||
|
|
f6614c1c58 | ||
|
|
889de5e08a | ||
|
|
b97d1a4c6c | ||
|
|
cfde0d7867 | ||
|
|
399cef8fb2 | ||
|
|
58e447d8d0 | ||
|
|
43f339b91e | ||
|
|
9534443771 | ||
|
|
39755a2fce | ||
|
|
73be3641ea | ||
|
|
03b413a892 | ||
|
|
31c37662cf | ||
|
|
57a452a705 | ||
|
|
9d174cd87d | ||
|
|
9a3c39878d | ||
|
|
498c6e3d8b | ||
|
|
25cb3728ca | ||
|
|
fbe147ce61 | ||
|
|
eb287d9b8d | ||
|
|
4dd787be13 | ||
|
|
5d91b678bb | ||
|
|
b3ee3551ca | ||
|
|
2e5ef7f059 | ||
|
|
72646fc7ff | ||
|
|
01fec1ae02 | ||
|
|
3be1de230c | ||
|
|
e83e580f23 | ||
|
|
8e80117eb8 | ||
|
|
0c2d060d43 | ||
|
|
5157bc375d | ||
|
|
b35b560ca1 | ||
|
|
c0da41a18a | ||
|
|
d9f5588a1f | ||
|
|
61c3c252ea | ||
|
|
ed3ca78667 | ||
|
|
6fb7bb1c08 | ||
|
|
57cef527ba | ||
|
|
48a2a75211 | ||
|
|
3f0159cd89 | ||
|
|
7baae40b2d | ||
|
|
fc0bdce151 | ||
|
|
64de28427c | ||
|
|
c4ca3ca743 | ||
|
|
724511e244 | ||
|
|
e3646595ab | ||
|
|
d2ecd0c777 | ||
|
|
a24308baa5 | ||
|
|
aa8e85fbf4 | ||
|
|
11a892f991 | ||
|
|
cf255fc78e | ||
|
|
258de7a6fd | ||
|
|
fcfee31972 | ||
|
|
11b59b0a65 | ||
|
|
0bb2bc651b | ||
|
|
c63788b686 | ||
|
|
60e86f563c | ||
|
|
ce39e97b86 | ||
|
|
06aa378dfd | ||
|
|
43ebb93ff6 | ||
|
|
4fe2ed3df0 | ||
|
|
1a299d10ed | ||
|
|
d1dc6293e8 | ||
|
|
9f1ea57d3c | ||
|
|
917b0512bf | ||
|
|
b9b5b90330 | ||
|
|
216758dbc7 | ||
|
|
96dc085b35 | ||
|
|
b28a3b6a5a | ||
|
|
500ee54f04 | ||
|
|
dd95b199b7 | ||
|
|
3d9ac5a85e | ||
|
|
8503678fc7 | ||
|
|
37ce5e447f | ||
|
|
44d7903a4b | ||
|
|
a3c0cee2cc | ||
|
|
4cc9b1be2b | ||
|
|
f1fba1ed14 | ||
|
|
3bb42522b8 | ||
|
|
143cd8f856 | ||
|
|
4547f3c2b9 | ||
|
|
c3ff5d954a | ||
|
|
43ddee5d30 | ||
|
|
813a1984f9 | ||
|
|
9a5f5f9f8f | ||
|
|
cb55039ec9 | ||
|
|
a51a9700cf | ||
|
|
59d40c8b4e | ||
|
|
7adcba5b7f | ||
|
|
fe6de396f2 | ||
|
|
47af7a9023 | ||
|
|
b1f5367822 | ||
|
|
cd40d150c1 | ||
|
|
98160512b8 | ||
|
|
9a235f2256 | ||
|
|
754e7845f3 | ||
|
|
cbd6b67456 | ||
|
|
1f0e73906e | ||
|
|
f4d1c924d7 | ||
|
|
1a98312595 | ||
|
|
3bc4a6c9dc | ||
|
|
8b96b75960 | ||
|
|
d69dbd2793 | ||
|
|
cd5eb89ebc | ||
|
|
20c683fb4f | ||
|
|
c427e24360 | ||
|
|
feced5505a | ||
|
|
806eebd269 | ||
|
|
b394e15998 | ||
|
|
13d446e27e | ||
|
|
85d62f7e5e | ||
|
|
7515c180b9 | ||
|
|
36d44925cb | ||
|
|
c0f3f97811 | ||
|
|
4e72026ff9 | ||
|
|
74df2d22da | ||
|
|
68d053336b | ||
|
|
876b66b295 | ||
|
|
5eb6ba6e5c | ||
|
|
1dc14ed9f1 | ||
|
|
72bb6c4c99 | ||
|
|
a960eb471b | ||
|
|
e76d89f0db | ||
|
|
d2fdd3c753 | ||
|
|
30de1cdf5d | ||
|
|
6b151e6be5 | ||
|
|
f5450cdf50 | ||
|
|
4b9de8ac42 | ||
|
|
13a6e027ef | ||
|
|
fc77c3ce48 | ||
|
|
85c8f6c446 | ||
|
|
d349209dc9 | ||
|
|
c8779e2a6b | ||
|
|
b55606c37f | ||
|
|
12fb091bbc | ||
|
|
bc37d7169d | ||
|
|
9ae26f2645 | ||
|
|
6367729f98 | ||
|
|
b8b93af977 | ||
|
|
9f96d0c772 | ||
|
|
d5c940ba4c | ||
|
|
144e716cdf | ||
|
|
686040a316 | ||
|
|
d427072cc9 | ||
|
|
60a446123b | ||
|
|
c009f58980 | ||
|
|
79b10ba9a5 | ||
|
|
d05e0f23ea | ||
|
|
26b6118f50 | ||
|
|
0a90c2a95e | ||
|
|
d77fd5c13e | ||
|
|
4dec79a5fb | ||
|
|
b7e0ed9b87 | ||
|
|
0ab7caedd9 | ||
|
|
3824a591d5 | ||
|
|
3e6123f6f6 | ||
|
|
2763e1502b | ||
|
|
0b90622251 | ||
|
|
9b8895df39 | ||
|
|
f6c1453b5b | ||
|
|
7e9619b41b | ||
|
|
90aeb38427 | ||
|
|
6040187844 | ||
|
|
e72d0ed2c6 | ||
|
|
c3b4e2b237 | ||
|
|
7769764b0b | ||
|
|
b25146265d | ||
|
|
e0df59464e | ||
|
|
a67320e431 | ||
|
|
e41279fe6b | ||
|
|
29eaab59be | ||
|
|
541d53127a | ||
|
|
bdcf754d34 | ||
|
|
a67e85e092 | ||
|
|
24536fa296 | ||
|
|
38ed21edd2 | ||
|
|
5d2f2bdd1d | ||
|
|
6515403ae6 | ||
|
|
2f696b2ce7 | ||
|
|
61d51d971c | ||
|
|
90c7680743 | ||
|
|
b59bb998eb | ||
|
|
9845306b68 | ||
|
|
4eb683ab46 | ||
|
|
9d7feb176a | ||
|
|
eab0bf8f82 | ||
|
|
085a4c421b | ||
|
|
4fc4fb2fb3 | ||
|
|
0e5ed6a30c | ||
|
|
5cc59ae3ac | ||
|
|
161f4cbdff | ||
|
|
5b0f69b411 | ||
|
|
23f3f8d90c | ||
|
|
2ac855488b | ||
|
|
c08ddb61b3 | ||
|
|
fb55f2824e | ||
|
|
1b0c93e5ff | ||
|
|
19c8c36030 | ||
|
|
931118e1d8 | ||
|
|
61f1660b38 | ||
|
|
634cf900e6 | ||
|
|
a148f30070 | ||
|
|
3e0daa62c4 | ||
|
|
aac1bae899 | ||
|
|
f5f2f58664 | ||
|
|
f428a44bfc | ||
|
|
ce412e4404 | ||
|
|
7bd4b4558e | ||
|
|
619fb6633a | ||
|
|
bf64339890 | ||
|
|
d9787b168d | ||
|
|
4a81d9b6f7 | ||
|
|
4370c47f6e | ||
|
|
f69e6289b5 | ||
|
|
bfd0cd7108 | ||
|
|
df534736a1 | ||
|
|
93a97d5d6f | ||
|
|
3275875ae5 | ||
|
|
88f830a9e5 | ||
|
|
c609c4af14 | ||
|
|
94c8339e17 | ||
|
|
b742a776eb | ||
|
|
1899eed839 | ||
|
|
43d1a20d8c | ||
|
|
1692f32ffc | ||
|
|
e07869e71f | ||
|
|
c9dfb215ae | ||
|
|
98896f9a75 | ||
|
|
a14cc8cc4d | ||
|
|
e1940eec77 | ||
|
|
a1dfb42812 | ||
|
|
3b2fad6ea8 | ||
|
|
d9df27d574 | ||
|
|
ae45707c84 | ||
|
|
e22d7b31f9 | ||
|
|
0dcb341059 | ||
|
|
3ecb267594 | ||
|
|
2744b44d93 | ||
|
|
0528ef58b2 | ||
|
|
2a223363b8 | ||
|
|
de9a5eb71e | ||
|
|
2b0afd6acb | ||
|
|
3ab421cbe3 | ||
|
|
54dac39e83 | ||
|
|
2de212710a | ||
|
|
60fbecd4a7 | ||
|
|
82458b5f7e | ||
|
|
d0974cfdd2 | ||
|
|
da16e4ed99 | ||
|
|
55536bba4c | ||
|
|
388222c5c7 | ||
|
|
169c60883b | ||
|
|
285443f10f | ||
|
|
d83e579dfe | ||
|
|
486a07602b | ||
|
|
949ba589c0 | ||
|
|
b6a5d19871 | ||
|
|
ce002ce8c1 | ||
|
|
9c3e366d03 | ||
|
|
bb96e22e64 | ||
|
|
a691f2ebac | ||
|
|
7638822e49 | ||
|
|
a9cf728ee1 | ||
|
|
2c4a135c2b | ||
|
|
d686634c0b | ||
|
|
32b0003a72 | ||
|
|
c234b4c78f | ||
|
|
f2a242b09a | ||
|
|
a1efb28f0b | ||
|
|
d217ab39c2 | ||
|
|
31840328b7 | ||
|
|
54547a11d0 | ||
|
|
f99db8d754 | ||
|
|
1ce916182d | ||
|
|
09ba09f4e3 | ||
|
|
2ac85ac61d | ||
|
|
44e024a94e |
856
.editorconfig
856
.editorconfig
File diff suppressed because it is too large
Load Diff
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -15,10 +15,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check Code
|
||||
run: |
|
||||
@@ -57,10 +57,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
|
||||
147
.github/workflows/documentation.yml
vendored
147
.github/workflows/documentation.yml
vendored
@@ -1,6 +1,9 @@
|
||||
name: Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ bleed ]
|
||||
tags: [ 'release-*', 'playtest-*' ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
@@ -12,18 +15,49 @@ permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
wiki:
|
||||
name: Update Wiki
|
||||
prepare:
|
||||
name: Prepare version strings
|
||||
if: github.repository == 'openra/openra'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Prepare environment variables
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
if [ "${{ github.ref_type }}" = "tag" ]; then
|
||||
VERSION_TYPE=`echo "${GITHUB_REF#refs/tags/}" | cut -d"-" -f1`
|
||||
echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
echo "VERSION_TYPE=$VERSION_TYPE" >> $GITHUB_ENV
|
||||
else
|
||||
echo "GIT_TAG=bleed" >> $GITHUB_ENV
|
||||
echo "VERSION_TYPE=bleed" >> $GITHUB_ENV
|
||||
fi
|
||||
else
|
||||
VERSION_TYPE=`echo "${{ github.event.inputs.tag }}" | cut -d"-" -f1`
|
||||
echo "GIT_TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
|
||||
echo "VERSION_TYPE=$VERSION_TYPE" >> $GITHUB_ENV
|
||||
fi
|
||||
outputs:
|
||||
git_tag: ${{ env.GIT_TAG }}
|
||||
version_type: ${{ env.VERSION_TYPE }}
|
||||
|
||||
wiki:
|
||||
name: Update Wiki
|
||||
needs: prepare
|
||||
if: github.repository == 'openra/openra' && needs.prepare.outputs.version_type != 'bleed'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Debug output
|
||||
run: |
|
||||
echo ${{ needs.prepare.outputs.git_tag }}
|
||||
echo ${{ needs.prepare.outputs.version_type }}
|
||||
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag }}
|
||||
ref: ${{ needs.prepare.outputs.git_tag }}
|
||||
|
||||
- name: Install .NET 6
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
@@ -32,49 +66,53 @@ jobs:
|
||||
make all
|
||||
|
||||
- name: Clone Wiki
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: openra/openra.wiki
|
||||
token: ${{ secrets.DOCS_TOKEN }}
|
||||
path: wiki
|
||||
|
||||
- name: Update Wiki (Playtest)
|
||||
if: startsWith(github.event.inputs.tag, 'playtest-')
|
||||
env:
|
||||
GIT_TAG: ${{ github.event.inputs.tag }}
|
||||
if: startsWith(needs.prepare.outputs.git_tag, 'playtest-')
|
||||
run: |
|
||||
./utility.sh all --settings-docs "${GIT_TAG}" > "wiki/Settings (playtest).md"
|
||||
./utility.sh all --settings-docs "${{ needs.prepare.outputs.git_tag }}" > "wiki/Settings (playtest).md"
|
||||
|
||||
- name: Update Wiki (Release)
|
||||
if: startsWith(github.event.inputs.tag, 'release-')
|
||||
env:
|
||||
GIT_TAG: ${{ github.event.inputs.tag }}
|
||||
if: startsWith(needs.prepare.outputs.git_tag, 'release-')
|
||||
run: |
|
||||
./utility.sh all --settings-docs "${GIT_TAG}" > "wiki/Settings.md"
|
||||
./utility.sh all --settings-docs "${{ needs.prepare.outputs.git_tag }}" > "wiki/Settings.md"
|
||||
|
||||
- name: Push Wiki
|
||||
env:
|
||||
GIT_TAG: ${{ github.event.inputs.tag }}
|
||||
run: |
|
||||
cd wiki
|
||||
git config --local user.email "actions@github.com"
|
||||
git config --local user.name "GitHub Actions"
|
||||
git add --all
|
||||
git commit -m "Update auto-generated documentation for ${GIT_TAG}"
|
||||
git push origin master
|
||||
git status
|
||||
git diff-index --quiet HEAD || \
|
||||
(
|
||||
git add --all && \
|
||||
git commit -m "Update auto-generated documentation for ${{ needs.prepare.outputs.git_tag }}" && \
|
||||
git push origin master
|
||||
)
|
||||
|
||||
docs:
|
||||
name: Update docs.openra.net
|
||||
needs: prepare
|
||||
if: github.repository == 'openra/openra'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Debug output
|
||||
run: |
|
||||
echo ${{ needs.prepare.outputs.git_tag }}
|
||||
echo ${{ needs.prepare.outputs.version_type }}
|
||||
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag }}
|
||||
ref: ${{ needs.prepare.outputs.git_tag }}
|
||||
|
||||
- name: Install .NET 6
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
@@ -82,62 +120,31 @@ jobs:
|
||||
run: |
|
||||
make all
|
||||
|
||||
- name: Clone docs.openra.net (Playtest)
|
||||
if: startsWith(github.event.inputs.tag, 'playtest-')
|
||||
uses: actions/checkout@v3
|
||||
# version_type is release/playtest/bleed - the name of the target branch.
|
||||
- name: Clone docs.openra.net
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: openra/docs
|
||||
token: ${{ secrets.DOCS_TOKEN }}
|
||||
path: docs
|
||||
ref: playtest
|
||||
ref: ${{ needs.prepare.outputs.version_type }}
|
||||
|
||||
- name: Clone docs.openra.net (Release)
|
||||
if: startsWith(github.event.inputs.tag, 'release-')
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: openra/docs
|
||||
token: ${{ secrets.DOCS_TOKEN }}
|
||||
path: docs
|
||||
ref: release
|
||||
|
||||
- name: Update docs.openra.net (Playtest)
|
||||
if: startsWith(github.event.inputs.tag, 'playtest-')
|
||||
env:
|
||||
GIT_TAG: ${{ github.event.inputs.tag }}
|
||||
- name: Generate docs files
|
||||
run: |
|
||||
./utility.sh all --docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/traits.md"
|
||||
./utility.sh all --weapon-docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/weapons.md"
|
||||
./utility.sh all --sprite-sequence-docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/sprite-sequences.md"
|
||||
./utility.sh all --lua-docs "${GIT_TAG}" > "docs/api/lua.md"
|
||||
./utility.sh all --docs "${{ needs.prepare.outputs.git_tag }}" | python3 ./packaging/format-docs.py > "docs/api/traits.md"
|
||||
./utility.sh all --weapon-docs "${{ needs.prepare.outputs.git_tag }}" | python3 ./packaging/format-docs.py > "docs/api/weapons.md"
|
||||
./utility.sh all --sprite-sequence-docs "${{ needs.prepare.outputs.git_tag }}" | python3 ./packaging/format-docs.py > "docs/api/sprite-sequences.md"
|
||||
./utility.sh all --lua-docs "${{ needs.prepare.outputs.git_tag }}" > "docs/api/lua.md"
|
||||
|
||||
- name: Update docs.openra.net (Release)
|
||||
if: startsWith(github.event.inputs.tag, 'release-')
|
||||
env:
|
||||
GIT_TAG: ${{ github.event.inputs.tag }}
|
||||
run: |
|
||||
./utility.sh all --docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/traits.md"
|
||||
./utility.sh all --weapon-docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/weapons.md"
|
||||
./utility.sh all --sprite-sequence-docs "${GIT_TAG}" | python3 ./packaging/format-docs.py > "docs/api/sprite-sequences.md"
|
||||
./utility.sh all --lua-docs "${GIT_TAG}" > "docs/api/lua.md"
|
||||
|
||||
- name: Commit docs.openra.net
|
||||
env:
|
||||
GIT_TAG: ${{ github.event.inputs.tag }}
|
||||
- name: Update docs.openra.net
|
||||
run: |
|
||||
cd docs
|
||||
git config --local user.email "actions@github.com"
|
||||
git config --local user.name "GitHub Actions"
|
||||
git add api/*.md
|
||||
git commit -m "Update auto-generated documentation for ${GIT_TAG}"
|
||||
|
||||
- name: Push docs.openra.net (Release)
|
||||
if: startsWith(github.event.inputs.tag, 'release-')
|
||||
run: |
|
||||
cd docs
|
||||
git push origin release
|
||||
|
||||
- name: Push docs.openra.net (Playtest)
|
||||
if: startsWith(github.event.inputs.tag, 'playtest-')
|
||||
run: |
|
||||
cd docs
|
||||
git push origin playtest
|
||||
git status
|
||||
git diff-index --quiet HEAD || \
|
||||
(
|
||||
git add api/*.md && \
|
||||
git commit -m "Update auto-generated documentation for ${{ needs.prepare.outputs.git_tag }}" && \
|
||||
git push origin ${{ needs.prepare.outputs.version_type }}
|
||||
)
|
||||
|
||||
14
.github/workflows/packaging.yml
vendored
14
.github/workflows/packaging.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare Environment
|
||||
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
|
||||
@@ -40,10 +40,10 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
@@ -70,10 +70,10 @@ jobs:
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
@@ -105,10 +105,10 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install .NET 6.0
|
||||
uses: actions/setup-dotnet@v3
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
|
||||
1
AUTHORS
1
AUTHORS
@@ -150,6 +150,7 @@ Also thanks to:
|
||||
* Teemu Nieminen (Temeez)
|
||||
* Thomas Christlieb (ThomasChr)
|
||||
* Tim Mylemans (gecko)
|
||||
* Tinix
|
||||
* Tirili
|
||||
* Tomas Einarsson (Mesacer)
|
||||
* Tom van Leth (tovl)
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<Optimize>false</Optimize>
|
||||
<!-- Enable only for Debug builds to improve compile-time performance for Release builds -->
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<!-- Enabling GenerateDocumentationFile is required for IDE0005 (Remove unnecessary import)
|
||||
<!-- Enabling GenerateDocumentationFile is required for IDE0005 (Remove unnecessary import)
|
||||
rule to run in command line builds. https://github.com/dotnet/roslyn/issues/41640
|
||||
Enable only for Debug builds to improve compile-time performance for Release builds -->
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
@@ -51,8 +51,10 @@
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<!-- StyleCop -->
|
||||
<!-- StyleCop/Roslynator -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
|
||||
<!-- Roslynator analyzers fail to run under Mono (AD0001) -->
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -146,18 +146,22 @@ namespace OpenRA.Activities
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Called every tick to run activity logic. Returns false if the activity should
|
||||
/// remain active, or true if it is complete. Cancelled activities must ensure they
|
||||
/// return the actor to a consistent state before returning true.
|
||||
///
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Child activities can be queued using QueueChild, and these will be ticked
|
||||
/// instead of the parent while they are active. Activities that need to run logic
|
||||
/// in parallel with child activities should set ChildHasPriority to false and
|
||||
/// manually call TickChildren.
|
||||
///
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Queuing one or more child activities and returning true is valid, and causes
|
||||
/// the activity to be completed immediately (without ticking again) once the
|
||||
/// children have completed.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public virtual bool Tick(Actor self)
|
||||
{
|
||||
@@ -222,10 +226,11 @@ namespace OpenRA.Activities
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints the activity tree, starting from the top or optionally from a given origin.
|
||||
///
|
||||
/// <para>Prints the activity tree, starting from the top or optionally from a given origin.</para>
|
||||
/// <para>
|
||||
/// Call this method from any place that's called during a tick, such as the Tick() method itself or
|
||||
/// the Before(First|Last)Run() methods. The origin activity will be marked in the output.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="self">The actor performing this activity.</param>
|
||||
/// <param name="origin">Activity from which to start traversing, and which to mark. If null, mark the calling activity, and start traversal from the top.</param>
|
||||
|
||||
@@ -71,7 +71,12 @@ namespace OpenRA
|
||||
public IEffectiveOwner EffectiveOwner { get; }
|
||||
public IOccupySpace OccupiesSpace { get; }
|
||||
public ITargetable[] Targetables { get; }
|
||||
public IEnumerable<ITargetablePositions> EnabledTargetablePositions { get; private set; }
|
||||
public IEnumerable<ITargetablePositions> EnabledTargetablePositions { get; }
|
||||
readonly ICrushable[] crushables;
|
||||
public ICrushable[] Crushables
|
||||
{
|
||||
get => crushables ?? throw new InvalidOperationException($"Crushables for {Info.Name} are not initialized.");
|
||||
}
|
||||
|
||||
public bool IsIdle => CurrentActivity == null;
|
||||
public bool IsDead => Disposed || (health != null && health.IsDead);
|
||||
@@ -155,6 +160,7 @@ namespace OpenRA
|
||||
var targetablesList = new List<ITargetable>();
|
||||
var targetablePositionsList = new List<ITargetablePositions>();
|
||||
var syncHashesList = new List<SyncHash>();
|
||||
var crushablesList = new List<ICrushable>();
|
||||
|
||||
foreach (var traitInfo in Info.TraitsInConstructOrder())
|
||||
{
|
||||
@@ -181,6 +187,7 @@ namespace OpenRA
|
||||
{ if (trait is ITargetable t) targetablesList.Add(t); }
|
||||
{ if (trait is ITargetablePositions t) targetablePositionsList.Add(t); }
|
||||
{ if (trait is ISync t) syncHashesList.Add(new SyncHash(t)); }
|
||||
{ if (trait is ICrushable t) crushablesList.Add(t); }
|
||||
}
|
||||
|
||||
resolveOrders = resolveOrdersList.ToArray();
|
||||
@@ -195,6 +202,7 @@ namespace OpenRA
|
||||
EnabledTargetablePositions = targetablePositions.Where(Exts.IsTraitEnabled);
|
||||
enabledTargetableWorldPositions = EnabledTargetablePositions.SelectMany(tp => tp.TargetablePositions(this));
|
||||
SyncHashes = syncHashesList.ToArray();
|
||||
crushables = crushablesList.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,14 +61,14 @@ namespace OpenRA
|
||||
|
||||
public static readonly CVec[] Directions =
|
||||
{
|
||||
new CVec(-1, -1),
|
||||
new CVec(-1, 0),
|
||||
new CVec(-1, 1),
|
||||
new CVec(0, -1),
|
||||
new CVec(0, 1),
|
||||
new CVec(1, -1),
|
||||
new CVec(1, 0),
|
||||
new CVec(1, 1),
|
||||
new(-1, -1),
|
||||
new(-1, 0),
|
||||
new(-1, 1),
|
||||
new(0, -1),
|
||||
new(0, 1),
|
||||
new(1, -1),
|
||||
new(1, 0),
|
||||
new(1, 1),
|
||||
};
|
||||
|
||||
#region Scripting interface
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace OpenRA
|
||||
// Fixed byte pattern for the OID header
|
||||
static readonly byte[] OIDHeader = { 0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0 };
|
||||
|
||||
static readonly char[] HexUpperAlphabet = "0123456789ABCDEF".ToArray();
|
||||
static readonly char[] HexLowerAlphabet = "0123456789abcdef".ToArray();
|
||||
|
||||
public static string PublicKeyFingerprint(RSAParameters parameters)
|
||||
{
|
||||
// Public key fingerprint is defined as the SHA1 of the modulus + exponent bytes
|
||||
@@ -53,33 +56,33 @@ namespace OpenRA
|
||||
using (var s = new MemoryStream(data))
|
||||
{
|
||||
// SEQUENCE
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
ReadTLVLength(s);
|
||||
|
||||
// SEQUENCE -> fixed header junk
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
var headerLength = ReadTLVLength(s);
|
||||
s.Position += headerLength;
|
||||
|
||||
// SEQUENCE -> BIT_STRING
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
ReadTLVLength(s);
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
|
||||
// SEQUENCE -> BIT_STRING -> SEQUENCE
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
ReadTLVLength(s);
|
||||
|
||||
// SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (modulus)
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
var modulusLength = ReadTLVLength(s);
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
var modulus = s.ReadBytes(modulusLength - 1);
|
||||
|
||||
// SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (exponent)
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
var exponentLength = ReadTLVLength(s);
|
||||
s.ReadByte();
|
||||
s.ReadUInt8();
|
||||
var exponent = s.ReadBytes(exponentLength - 1);
|
||||
|
||||
return new RSAParameters
|
||||
@@ -158,13 +161,13 @@ namespace OpenRA
|
||||
|
||||
static int ReadTLVLength(Stream s)
|
||||
{
|
||||
var length = s.ReadByte();
|
||||
var length = s.ReadUInt8();
|
||||
if (length < 0x80)
|
||||
return length;
|
||||
|
||||
var data = new byte[4];
|
||||
s.ReadBytes(data, 0, Math.Min(length & 0x7F, 4));
|
||||
return BitConverter.ToInt32(data.ToArray(), 0);
|
||||
Span<byte> data = stackalloc byte[4];
|
||||
s.ReadBytes(data[..Math.Min(length & 0x7F, 4)]);
|
||||
return BitConverter.ToInt32(data);
|
||||
}
|
||||
|
||||
static int TripletFullLength(int dataLength)
|
||||
@@ -249,19 +252,44 @@ namespace OpenRA
|
||||
|
||||
public static string SHA1Hash(Stream data)
|
||||
{
|
||||
using (var csp = SHA1.Create())
|
||||
return new string(csp.ComputeHash(data).SelectMany(a => a.ToString("x2")).ToArray());
|
||||
using var csp = SHA1.Create();
|
||||
return ToHex(csp.ComputeHash(data), true);
|
||||
}
|
||||
|
||||
public static string SHA1Hash(byte[] data)
|
||||
{
|
||||
using (var csp = SHA1.Create())
|
||||
return new string(csp.ComputeHash(data).SelectMany(a => a.ToString("x2")).ToArray());
|
||||
using var csp = SHA1.Create();
|
||||
return ToHex(csp.ComputeHash(data), true);
|
||||
}
|
||||
|
||||
public static string SHA1Hash(string data)
|
||||
{
|
||||
return SHA1Hash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
public static string ToHex(ReadOnlySpan<byte> source, bool lowerCase = false)
|
||||
{
|
||||
if (source.Length == 0)
|
||||
return string.Empty;
|
||||
|
||||
// excessively avoid stack overflow if source is too large (considering that we're allocating a new string)
|
||||
var buffer = source.Length <= 256 ? stackalloc char[source.Length * 2] : new char[source.Length * 2];
|
||||
return ToHexInternal(source, buffer, lowerCase);
|
||||
}
|
||||
|
||||
static string ToHexInternal(ReadOnlySpan<byte> source, Span<char> buffer, bool lowerCase)
|
||||
{
|
||||
var sourceIndex = 0;
|
||||
var alphabet = lowerCase ? HexLowerAlphabet : HexUpperAlphabet;
|
||||
|
||||
for (var i = 0; i < buffer.Length; i += 2)
|
||||
{
|
||||
var b = source[sourceIndex++];
|
||||
buffer[i] = alphabet[b >> 4];
|
||||
buffer[i + 1] = alphabet[b & 0xF];
|
||||
}
|
||||
|
||||
return new string(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ namespace OpenRA
|
||||
// Several types of support directory types are available, depending on
|
||||
// how the player has installed and launched the game.
|
||||
// Read registration metadata from all of them
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
foreach (var source in GetSupportDirs(ModRegistration.User | ModRegistration.System))
|
||||
{
|
||||
var metadataPath = Path.Combine(source, "ModMetadata");
|
||||
@@ -76,7 +77,7 @@ namespace OpenRA
|
||||
{
|
||||
try
|
||||
{
|
||||
var yaml = MiniYaml.FromStream(File.OpenRead(path), path).First().Value;
|
||||
var yaml = MiniYaml.FromFile(path, stringPool: stringPool).First().Value;
|
||||
LoadMod(yaml, path);
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -94,17 +95,17 @@ namespace OpenRA
|
||||
|
||||
if (sheetBuilder != null)
|
||||
{
|
||||
var iconNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon");
|
||||
var iconNode = yaml.NodeWithKeyOrDefault("Icon");
|
||||
if (iconNode != null && !string.IsNullOrEmpty(iconNode.Value.Value))
|
||||
using (var stream = new MemoryStream(Convert.FromBase64String(iconNode.Value.Value)))
|
||||
mod.Icon = sheetBuilder.Add(new Png(stream));
|
||||
|
||||
var icon2xNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon2x");
|
||||
var icon2xNode = yaml.NodeWithKeyOrDefault("Icon2x");
|
||||
if (icon2xNode != null && !string.IsNullOrEmpty(icon2xNode.Value.Value))
|
||||
using (var stream = new MemoryStream(Convert.FromBase64String(icon2xNode.Value.Value)))
|
||||
mod.Icon2x = sheetBuilder.Add(new Png(stream), 1f / 2);
|
||||
|
||||
var icon3xNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon3x");
|
||||
var icon3xNode = yaml.NodeWithKeyOrDefault("Icon3x");
|
||||
if (icon3xNode != null && !string.IsNullOrEmpty(icon3xNode.Value.Value))
|
||||
using (var stream = new MemoryStream(Convert.FromBase64String(icon3xNode.Value.Value)))
|
||||
mod.Icon3x = sheetBuilder.Add(new Png(stream), 1f / 3);
|
||||
@@ -122,7 +123,7 @@ namespace OpenRA
|
||||
return;
|
||||
|
||||
var key = ExternalMod.MakeKey(mod);
|
||||
var yaml = new MiniYamlNode("Registration", new MiniYaml("", new List<MiniYamlNode>()
|
||||
var yaml = new MiniYamlNode("Registration", new MiniYaml("", new[]
|
||||
{
|
||||
new MiniYamlNode("Id", mod.Id),
|
||||
new MiniYamlNode("Version", mod.Metadata.Version),
|
||||
@@ -131,17 +132,21 @@ namespace OpenRA
|
||||
new MiniYamlNode("LaunchArgs", new[] { "Game.Mod=" + mod.Id }.Concat(launchArgs).JoinWith(", "))
|
||||
}));
|
||||
|
||||
var iconNodes = new List<MiniYamlNode>();
|
||||
|
||||
using (var stream = mod.Package.GetStream("icon.png"))
|
||||
if (stream != null)
|
||||
yaml.Value.Nodes.Add(new MiniYamlNode("Icon", Convert.ToBase64String(stream.ReadAllBytes())));
|
||||
iconNodes.Add(new MiniYamlNode("Icon", Convert.ToBase64String(stream.ReadAllBytes())));
|
||||
|
||||
using (var stream = mod.Package.GetStream("icon-2x.png"))
|
||||
if (stream != null)
|
||||
yaml.Value.Nodes.Add(new MiniYamlNode("Icon2x", Convert.ToBase64String(stream.ReadAllBytes())));
|
||||
iconNodes.Add(new MiniYamlNode("Icon2x", Convert.ToBase64String(stream.ReadAllBytes())));
|
||||
|
||||
using (var stream = mod.Package.GetStream("icon-3x.png"))
|
||||
if (stream != null)
|
||||
yaml.Value.Nodes.Add(new MiniYamlNode("Icon3x", Convert.ToBase64String(stream.ReadAllBytes())));
|
||||
iconNodes.Add(new MiniYamlNode("Icon3x", Convert.ToBase64String(stream.ReadAllBytes())));
|
||||
|
||||
yaml = yaml.WithValue(yaml.Value.WithNodesAppended(iconNodes));
|
||||
|
||||
var sources = new HashSet<string>();
|
||||
if (registration.HasFlag(ModRegistration.System))
|
||||
@@ -201,7 +206,7 @@ namespace OpenRA
|
||||
string modKey = null;
|
||||
try
|
||||
{
|
||||
var yaml = MiniYaml.FromStream(File.OpenRead(path), path).First().Value;
|
||||
var yaml = MiniYaml.FromFile(path).First().Value;
|
||||
var m = FieldLoader.Load<ExternalMod>(yaml);
|
||||
modKey = ExternalMod.MakeKey(m);
|
||||
|
||||
|
||||
@@ -22,15 +22,14 @@ namespace OpenRA
|
||||
{
|
||||
public static class Exts
|
||||
{
|
||||
public static bool IsUppercase(this string str)
|
||||
public static string FormatInvariant(this string format, params object[] args)
|
||||
{
|
||||
return string.Compare(str.ToUpperInvariant(), str, false) == 0;
|
||||
return string.Format(CultureInfo.InvariantCulture, format, args);
|
||||
}
|
||||
|
||||
public static T WithDefault<T>(T def, Func<T> f)
|
||||
public static string FormatCurrent(this string format, params object[] args)
|
||||
{
|
||||
try { return f(); }
|
||||
catch { return def; }
|
||||
return string.Format(CultureInfo.CurrentCulture, format, args);
|
||||
}
|
||||
|
||||
public static Lazy<T> Lazy<T>(Func<T> p) { return new Lazy<T>(p); }
|
||||
@@ -131,11 +130,35 @@ namespace OpenRA
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static T GetOrAdd<T>(this HashSet<T> set, T value)
|
||||
{
|
||||
if (!set.TryGetValue(value, out var ret))
|
||||
set.Add(ret = value);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static T GetOrAdd<T>(this HashSet<T> set, T value, Func<T, T> createFn)
|
||||
{
|
||||
if (!set.TryGetValue(value, out var ret))
|
||||
set.Add(ret = createFn(value));
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static int IndexOf<T>(this T[] array, T value)
|
||||
{
|
||||
return Array.IndexOf(array, value);
|
||||
}
|
||||
|
||||
public static T FirstOrDefault<T>(this T[] array, Predicate<T> match)
|
||||
{
|
||||
return Array.Find(array, match);
|
||||
}
|
||||
|
||||
public static T FirstOrDefault<T>(this List<T> list, Predicate<T> match)
|
||||
{
|
||||
return list.Find(match);
|
||||
}
|
||||
|
||||
public static T Random<T>(this IEnumerable<T> ts, MersenneTwister r)
|
||||
{
|
||||
return Random(ts, r, true);
|
||||
@@ -148,7 +171,7 @@ namespace OpenRA
|
||||
|
||||
static T Random<T>(IEnumerable<T> ts, MersenneTwister r, bool throws)
|
||||
{
|
||||
var xs = ts as ICollection<T>;
|
||||
var xs = ts as IReadOnlyCollection<T>;
|
||||
xs ??= ts.ToList();
|
||||
if (xs.Count == 0)
|
||||
{
|
||||
@@ -303,9 +326,9 @@ namespace OpenRA
|
||||
|
||||
// Adjust for other rounding modes
|
||||
if (round == ISqrtRoundMode.Nearest && remainder > root)
|
||||
root += 1;
|
||||
root++;
|
||||
else if (round == ISqrtRoundMode.Ceiling && root * root < number)
|
||||
root += 1;
|
||||
root++;
|
||||
|
||||
return root;
|
||||
}
|
||||
@@ -344,9 +367,9 @@ namespace OpenRA
|
||||
|
||||
// Adjust for other rounding modes
|
||||
if (round == ISqrtRoundMode.Nearest && remainder > root)
|
||||
root += 1;
|
||||
root++;
|
||||
else if (round == ISqrtRoundMode.Ceiling && root * root < number)
|
||||
root += 1;
|
||||
root++;
|
||||
|
||||
return root;
|
||||
}
|
||||
@@ -356,6 +379,11 @@ namespace OpenRA
|
||||
return number * 46341 / 32768;
|
||||
}
|
||||
|
||||
public static int MultiplyBySqrtTwoOverTwo(int number)
|
||||
{
|
||||
return (int)(number * 23170L / 32768L);
|
||||
}
|
||||
|
||||
public static int IntegerDivisionRoundingAwayFromZero(int dividend, int divisor)
|
||||
{
|
||||
var quotient = Math.DivRem(dividend, divisor, out var remainder);
|
||||
@@ -493,17 +521,22 @@ namespace OpenRA
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int ParseIntegerInvariant(string s)
|
||||
{
|
||||
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static byte ParseByte(string s)
|
||||
public static byte ParseByteInvariant(string s)
|
||||
{
|
||||
return byte.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static bool TryParseIntegerInvariant(string s, out int i)
|
||||
public static short ParseInt16Invariant(string s)
|
||||
{
|
||||
return short.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static int ParseInt32Invariant(string s)
|
||||
{
|
||||
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static bool TryParseInt32Invariant(string s, out int i)
|
||||
{
|
||||
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
||||
}
|
||||
@@ -513,6 +546,26 @@ namespace OpenRA
|
||||
return long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
||||
}
|
||||
|
||||
public static string ToStringInvariant(this byte i)
|
||||
{
|
||||
return i.ToString(NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static string ToStringInvariant(this byte i, string format)
|
||||
{
|
||||
return i.ToString(format, NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static string ToStringInvariant(this int i)
|
||||
{
|
||||
return i.ToString(NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static string ToStringInvariant(this int i, string format)
|
||||
{
|
||||
return i.ToString(format, NumberFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
public static bool IsTraitEnabled<T>(this T trait)
|
||||
{
|
||||
return trait is not IDisabledTrait disabledTrait || !disabledTrait.IsTraitDisabled;
|
||||
@@ -562,11 +615,6 @@ namespace OpenRA
|
||||
{
|
||||
return new LineSplitEnumerator(str.AsSpan(), separator);
|
||||
}
|
||||
|
||||
public static bool TryParseInt32Invariant(string s, out int i)
|
||||
{
|
||||
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
|
||||
}
|
||||
}
|
||||
|
||||
public ref struct LineSplitEnumerator
|
||||
@@ -581,7 +629,7 @@ namespace OpenRA
|
||||
Current = default;
|
||||
}
|
||||
|
||||
public LineSplitEnumerator GetEnumerator() => this;
|
||||
public readonly LineSplitEnumerator GetEnumerator() => this;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
|
||||
@@ -87,6 +87,7 @@ namespace OpenRA
|
||||
{ typeof(WAngle), ParseWAngle },
|
||||
{ typeof(WRot), ParseWRot },
|
||||
{ typeof(CPos), ParseCPos },
|
||||
{ typeof(CPos[]), ParseCPosArray },
|
||||
{ typeof(CVec), ParseCVec },
|
||||
{ typeof(CVec[]), ParseCVecArray },
|
||||
{ typeof(BooleanExpression), ParseBooleanExpression },
|
||||
@@ -118,7 +119,7 @@ namespace OpenRA
|
||||
|
||||
static object ParseInt(string fieldName, Type fieldType, string value, MemberInfo field)
|
||||
{
|
||||
if (Exts.TryParseIntegerInvariant(value, out var res))
|
||||
if (Exts.TryParseInt32Invariant(value, out var res))
|
||||
{
|
||||
if (res >= 0 && res < BoxedInts.Length)
|
||||
return BoxedInts[res];
|
||||
@@ -195,11 +196,11 @@ namespace OpenRA
|
||||
if (value != null)
|
||||
{
|
||||
var parts = value.Split(SplitComma);
|
||||
if (parts.Length == 3)
|
||||
{
|
||||
if (WDist.TryParse(parts[0], out var rx) && WDist.TryParse(parts[1], out var ry) && WDist.TryParse(parts[2], out var rz))
|
||||
return new WVec(rx, ry, rz);
|
||||
}
|
||||
if (parts.Length == 3
|
||||
&& WDist.TryParse(parts[0], out var rx)
|
||||
&& WDist.TryParse(parts[1], out var ry)
|
||||
&& WDist.TryParse(parts[2], out var rz))
|
||||
return new WVec(rx, ry, rz);
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -219,8 +220,8 @@ namespace OpenRA
|
||||
for (var i = 0; i < vecs.Length; ++i)
|
||||
{
|
||||
if (WDist.TryParse(parts[3 * i], out var rx)
|
||||
&& WDist.TryParse(parts[3 * i + 1], out var ry)
|
||||
&& WDist.TryParse(parts[3 * i + 2], out var rz))
|
||||
&& WDist.TryParse(parts[3 * i + 1], out var ry)
|
||||
&& WDist.TryParse(parts[3 * i + 2], out var rz))
|
||||
vecs[i] = new WVec(rx, ry, rz);
|
||||
}
|
||||
|
||||
@@ -235,13 +236,11 @@ namespace OpenRA
|
||||
if (value != null)
|
||||
{
|
||||
var parts = value.Split(SplitComma);
|
||||
if (parts.Length == 3)
|
||||
{
|
||||
if (WDist.TryParse(parts[0], out var rx)
|
||||
&& WDist.TryParse(parts[1], out var ry)
|
||||
&& WDist.TryParse(parts[2], out var rz))
|
||||
return new WPos(rx, ry, rz);
|
||||
}
|
||||
if (parts.Length == 3
|
||||
&& WDist.TryParse(parts[0], out var rx)
|
||||
&& WDist.TryParse(parts[1], out var ry)
|
||||
&& WDist.TryParse(parts[2], out var rz))
|
||||
return new WPos(rx, ry, rz);
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -249,7 +248,7 @@ namespace OpenRA
|
||||
|
||||
static object ParseWAngle(string fieldName, Type fieldType, string value, MemberInfo field)
|
||||
{
|
||||
if (Exts.TryParseIntegerInvariant(value, out var res))
|
||||
if (Exts.TryParseInt32Invariant(value, out var res))
|
||||
return new WAngle(res);
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
}
|
||||
@@ -259,13 +258,11 @@ namespace OpenRA
|
||||
if (value != null)
|
||||
{
|
||||
var parts = value.Split(SplitComma);
|
||||
if (parts.Length == 3)
|
||||
{
|
||||
if (Exts.TryParseIntegerInvariant(parts[0], out var rr)
|
||||
&& Exts.TryParseIntegerInvariant(parts[1], out var rp)
|
||||
&& Exts.TryParseIntegerInvariant(parts[2], out var ry))
|
||||
return new WRot(new WAngle(rr), new WAngle(rp), new WAngle(ry));
|
||||
}
|
||||
if (parts.Length == 3
|
||||
&& Exts.TryParseInt32Invariant(parts[0], out var rr)
|
||||
&& Exts.TryParseInt32Invariant(parts[1], out var rp)
|
||||
&& Exts.TryParseInt32Invariant(parts[2], out var ry))
|
||||
return new WRot(new WAngle(rr), new WAngle(rp), new WAngle(ry));
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -278,10 +275,33 @@ namespace OpenRA
|
||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 3)
|
||||
return new CPos(
|
||||
Exts.ParseIntegerInvariant(parts[0]),
|
||||
Exts.ParseIntegerInvariant(parts[1]),
|
||||
Exts.ParseByte(parts[2]));
|
||||
return new CPos(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
|
||||
Exts.ParseInt32Invariant(parts[0]),
|
||||
Exts.ParseInt32Invariant(parts[1]),
|
||||
Exts.ParseByteInvariant(parts[2]));
|
||||
return new CPos(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
}
|
||||
|
||||
static object ParseCPosArray(string fieldName, Type fieldType, string value, MemberInfo field)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
var parts = value.Split(SplitComma);
|
||||
|
||||
if (parts.Length % 2 != 0)
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
|
||||
var vecs = new CPos[parts.Length / 2];
|
||||
for (var i = 0; i < vecs.Length; i++)
|
||||
{
|
||||
if (int.TryParse(parts[2 * i], out var rx)
|
||||
&& int.TryParse(parts[2 * i + 1], out var ry))
|
||||
vecs[i] = new CPos(rx, ry);
|
||||
}
|
||||
|
||||
return vecs;
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -292,7 +312,7 @@ namespace OpenRA
|
||||
if (value != null)
|
||||
{
|
||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
||||
return new CVec(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
|
||||
return new CVec(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -385,7 +405,7 @@ namespace OpenRA
|
||||
|
||||
var ints = new int2[parts.Length / 2];
|
||||
for (var i = 0; i < ints.Length; i++)
|
||||
ints[i] = new int2(Exts.ParseIntegerInvariant(parts[2 * i]), Exts.ParseIntegerInvariant(parts[2 * i + 1]));
|
||||
ints[i] = new int2(Exts.ParseInt32Invariant(parts[2 * i]), Exts.ParseInt32Invariant(parts[2 * i + 1]));
|
||||
|
||||
return ints;
|
||||
}
|
||||
@@ -398,7 +418,7 @@ namespace OpenRA
|
||||
if (value != null)
|
||||
{
|
||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
||||
return new Size(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
|
||||
return new Size(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -412,7 +432,7 @@ namespace OpenRA
|
||||
if (parts.Length != 2)
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
|
||||
return new int2(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
|
||||
return new int2(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -460,10 +480,10 @@ namespace OpenRA
|
||||
{
|
||||
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
||||
return new Rectangle(
|
||||
Exts.ParseIntegerInvariant(parts[0]),
|
||||
Exts.ParseIntegerInvariant(parts[1]),
|
||||
Exts.ParseIntegerInvariant(parts[2]),
|
||||
Exts.ParseIntegerInvariant(parts[3]));
|
||||
Exts.ParseInt32Invariant(parts[0]),
|
||||
Exts.ParseInt32Invariant(parts[1]),
|
||||
Exts.ParseInt32Invariant(parts[2]),
|
||||
Exts.ParseInt32Invariant(parts[3]));
|
||||
}
|
||||
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
@@ -500,7 +520,7 @@ namespace OpenRA
|
||||
if (yaml == null)
|
||||
return Activator.CreateInstance(fieldType);
|
||||
|
||||
var dict = Activator.CreateInstance(fieldType, yaml.Nodes.Count);
|
||||
var dict = Activator.CreateInstance(fieldType, yaml.Nodes.Length);
|
||||
var arguments = fieldType.GetGenericArguments();
|
||||
var addMethod = fieldType.GetMethod(nameof(Dictionary<object, object>.Add), arguments);
|
||||
var addArgs = new object[2];
|
||||
@@ -531,7 +551,7 @@ namespace OpenRA
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return null;
|
||||
|
||||
var innerType = fieldType.GetGenericArguments().First();
|
||||
var innerType = fieldType.GetGenericArguments()[0];
|
||||
var innerValue = GetValue("Nullable<T>", innerType, value, field);
|
||||
return fieldType.GetConstructor(new[] { innerType }).Invoke(new[] { innerValue });
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA
|
||||
@@ -58,7 +59,7 @@ namespace OpenRA
|
||||
|
||||
return new MiniYaml(
|
||||
null,
|
||||
fields.Select(info => new MiniYamlNode(info.YamlName, FormatValue(o, info.Field))).ToList());
|
||||
fields.Select(info => new MiniYamlNode(info.YamlName, FormatValue(o, info.Field))));
|
||||
}
|
||||
|
||||
public static MiniYamlNode SaveField(object o, string field)
|
||||
@@ -84,7 +85,7 @@ namespace OpenRA
|
||||
// This is only for documentation generation
|
||||
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
var result = "";
|
||||
var result = new StringBuilder();
|
||||
var dict = (System.Collections.IDictionary)v;
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
@@ -94,10 +95,10 @@ namespace OpenRA
|
||||
var formattedKey = FormatValue(key);
|
||||
var formattedValue = FormatValue(value);
|
||||
|
||||
result += $"{formattedKey}: {formattedValue}{Environment.NewLine}";
|
||||
result.Append($"{formattedKey}: {formattedValue}{Environment.NewLine}");
|
||||
}
|
||||
|
||||
return result;
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
if (v is DateTime d)
|
||||
|
||||
@@ -16,6 +16,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using ICSharpCode.SharpZipLib.Checksum;
|
||||
using ICSharpCode.SharpZipLib.Zip.Compression;
|
||||
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Primitives;
|
||||
@@ -45,12 +46,13 @@ namespace OpenRA.FileFormats
|
||||
var data = new List<byte>();
|
||||
Type = SpriteFrameType.Rgba32;
|
||||
|
||||
byte bitDepth = 8;
|
||||
while (true)
|
||||
{
|
||||
var length = IPAddress.NetworkToHostOrder(s.ReadInt32());
|
||||
var type = Encoding.UTF8.GetString(s.ReadBytes(4));
|
||||
var type = s.ReadASCII(4);
|
||||
var content = s.ReadBytes(length);
|
||||
/*var crc = */s.ReadInt32();
|
||||
s.ReadInt32(); // crc
|
||||
|
||||
if (!headerParsed && type != "IHDR")
|
||||
throw new InvalidDataException("Invalid PNG file - header does not appear first.");
|
||||
@@ -66,7 +68,7 @@ namespace OpenRA.FileFormats
|
||||
Width = IPAddress.NetworkToHostOrder(ms.ReadInt32());
|
||||
Height = IPAddress.NetworkToHostOrder(ms.ReadInt32());
|
||||
|
||||
var bitDepth = ms.ReadUInt8();
|
||||
bitDepth = ms.ReadUInt8();
|
||||
var colorType = (PngColorType)ms.ReadUInt8();
|
||||
if (IsPaletted(bitDepth, colorType))
|
||||
Type = SpriteFrameType.Indexed8;
|
||||
@@ -76,7 +78,7 @@ namespace OpenRA.FileFormats
|
||||
Data = new byte[Width * Height * PixelStride];
|
||||
|
||||
var compression = ms.ReadUInt8();
|
||||
/*var filter = */ms.ReadUInt8();
|
||||
ms.ReadUInt8(); // filter
|
||||
var interlace = ms.ReadUInt8();
|
||||
|
||||
if (compression != 0)
|
||||
@@ -92,8 +94,8 @@ namespace OpenRA.FileFormats
|
||||
|
||||
case "PLTE":
|
||||
{
|
||||
Palette = new Color[256];
|
||||
for (var i = 0; i < length / 3; i++)
|
||||
Palette = new Color[length / 3];
|
||||
for (var i = 0; i < Palette.Length; i++)
|
||||
{
|
||||
var r = ms.ReadUInt8(); var g = ms.ReadUInt8(); var b = ms.ReadUInt8();
|
||||
Palette[i] = Color.FromArgb(r, g, b);
|
||||
@@ -136,14 +138,34 @@ namespace OpenRA.FileFormats
|
||||
{
|
||||
var pxStride = PixelStride;
|
||||
var rowStride = Width * pxStride;
|
||||
var pixelsPerByte = 8 / bitDepth;
|
||||
var sourceRowStride = Exts.IntegerDivisionRoundingAwayFromZero(rowStride, pixelsPerByte);
|
||||
|
||||
Span<byte> prevLine = new byte[rowStride];
|
||||
for (var y = 0; y < Height; y++)
|
||||
{
|
||||
var filter = (PngFilter)ds.ReadUInt8();
|
||||
ds.ReadBytes(Data, y * rowStride, rowStride);
|
||||
ds.ReadBytes(Data, y * rowStride, sourceRowStride);
|
||||
var line = Data.AsSpan(y * rowStride, rowStride);
|
||||
|
||||
// If the source has a bit depth of 1, 2 or 4 it packs multiple pixels per byte.
|
||||
// Unpack to bit depth of 8, yielding 1 pixel per byte.
|
||||
// This makes life easier for consumers of palleted data.
|
||||
if (bitDepth < 8)
|
||||
{
|
||||
var mask = 0xFF >> (8 - bitDepth);
|
||||
for (var i = sourceRowStride - 1; i >= 0; i--)
|
||||
{
|
||||
var packed = line[i];
|
||||
for (var j = 0; j < pixelsPerByte; j++)
|
||||
{
|
||||
var dest = i * pixelsPerByte + j;
|
||||
if (dest < line.Length) // Guard against last byte being only partially packed
|
||||
line[dest] = (byte)(packed >> (8 - (j + 1) * bitDepth) & mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (filter)
|
||||
{
|
||||
case PngFilter.None:
|
||||
@@ -269,7 +291,7 @@ namespace OpenRA.FileFormats
|
||||
|
||||
static bool IsPaletted(byte bitDepth, PngColorType colorType)
|
||||
{
|
||||
if (bitDepth == 8 && colorType == (PngColorType.Indexed | PngColorType.Color))
|
||||
if (bitDepth <= 8 && colorType == (PngColorType.Indexed | PngColorType.Color))
|
||||
return true;
|
||||
|
||||
if (bitDepth == 8 && colorType == (PngColorType.Color | PngColorType.Alpha))
|
||||
@@ -287,10 +309,10 @@ namespace OpenRA.FileFormats
|
||||
|
||||
var typeBytes = Encoding.ASCII.GetBytes(type);
|
||||
output.Write(IPAddress.HostToNetworkOrder((int)input.Length));
|
||||
output.WriteArray(typeBytes);
|
||||
output.Write(typeBytes);
|
||||
|
||||
var data = input.ReadAllBytes();
|
||||
output.WriteArray(data);
|
||||
output.Write(data);
|
||||
|
||||
var crc32 = new Crc32();
|
||||
crc32.Update(typeBytes);
|
||||
@@ -302,7 +324,7 @@ namespace OpenRA.FileFormats
|
||||
{
|
||||
using (var output = new MemoryStream())
|
||||
{
|
||||
output.WriteArray(Signature);
|
||||
output.Write(Signature);
|
||||
using (var header = new MemoryStream())
|
||||
{
|
||||
header.Write(IPAddress.HostToNetworkOrder(Width));
|
||||
@@ -350,13 +372,14 @@ namespace OpenRA.FileFormats
|
||||
|
||||
using (var data = new MemoryStream())
|
||||
{
|
||||
using (var compressed = new DeflaterOutputStream(data))
|
||||
using (var compressed = new DeflaterOutputStream(data, new Deflater(Deflater.BEST_COMPRESSION)))
|
||||
{
|
||||
var rowStride = Width * PixelStride;
|
||||
for (var y = 0; y < Height; y++)
|
||||
{
|
||||
// Write uncompressed scanlines for simplicity
|
||||
compressed.WriteByte(0);
|
||||
// Assuming no filtering for simplicity
|
||||
const byte FilterType = 0;
|
||||
compressed.WriteByte(FilterType);
|
||||
compressed.Write(Data, y * rowStride, rowStride);
|
||||
}
|
||||
|
||||
@@ -371,7 +394,7 @@ namespace OpenRA.FileFormats
|
||||
{
|
||||
using (var text = new MemoryStream())
|
||||
{
|
||||
text.WriteArray(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
|
||||
text.Write(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
|
||||
WritePngChunk(output, "tEXt", text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ namespace OpenRA.FileFormats
|
||||
throw new NotSupportedException($"Metadata version {version} is not supported");
|
||||
|
||||
// Read game info (max 100K limit as a safeguard against corrupted files)
|
||||
var data = fs.ReadString(Encoding.UTF8, 1024 * 100);
|
||||
GameInfo = GameInformation.Deserialize(data);
|
||||
var data = fs.ReadLengthPrefixedString(Encoding.UTF8, 1024 * 100);
|
||||
GameInfo = GameInformation.Deserialize(data, path);
|
||||
}
|
||||
|
||||
public void Write(BinaryWriter writer)
|
||||
@@ -62,7 +62,7 @@ namespace OpenRA.FileFormats
|
||||
{
|
||||
// Write lobby info data
|
||||
writer.Flush();
|
||||
dataLength += writer.BaseStream.WriteString(Encoding.UTF8, GameInfo.Serialize());
|
||||
dataLength += writer.BaseStream.WriteLengthPrefixedString(Encoding.UTF8, GameInfo.Serialize());
|
||||
}
|
||||
|
||||
// Write total length & end marker
|
||||
|
||||
@@ -83,14 +83,14 @@ namespace OpenRA.FileSystem
|
||||
|
||||
public void Mount(string name, string explicitName = null)
|
||||
{
|
||||
var optional = name.StartsWith("~", StringComparison.Ordinal);
|
||||
var optional = name.StartsWith('~');
|
||||
if (optional)
|
||||
name = name[1..];
|
||||
|
||||
try
|
||||
{
|
||||
IReadOnlyPackage package;
|
||||
if (name.StartsWith("$", StringComparison.Ordinal))
|
||||
if (name.StartsWith('$'))
|
||||
{
|
||||
name = name[1..];
|
||||
|
||||
@@ -109,10 +109,8 @@ namespace OpenRA.FileSystem
|
||||
|
||||
Mount(package, explicitName);
|
||||
}
|
||||
catch
|
||||
catch when (optional)
|
||||
{
|
||||
if (!optional)
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,9 +159,7 @@ namespace OpenRA.FileSystem
|
||||
explicitMounts.Remove(key);
|
||||
|
||||
// Mod packages aren't owned by us, so we shouldn't dispose them
|
||||
if (modPackages.Contains(package))
|
||||
modPackages.Remove(package);
|
||||
else
|
||||
if (!modPackages.Remove(package))
|
||||
package.Dispose();
|
||||
}
|
||||
else
|
||||
@@ -190,6 +186,12 @@ namespace OpenRA.FileSystem
|
||||
UnmountAll();
|
||||
foreach (var kv in manifest.Packages)
|
||||
Mount(kv.Key, kv.Value);
|
||||
|
||||
mountedPackages.TrimExcess();
|
||||
explicitMounts.TrimExcess();
|
||||
modPackages.TrimExcess();
|
||||
foreach (var packages in fileIndex.Values)
|
||||
packages.TrimExcess();
|
||||
}
|
||||
|
||||
Stream GetFromCache(string filename)
|
||||
@@ -226,14 +228,11 @@ namespace OpenRA.FileSystem
|
||||
public bool TryOpen(string filename, out Stream s)
|
||||
{
|
||||
var explicitSplit = filename.IndexOf('|');
|
||||
if (explicitSplit > 0)
|
||||
if (explicitSplit > 0 && explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
|
||||
{
|
||||
if (explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
|
||||
{
|
||||
s = explicitPackage.GetStream(filename[(explicitSplit + 1)..]);
|
||||
if (s != null)
|
||||
return true;
|
||||
}
|
||||
s = explicitPackage.GetStream(filename[(explicitSplit + 1)..]);
|
||||
if (s != null)
|
||||
return true;
|
||||
}
|
||||
|
||||
s = GetFromCache(filename);
|
||||
@@ -262,10 +261,10 @@ namespace OpenRA.FileSystem
|
||||
public bool Exists(string filename)
|
||||
{
|
||||
var explicitSplit = filename.IndexOf('|');
|
||||
if (explicitSplit > 0)
|
||||
if (explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
|
||||
if (explicitPackage.Contains(filename[(explicitSplit + 1)..]))
|
||||
return true;
|
||||
if (explicitSplit > 0 &&
|
||||
explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage) &&
|
||||
explicitPackage.Contains(filename[(explicitSplit + 1)..]))
|
||||
return true;
|
||||
|
||||
return fileIndex.ContainsKey(filename);
|
||||
}
|
||||
@@ -295,7 +294,7 @@ namespace OpenRA.FileSystem
|
||||
public static string ResolveAssemblyPath(string path, Manifest manifest, InstalledMods installedMods)
|
||||
{
|
||||
var explicitSplit = path.IndexOf('|');
|
||||
if (explicitSplit > 0 && !path.StartsWith("^"))
|
||||
if (explicitSplit > 0 && !path.StartsWith('^'))
|
||||
{
|
||||
var parent = path[..explicitSplit];
|
||||
var filename = path[(explicitSplit + 1)..];
|
||||
@@ -304,7 +303,7 @@ namespace OpenRA.FileSystem
|
||||
if (parentPath == null)
|
||||
return null;
|
||||
|
||||
if (parentPath.StartsWith("$", StringComparison.Ordinal))
|
||||
if (parentPath.StartsWith('$'))
|
||||
{
|
||||
if (!installedMods.TryGetValue(parentPath[1..], out var mod))
|
||||
return null;
|
||||
|
||||
@@ -84,7 +84,7 @@ namespace OpenRA.FileSystem
|
||||
// in FileSystem.OpenPackage. Their internal name therefore contains the
|
||||
// full parent path too. We need to be careful to not add a second path
|
||||
// prefix to these hacked packages.
|
||||
var filePath = filename.StartsWith(Name) ? filename : Path.Combine(Name, filename);
|
||||
var filePath = filename.StartsWith(Name, StringComparison.Ordinal) ? filename : Path.Combine(Name, filename);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
|
||||
using (var s = File.Create(filePath))
|
||||
@@ -98,7 +98,7 @@ namespace OpenRA.FileSystem
|
||||
// in FileSystem.OpenPackage. Their internal name therefore contains the
|
||||
// full parent path too. We need to be careful to not add a second path
|
||||
// prefix to these hacked packages.
|
||||
var filePath = filename.StartsWith(Name) ? filename : Path.Combine(Name, filename);
|
||||
var filePath = filename.StartsWith(Name, StringComparison.Ordinal) ? filename : Path.Combine(Name, filename);
|
||||
if (Directory.Exists(filePath))
|
||||
Directory.Delete(filePath, true);
|
||||
else if (File.Exists(filePath))
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace OpenRA.FileSystem
|
||||
{
|
||||
const uint ZipSignature = 0x04034b50;
|
||||
|
||||
class ReadOnlyZipFile : IReadOnlyPackage
|
||||
public class ReadOnlyZipFile : IReadOnlyPackage
|
||||
{
|
||||
public string Name { get; protected set; }
|
||||
protected ZipFile pkg;
|
||||
@@ -68,6 +68,7 @@ namespace OpenRA.FileSystem
|
||||
public void Dispose()
|
||||
{
|
||||
pkg?.Close();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
|
||||
@@ -93,7 +94,7 @@ namespace OpenRA.FileSystem
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage
|
||||
public sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage
|
||||
{
|
||||
readonly MemoryStream pkgStream = new();
|
||||
|
||||
@@ -117,10 +118,7 @@ namespace OpenRA.FileSystem
|
||||
|
||||
void Commit()
|
||||
{
|
||||
var pos = pkgStream.Position;
|
||||
pkgStream.Position = 0;
|
||||
File.WriteAllBytes(Name, pkgStream.ReadBytes((int)pkgStream.Length));
|
||||
pkgStream.Position = pos;
|
||||
File.WriteAllBytes(Name, pkgStream.ToArray());
|
||||
}
|
||||
|
||||
public void Update(string filename, byte[] contents)
|
||||
@@ -147,7 +145,7 @@ namespace OpenRA.FileSystem
|
||||
|
||||
public ZipFolder(ReadOnlyZipFile parent, string path)
|
||||
{
|
||||
if (path.EndsWith("/", StringComparison.Ordinal))
|
||||
if (path.EndsWith('/'))
|
||||
path = path[..^1];
|
||||
|
||||
Name = path;
|
||||
|
||||
@@ -180,6 +180,7 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public static event Action BeforeGameStart = () => { };
|
||||
public static event Action AfterGameStart = () => { };
|
||||
internal static void StartGame(string mapUID, WorldType type)
|
||||
{
|
||||
// Dispose of the old world before creating a new one.
|
||||
@@ -223,6 +224,12 @@ namespace OpenRA
|
||||
// Much better to clean up now then to drop frames during gameplay for GC pauses.
|
||||
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
|
||||
GC.Collect();
|
||||
|
||||
// PostLoadComplete is designed for anything that should trigger at the very end of loading.
|
||||
// e.g. audio notifications that the game is starting.
|
||||
OrderManager.World.PostLoadComplete(worldRenderer);
|
||||
|
||||
AfterGameStart();
|
||||
}
|
||||
|
||||
public static void RestartGame()
|
||||
@@ -416,7 +423,7 @@ namespace OpenRA
|
||||
|
||||
// Sanitize input from platform-specific launchers
|
||||
// Process.Start requires paths to not be quoted, even if they contain spaces
|
||||
if (launchPath != null && launchPath.First() == '"' && launchPath.Last() == '"')
|
||||
if (launchPath != null && launchPath[0] == '"' && launchPath.Last() == '"')
|
||||
launchPath = launchPath[1..^1];
|
||||
|
||||
// Metadata registration requires an explicit launch path
|
||||
@@ -524,10 +531,11 @@ namespace OpenRA
|
||||
.Where(m => m.Status == MapStatus.Available && m.Visibility.HasFlag(MapVisibility.Shellmap))
|
||||
.Select(m => m.Uid);
|
||||
|
||||
if (!shellmaps.Any())
|
||||
var shellmap = shellmaps.RandomOrDefault(CosmeticRandom);
|
||||
if (shellmap == null)
|
||||
throw new InvalidDataException("No valid shellmaps available");
|
||||
|
||||
return shellmaps.Random(CosmeticRandom);
|
||||
return shellmap;
|
||||
}
|
||||
|
||||
public static void SwitchToExternalMod(ExternalMod mod, string[] launchArguments = null, Action onFailed = null)
|
||||
@@ -601,7 +609,10 @@ namespace OpenRA
|
||||
|
||||
if (orderManager.LastTickTime.ShouldAdvance(tick))
|
||||
{
|
||||
using (new PerfSample("tick_time"))
|
||||
if (orderManager.GameStarted && orderManager.LocalFrameNumber == 0)
|
||||
PerfHistory.Reset(); // Remove history that occurred whilst the new game was loading.
|
||||
|
||||
using (var sample = new PerfSample("tick_time"))
|
||||
{
|
||||
orderManager.LastTickTime.AdvanceTickTime(tick);
|
||||
|
||||
@@ -610,7 +621,11 @@ namespace OpenRA
|
||||
Sync.RunUnsynced(world, orderManager.TickImmediate);
|
||||
|
||||
if (world == null)
|
||||
{
|
||||
if (orderManager.GameStarted)
|
||||
PerfHistory.Reset(); // Remove old history when a new game starts.
|
||||
return;
|
||||
}
|
||||
|
||||
if (orderManager.TryTick())
|
||||
{
|
||||
@@ -664,7 +679,7 @@ namespace OpenRA
|
||||
// Prepare renderables (i.e. render voxels) before calling BeginFrame
|
||||
using (new PerfSample("render_prepare"))
|
||||
{
|
||||
Renderer.WorldModelRenderer.BeginFrame();
|
||||
worldRenderer?.BeginFrame();
|
||||
|
||||
// World rendering is disabled while the loading screen is displayed
|
||||
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
|
||||
@@ -674,7 +689,7 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
Ui.PrepareRenderables();
|
||||
Renderer.WorldModelRenderer.EndFrame();
|
||||
worldRenderer?.EndFrame();
|
||||
}
|
||||
|
||||
// worldRenderer is null during the initial install/download screen
|
||||
@@ -778,7 +793,7 @@ namespace OpenRA
|
||||
var logicWorld = worldRenderer?.World;
|
||||
|
||||
// ReplayTimestep = 0 means the replay is paused: we need to keep logicInterval as UI.Timestep to avoid breakage
|
||||
if (logicWorld != null && !(logicWorld.IsReplay && logicWorld.ReplayTimestep == 0))
|
||||
if (logicWorld != null && (!logicWorld.IsReplay || logicWorld.ReplayTimestep != 0))
|
||||
logicInterval = logicWorld == OrderManager.World ? OrderManager.SuggestedTimestep : logicWorld.Timestep;
|
||||
|
||||
// Ideal time between screen updates
|
||||
@@ -913,15 +928,15 @@ namespace OpenRA
|
||||
{
|
||||
var endpoints = new List<IPEndPoint>
|
||||
{
|
||||
new IPEndPoint(IPAddress.IPv6Any, settings.ListenPort),
|
||||
new IPEndPoint(IPAddress.Any, settings.ListenPort)
|
||||
new(IPAddress.IPv6Any, settings.ListenPort),
|
||||
new(IPAddress.Any, settings.ListenPort)
|
||||
};
|
||||
server = new Server.Server(endpoints, settings, ModData, ServerType.Multiplayer);
|
||||
|
||||
return server.GetEndpointForLocalConnection();
|
||||
}
|
||||
|
||||
public static ConnectionTarget CreateLocalServer(string map)
|
||||
public static ConnectionTarget CreateLocalServer(string map, bool isSkirmish = false)
|
||||
{
|
||||
var settings = new ServerSettings()
|
||||
{
|
||||
@@ -935,9 +950,9 @@ namespace OpenRA
|
||||
// This would break the Restart button, which relies on the PlayerIndex always being the same for local servers
|
||||
var endpoints = new List<IPEndPoint>
|
||||
{
|
||||
new IPEndPoint(IPAddress.Loopback, 0)
|
||||
new(IPAddress.Loopback, 0)
|
||||
};
|
||||
server = new Server.Server(endpoints, settings, ModData, ServerType.Local);
|
||||
server = new Server.Server(endpoints, settings, ModData, isSkirmish ? ServerType.Skirmish : ServerType.Local);
|
||||
|
||||
return server.GetEndpointForLocalConnection();
|
||||
}
|
||||
|
||||
@@ -49,13 +49,13 @@ namespace OpenRA
|
||||
playersByRuntime = new Dictionary<OpenRA.Player, Player>();
|
||||
}
|
||||
|
||||
public static GameInformation Deserialize(string data)
|
||||
public static GameInformation Deserialize(string data, string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = new GameInformation();
|
||||
|
||||
var nodes = MiniYaml.FromString(data);
|
||||
var nodes = MiniYaml.FromString(data, path);
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var keyParts = node.Key.Split('@');
|
||||
@@ -85,7 +85,7 @@ namespace OpenRA
|
||||
{
|
||||
var nodes = new List<MiniYamlNode>
|
||||
{
|
||||
new MiniYamlNode("Root", FieldSaver.Save(this))
|
||||
new("Root", FieldSaver.Save(this))
|
||||
};
|
||||
|
||||
for (var i = 0; i < Players.Count; i++)
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace OpenRA
|
||||
/// </summary>
|
||||
public class ActorInfo
|
||||
{
|
||||
public const string AbstractActorPrefix = "^";
|
||||
public const char AbstractActorPrefix = '^';
|
||||
public const char TraitInstanceSeparator = '@';
|
||||
|
||||
/// <summary>
|
||||
@@ -130,6 +130,7 @@ namespace OpenRA
|
||||
|
||||
// Continue resolving traits as long as possible.
|
||||
// Each time we resolve some traits, this means dependencies for other traits may then be possible to satisfy in the next pass.
|
||||
#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection
|
||||
var readyToResolve = more.ToList();
|
||||
while (readyToResolve.Count != 0)
|
||||
{
|
||||
@@ -138,6 +139,7 @@ namespace OpenRA
|
||||
readyToResolve.Clear();
|
||||
readyToResolve.AddRange(more);
|
||||
}
|
||||
#pragma warning restore CA1851
|
||||
|
||||
if (unresolved.Count != 0)
|
||||
{
|
||||
@@ -160,7 +162,7 @@ namespace OpenRA
|
||||
throw new YamlException(exceptionString);
|
||||
}
|
||||
|
||||
constructOrderCache = resolved.Select(r => r.Trait).ToList();
|
||||
constructOrderCache = resolved.ConvertAll(r => r.Trait);
|
||||
return constructOrderCache;
|
||||
}
|
||||
|
||||
@@ -185,7 +187,7 @@ namespace OpenRA
|
||||
public bool HasTraitInfo<T>() where T : ITraitInfoInterface { return traits.Contains<T>(); }
|
||||
public T TraitInfo<T>() where T : ITraitInfoInterface { return traits.Get<T>(); }
|
||||
public T TraitInfoOrDefault<T>() where T : ITraitInfoInterface { return traits.GetOrDefault<T>(); }
|
||||
public IEnumerable<T> TraitInfos<T>() where T : ITraitInfoInterface { return traits.WithInterface<T>(); }
|
||||
public IReadOnlyCollection<T> TraitInfos<T>() where T : ITraitInfoInterface { return traits.WithInterface<T>(); }
|
||||
|
||||
public BitSet<TargetableType> GetAllTargetTypes()
|
||||
{
|
||||
|
||||
@@ -124,7 +124,7 @@ namespace OpenRA
|
||||
{
|
||||
var actors = MergeOrDefault("Manifest,Rules", fs, m.Rules, null, null,
|
||||
k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value),
|
||||
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal));
|
||||
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix));
|
||||
|
||||
var weapons = MergeOrDefault("Manifest,Weapons", fs, m.Weapons, null, null,
|
||||
k => new WeaponInfo(k.Value));
|
||||
@@ -182,7 +182,7 @@ namespace OpenRA
|
||||
{
|
||||
var actors = MergeOrDefault("Rules", fileSystem, m.Rules, mapRules, dr.Actors,
|
||||
k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value),
|
||||
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal));
|
||||
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix));
|
||||
|
||||
var weapons = MergeOrDefault("Weapons", fileSystem, m.Weapons, mapWeapons, dr.Weapons,
|
||||
k => new WeaponInfo(k.Value));
|
||||
@@ -226,10 +226,10 @@ namespace OpenRA
|
||||
|
||||
static bool AnyCustomYaml(MiniYaml yaml)
|
||||
{
|
||||
return yaml != null && (yaml.Value != null || yaml.Nodes.Count > 0);
|
||||
return yaml != null && (yaml.Value != null || yaml.Nodes.Length > 0);
|
||||
}
|
||||
|
||||
static bool AnyFlaggedTraits(ModData modData, List<MiniYamlNode> actors)
|
||||
static bool AnyFlaggedTraits(ModData modData, IEnumerable<MiniYamlNode> actors)
|
||||
{
|
||||
foreach (var actorNode in actors)
|
||||
{
|
||||
@@ -260,18 +260,18 @@ namespace OpenRA
|
||||
return true;
|
||||
|
||||
// Any trait overrides that aren't explicitly whitelisted are flagged
|
||||
if (mapRules != null)
|
||||
{
|
||||
if (AnyFlaggedTraits(modData, mapRules.Nodes))
|
||||
return true;
|
||||
if (mapRules == null)
|
||||
return false;
|
||||
|
||||
if (mapRules.Value != null)
|
||||
{
|
||||
var mapFiles = FieldLoader.GetValue<string[]>("value", mapRules.Value);
|
||||
foreach (var f in mapFiles)
|
||||
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
|
||||
return true;
|
||||
}
|
||||
if (AnyFlaggedTraits(modData, mapRules.Nodes))
|
||||
return true;
|
||||
|
||||
if (mapRules.Value != null)
|
||||
{
|
||||
var mapFiles = FieldLoader.GetValue<string[]>("value", mapRules.Value);
|
||||
foreach (var f in mapFiles)
|
||||
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -40,16 +40,16 @@ namespace OpenRA.GameRules
|
||||
static Dictionary<string, SoundPool> ParseSoundPool(MiniYaml y, string key)
|
||||
{
|
||||
var ret = new Dictionary<string, SoundPool>();
|
||||
var classifiction = y.Nodes.First(x => x.Key == key);
|
||||
var classifiction = y.NodeWithKey(key);
|
||||
foreach (var t in classifiction.Value.Nodes)
|
||||
{
|
||||
var volumeModifier = 1f;
|
||||
var volumeModifierNode = t.Value.Nodes.FirstOrDefault(x => x.Key == nameof(SoundPool.VolumeModifier));
|
||||
var volumeModifierNode = t.Value.NodeWithKeyOrDefault(nameof(SoundPool.VolumeModifier));
|
||||
if (volumeModifierNode != null)
|
||||
volumeModifier = FieldLoader.GetValue<float>(volumeModifierNode.Key, volumeModifierNode.Value.Value);
|
||||
|
||||
var interruptType = SoundPool.DefaultInterruptType;
|
||||
var interruptTypeNode = t.Value.Nodes.FirstOrDefault(x => x.Key == nameof(SoundPool.InterruptType));
|
||||
var interruptTypeNode = t.Value.NodeWithKeyOrDefault(nameof(SoundPool.InterruptType));
|
||||
if (interruptTypeNode != null)
|
||||
interruptType = FieldLoader.GetValue<SoundPool.InterruptType>(interruptTypeNode.Key, interruptTypeNode.Value.Value);
|
||||
|
||||
|
||||
@@ -139,13 +139,14 @@ namespace OpenRA.GameRules
|
||||
{
|
||||
// Resolve any weapon-level yaml inheritance or removals
|
||||
// HACK: The "Defaults" sequence syntax prevents us from doing this generally during yaml parsing
|
||||
content.Nodes = MiniYaml.Merge(new[] { content.Nodes });
|
||||
content = content.WithNodes(MiniYaml.Merge(new IReadOnlyCollection<MiniYamlNode>[] { content.Nodes }));
|
||||
FieldLoader.Load(this, content);
|
||||
}
|
||||
|
||||
static object LoadProjectile(MiniYaml yaml)
|
||||
{
|
||||
if (!yaml.ToDictionary().TryGetValue("Projectile", out var proj))
|
||||
var proj = yaml.NodeWithKeyOrDefault("Projectile")?.Value;
|
||||
if (proj == null)
|
||||
return null;
|
||||
|
||||
var ret = Game.CreateObject<IProjectileInfo>(proj.Value + "Info");
|
||||
@@ -159,7 +160,7 @@ namespace OpenRA.GameRules
|
||||
static object LoadWarheads(MiniYaml yaml)
|
||||
{
|
||||
var retList = new List<IWarhead>();
|
||||
foreach (var node in yaml.Nodes.Where(n => n.Key.StartsWith("Warhead")))
|
||||
foreach (var node in yaml.Nodes.Where(n => n.Key.StartsWith("Warhead", StringComparison.Ordinal)))
|
||||
{
|
||||
var ret = Game.CreateObject<IWarhead>(node.Value.Value + "Warhead");
|
||||
if (ret == null)
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
@@ -38,7 +37,7 @@ namespace OpenRA
|
||||
static object LoadSpeeds(MiniYaml y)
|
||||
{
|
||||
var ret = new Dictionary<string, GameSpeed>();
|
||||
var speedsNode = y.Nodes.FirstOrDefault(n => n.Key == "Speeds");
|
||||
var speedsNode = y.NodeWithKeyOrDefault("Speeds");
|
||||
if (speedsNode == null)
|
||||
throw new YamlException("Error parsing GameSpeeds: Missing Speeds node!");
|
||||
|
||||
|
||||
@@ -77,11 +77,12 @@ namespace OpenRA.Graphics
|
||||
cachedPanelSprites = new Dictionary<string, Sprite[]>();
|
||||
cachedCollectionSheets = new Dictionary<Collection, (Sheet, int)>();
|
||||
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
var chrome = MiniYaml.Merge(modData.Manifest.Chrome
|
||||
.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s)));
|
||||
.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool)));
|
||||
|
||||
foreach (var c in chrome)
|
||||
if (!c.Key.StartsWith("^", StringComparison.Ordinal))
|
||||
if (!c.Key.StartsWith('^'))
|
||||
LoadCollection(c.Key, c.Value);
|
||||
}
|
||||
|
||||
@@ -234,7 +235,7 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
// PERF: We don't need to search for images if there are no definitions.
|
||||
// PERF: It's more efficient to send an empty array rather than an array of 9 nulls.
|
||||
if (!collection.Regions.Any())
|
||||
if (collection.Regions.Count == 0)
|
||||
return Array.Empty<Sprite>();
|
||||
|
||||
// Support manual definitions for unusual dialog layouts
|
||||
|
||||
@@ -24,10 +24,11 @@ namespace OpenRA.Graphics
|
||||
public CursorProvider(ModData modData)
|
||||
{
|
||||
var fileSystem = modData.DefaultFileSystem;
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
var sequenceYaml = MiniYaml.Merge(modData.Manifest.Cursors.Select(
|
||||
s => MiniYaml.FromStream(fileSystem.Open(s), s)));
|
||||
s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool)));
|
||||
|
||||
var nodesDict = new MiniYaml(null, sequenceYaml).ToDictionary();
|
||||
var cursorsYaml = new MiniYaml(null, sequenceYaml).NodeWithKey("Cursors").Value;
|
||||
|
||||
// Overwrite previous definitions if there are duplicates
|
||||
var pals = new Dictionary<string, IProvidesCursorPaletteInfo>();
|
||||
@@ -35,14 +36,14 @@ namespace OpenRA.Graphics
|
||||
if (p.Palette != null)
|
||||
pals[p.Palette] = p;
|
||||
|
||||
Palettes = nodesDict["Cursors"].Nodes.Select(n => n.Value.Value)
|
||||
Palettes = cursorsYaml.Nodes.Select(n => n.Value.Value)
|
||||
.Where(p => p != null)
|
||||
.Distinct()
|
||||
.ToDictionary(p => p, p => pals[p].ReadPalette(modData.DefaultFileSystem));
|
||||
|
||||
var frameCache = new FrameCache(fileSystem, modData.SpriteLoaders);
|
||||
var cursors = new Dictionary<string, CursorSequence>();
|
||||
foreach (var s in nodesDict["Cursors"].Nodes)
|
||||
foreach (var s in cursorsYaml.Nodes)
|
||||
foreach (var sequence in s.Value.Nodes)
|
||||
cursors.Add(sequence.Key, new CursorSequence(frameCache, sequence.Key, s.Key, s.Value.Value, sequence.Value));
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
var d = info.ToDictionary();
|
||||
|
||||
Start = Exts.ParseIntegerInvariant(d["Start"].Value);
|
||||
Start = Exts.ParseInt32Invariant(d["Start"].Value);
|
||||
Palette = palette;
|
||||
Name = name;
|
||||
|
||||
@@ -38,9 +38,9 @@ namespace OpenRA.Graphics
|
||||
(d.TryGetValue("End", out yaml) && yaml.Value == "*"))
|
||||
Length = Frames.Length;
|
||||
else if (d.TryGetValue("Length", out yaml))
|
||||
Length = Exts.ParseIntegerInvariant(yaml.Value);
|
||||
Length = Exts.ParseInt32Invariant(yaml.Value);
|
||||
else if (d.TryGetValue("End", out yaml))
|
||||
Length = Exts.ParseIntegerInvariant(yaml.Value) - Start;
|
||||
Length = Exts.ParseInt32Invariant(yaml.Value) - Start;
|
||||
else
|
||||
Length = 1;
|
||||
|
||||
@@ -54,13 +54,13 @@ namespace OpenRA.Graphics
|
||||
|
||||
if (d.TryGetValue("X", out yaml))
|
||||
{
|
||||
Exts.TryParseIntegerInvariant(yaml.Value, out var x);
|
||||
Exts.TryParseInt32Invariant(yaml.Value, out var x);
|
||||
Hotspot = Hotspot.WithX(x);
|
||||
}
|
||||
|
||||
if (d.TryGetValue("Y", out yaml))
|
||||
{
|
||||
Exts.TryParseIntegerInvariant(yaml.Value, out var y);
|
||||
Exts.TryParseInt32Invariant(yaml.Value, out var y);
|
||||
Hotspot = Hotspot.WithY(y);
|
||||
}
|
||||
}
|
||||
|
||||
56
OpenRA.Game/Graphics/MarkerTileRenderable.cs
Normal file
56
OpenRA.Game/Graphics/MarkerTileRenderable.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Linq;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public class MarkerTileRenderable : IRenderable, IFinalizedRenderable
|
||||
{
|
||||
readonly CPos pos;
|
||||
readonly Color color;
|
||||
|
||||
public MarkerTileRenderable(CPos pos, Color color)
|
||||
{
|
||||
this.pos = pos;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public WPos Pos => WPos.Zero;
|
||||
public int ZOffset => 0;
|
||||
public bool IsDecoration => true;
|
||||
|
||||
public IRenderable WithZOffset(int newOffset) { return this; }
|
||||
|
||||
public IRenderable OffsetBy(in WVec vec)
|
||||
{
|
||||
return new MarkerTileRenderable(pos, color);
|
||||
}
|
||||
|
||||
public IRenderable AsDecoration() { return this; }
|
||||
|
||||
public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; }
|
||||
public void Render(WorldRenderer wr)
|
||||
{
|
||||
var map = wr.World.Map;
|
||||
var r = map.Grid.Ramps[map.Ramp[pos]];
|
||||
var wpos = map.CenterOfCell(pos) - new WVec(0, 0, r.CenterHeightOffset);
|
||||
|
||||
var corners = r.Corners.Select(corner => wr.Viewport.WorldToViewPx(wr.Screen3DPosition(wpos + corner))).ToList();
|
||||
|
||||
Game.Renderer.RgbaColorRenderer.FillRect(corners[0], corners[1], corners[2], corners[3], color);
|
||||
}
|
||||
|
||||
public void RenderDebugGeometry(WorldRenderer wr) { }
|
||||
public Rectangle ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; }
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,8 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using OpenRA.FileSystem;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
@@ -30,6 +29,14 @@ namespace OpenRA.Graphics
|
||||
Rectangle AggregateBounds { get; }
|
||||
}
|
||||
|
||||
public interface IModelWidget
|
||||
{
|
||||
public string Palette { get; }
|
||||
public float Scale { get; }
|
||||
public void Setup(Func<bool> isVisible, Func<string> getPalette, Func<string> getPlayerPalette,
|
||||
Func<float> getScale, Func<IModel> getVoxel, Func<WRot> getRotation);
|
||||
}
|
||||
|
||||
public readonly struct ModelRenderData
|
||||
{
|
||||
public readonly int Start;
|
||||
@@ -44,51 +51,13 @@ namespace OpenRA.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
public interface IModelCache : IDisposable
|
||||
public interface IModelCacheInfo : ITraitInfoInterface { }
|
||||
|
||||
public interface IModelCache
|
||||
{
|
||||
IModel GetModel(string model);
|
||||
IModel GetModelSequence(string model, string sequence);
|
||||
bool HasModelSequence(string model, string sequence);
|
||||
IVertexBuffer<Vertex> VertexBuffer { get; }
|
||||
}
|
||||
|
||||
public interface IModelSequenceLoader
|
||||
{
|
||||
Action<string> OnMissingModelError { get; set; }
|
||||
IModelCache CacheModels(IReadOnlyFileSystem fileSystem, ModData modData, IReadOnlyDictionary<string, MiniYamlNode> modelDefinitions);
|
||||
}
|
||||
|
||||
public class PlaceholderModelSequenceLoader : IModelSequenceLoader
|
||||
{
|
||||
public Action<string> OnMissingModelError { get; set; }
|
||||
|
||||
sealed class PlaceholderModelCache : IModelCache
|
||||
{
|
||||
public IVertexBuffer<Vertex> VertexBuffer => throw new NotImplementedException();
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public IModel GetModel(string model)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IModel GetModelSequence(string model, string sequence)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool HasModelSequence(string model, string sequence)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public PlaceholderModelSequenceLoader(ModData modData) { }
|
||||
|
||||
public IModelCache CacheModels(IReadOnlyFileSystem fileSystem, ModData modData, IReadOnlyDictionary<string, MiniYamlNode> modelDefinitions)
|
||||
{
|
||||
return new PlaceholderModelCache();
|
||||
}
|
||||
IVertexBuffer<ModelVertex> VertexBuffer { get; }
|
||||
}
|
||||
}
|
||||
|
||||
53
OpenRA.Game/Graphics/ModelVertex.cs
Normal file
53
OpenRA.Game/Graphics/ModelVertex.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly struct ModelVertex
|
||||
{
|
||||
// 3d position
|
||||
public readonly float X, Y, Z;
|
||||
|
||||
// Primary and secondary texture coordinates or RGBA color
|
||||
public readonly float S, T, U, V;
|
||||
|
||||
// Palette and channel flags
|
||||
public readonly float P, C;
|
||||
|
||||
public ModelVertex(in float3 xyz, float s, float t, float u, float v, float p, float c)
|
||||
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, p, c) { }
|
||||
|
||||
public ModelVertex(float x, float y, float z, float s, float t, float u, float v, float p, float c)
|
||||
{
|
||||
X = x; Y = y; Z = z;
|
||||
S = s; T = t;
|
||||
U = u; V = v;
|
||||
P = p; C = c;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ModelShaderBindings : ShaderBindings
|
||||
{
|
||||
public ModelShaderBindings()
|
||||
: base("model")
|
||||
{ }
|
||||
|
||||
public override ShaderVertexAttribute[] Attributes { get; } = new[]
|
||||
{
|
||||
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
|
||||
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
|
||||
new ShaderVertexAttribute("aVertexTexMetadata", ShaderVertexAttributeType.Float, 2, 28),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ namespace OpenRA.Graphics
|
||||
|
||||
public static Color GetColor(this IPalette palette, int index)
|
||||
{
|
||||
return Color.FromArgb((int)palette[index]);
|
||||
return Color.FromArgb(palette[index]);
|
||||
}
|
||||
|
||||
public static IPalette AsReadOnly(this IPalette palette)
|
||||
@@ -103,7 +103,7 @@ namespace OpenRA.Graphics
|
||||
: this(p)
|
||||
{
|
||||
for (var i = 0; i < Palette.Size; i++)
|
||||
colors[i] = (uint)r.GetRemappedColor(this.GetColor(i), i).ToArgb();
|
||||
colors[i] = r.GetRemappedColor(this.GetColor(i), i).ToArgb();
|
||||
}
|
||||
|
||||
public ImmutablePalette(IPalette p)
|
||||
@@ -142,7 +142,7 @@ namespace OpenRA.Graphics
|
||||
|
||||
public void SetColor(int index, Color color)
|
||||
{
|
||||
colors[index] = (uint)color.ToArgb();
|
||||
colors[index] = color.ToArgb();
|
||||
}
|
||||
|
||||
public void SetFromPalette(IPalette p)
|
||||
@@ -153,7 +153,7 @@ namespace OpenRA.Graphics
|
||||
public void ApplyRemap(IPaletteRemap r)
|
||||
{
|
||||
for (var i = 0; i < Palette.Size; i++)
|
||||
colors[i] = (uint)r.GetRemappedColor(this.GetColor(i), i).ToArgb();
|
||||
colors[i] = r.GetRemappedColor(this.GetColor(i), i).ToArgb();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,19 +13,17 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
public sealed class PaletteReference
|
||||
{
|
||||
readonly float index;
|
||||
readonly HardwarePalette hardwarePalette;
|
||||
|
||||
public readonly string Name;
|
||||
public IPalette Palette { get; internal set; }
|
||||
public float TextureIndex => index / hardwarePalette.Height;
|
||||
public float TextureMidIndex => (index + 0.5f) / hardwarePalette.Height;
|
||||
public int TextureIndex { get; }
|
||||
|
||||
public PaletteReference(string name, int index, IPalette palette, HardwarePalette hardwarePalette)
|
||||
{
|
||||
Name = name;
|
||||
Palette = palette;
|
||||
this.index = index;
|
||||
TextureIndex = index;
|
||||
this.hardwarePalette = hardwarePalette;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,12 @@ namespace OpenRA
|
||||
Automatic,
|
||||
ANGLE,
|
||||
Modern,
|
||||
Embedded,
|
||||
Legacy
|
||||
Embedded
|
||||
}
|
||||
|
||||
public interface IPlatform
|
||||
{
|
||||
IPlatformWindow CreateWindow(Size size, WindowMode windowMode, float scaleModifier, int batchSize, int videoDisplay, GLProfile profile, bool enableLegacyGL);
|
||||
IPlatformWindow CreateWindow(Size size, WindowMode windowMode, float scaleModifier, int vertexBatchSize, int indexBatchSize, int videoDisplay, GLProfile profile);
|
||||
ISoundEngine CreateSound(string device);
|
||||
IFont CreateFont(byte[] data);
|
||||
}
|
||||
@@ -83,16 +82,18 @@ namespace OpenRA
|
||||
|
||||
public interface IGraphicsContext : IDisposable
|
||||
{
|
||||
IVertexBuffer<Vertex> CreateVertexBuffer(int size);
|
||||
Vertex[] CreateVertices(int size);
|
||||
IVertexBuffer<T> CreateVertexBuffer<T>(int size) where T : struct;
|
||||
T[] CreateVertices<T>(int size) where T : struct;
|
||||
IIndexBuffer CreateIndexBuffer(uint[] indices);
|
||||
ITexture CreateTexture();
|
||||
IFrameBuffer CreateFrameBuffer(Size s);
|
||||
IFrameBuffer CreateFrameBuffer(Size s, Color clearColor);
|
||||
IShader CreateShader(string name);
|
||||
IShader CreateShader(IShaderBindings shaderBindings);
|
||||
void EnableScissor(int x, int y, int width, int height);
|
||||
void DisableScissor();
|
||||
void Present();
|
||||
void DrawPrimitives(PrimitiveType pt, int firstVertex, int numVertices);
|
||||
void DrawElements(int numIndices, int offset);
|
||||
void Clear();
|
||||
void EnableDepthBuffer();
|
||||
void DisableDepthBuffer();
|
||||
@@ -102,7 +103,14 @@ namespace OpenRA
|
||||
string GLVersion { get; }
|
||||
}
|
||||
|
||||
public interface IVertexBuffer<T> : IDisposable
|
||||
public interface IRenderer
|
||||
{
|
||||
void BeginFrame();
|
||||
void EndFrame();
|
||||
void SetPalette(HardwarePalette palette);
|
||||
}
|
||||
|
||||
public interface IVertexBuffer<T> : IDisposable where T : struct
|
||||
{
|
||||
void Bind();
|
||||
void SetData(T[] vertices, int length);
|
||||
@@ -114,6 +122,11 @@ namespace OpenRA
|
||||
void SetData(T[] vertices, int offset, int start, int length);
|
||||
}
|
||||
|
||||
public interface IIndexBuffer : IDisposable
|
||||
{
|
||||
void Bind();
|
||||
}
|
||||
|
||||
public interface IShader
|
||||
{
|
||||
void SetBool(string name, bool value);
|
||||
@@ -124,6 +137,17 @@ namespace OpenRA
|
||||
void SetTexture(string param, ITexture texture);
|
||||
void SetMatrix(string param, float[] mtx);
|
||||
void PrepareRender();
|
||||
void Bind();
|
||||
}
|
||||
|
||||
public interface IShaderBindings
|
||||
{
|
||||
string VertexShaderName { get; }
|
||||
string VertexShaderCode { get; }
|
||||
string FragmentShaderName { get; }
|
||||
string FragmentShaderCode { get; }
|
||||
int Stride { get; }
|
||||
ShaderVertexAttribute[] Attributes { get; }
|
||||
}
|
||||
|
||||
public enum TextureScaleFilter { Nearest, Linear }
|
||||
@@ -132,6 +156,7 @@ namespace OpenRA
|
||||
{
|
||||
void SetData(byte[] colors, int width, int height);
|
||||
void SetFloatData(float[] data, int width, int height);
|
||||
void SetDataFromReadBuffer(Rectangle rect);
|
||||
byte[] GetData();
|
||||
Size Size { get; }
|
||||
TextureScaleFilter ScaleFilter { get; set; }
|
||||
|
||||
64
OpenRA.Game/Graphics/RenderPostProcessPassVertex.cs
Normal file
64
OpenRA.Game/Graphics/RenderPostProcessPassVertex.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly struct RenderPostProcessPassVertex
|
||||
{
|
||||
public readonly float X, Y;
|
||||
|
||||
public RenderPostProcessPassVertex(float x, float y)
|
||||
{
|
||||
X = x; Y = y;
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public readonly struct RenderPostProcessPassTexturedVertex
|
||||
{
|
||||
// 3d position
|
||||
public readonly float X, Y;
|
||||
public readonly float S, T;
|
||||
|
||||
public RenderPostProcessPassTexturedVertex(float x, float y, float s, float t)
|
||||
{
|
||||
X = x; Y = y;
|
||||
S = s; T = t;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RenderPostProcessPassShaderBindings : ShaderBindings
|
||||
{
|
||||
public RenderPostProcessPassShaderBindings(string name)
|
||||
: base("postprocess", "postprocess_" + name) { }
|
||||
|
||||
public override ShaderVertexAttribute[] Attributes { get; } = new[]
|
||||
{
|
||||
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0)
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class RenderPostProcessPassTexturedShaderBindings : ShaderBindings
|
||||
{
|
||||
public RenderPostProcessPassTexturedShaderBindings(string name)
|
||||
: base("postprocess_textured", "postprocess_textured_" + name)
|
||||
{ }
|
||||
|
||||
public override ShaderVertexAttribute[] Attributes { get; } = new[]
|
||||
{
|
||||
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0),
|
||||
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 2, 8),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ namespace OpenRA.Graphics
|
||||
static readonly float3 Offset = new(0.5f, 0.5f, 0f);
|
||||
|
||||
readonly SpriteRenderer parent;
|
||||
readonly Vertex[] vertices = new Vertex[6];
|
||||
readonly Vertex[] vertices = new Vertex[4];
|
||||
|
||||
public RgbaColorRenderer(SpriteRenderer parent)
|
||||
{
|
||||
@@ -45,14 +45,12 @@ namespace OpenRA.Graphics
|
||||
var eb = endColor.B / 255.0f;
|
||||
var ea = endColor.A / 255.0f;
|
||||
|
||||
vertices[0] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0, 0);
|
||||
vertices[1] = new Vertex(start + corner + Offset, sr, sg, sb, sa, 0, 0);
|
||||
vertices[2] = new Vertex(end + corner + Offset, er, eg, eb, ea, 0, 0);
|
||||
vertices[3] = new Vertex(end + corner + Offset, er, eg, eb, ea, 0, 0);
|
||||
vertices[4] = new Vertex(end - corner + Offset, er, eg, eb, ea, 0, 0);
|
||||
vertices[5] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0, 0);
|
||||
vertices[0] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0);
|
||||
vertices[1] = new Vertex(start + corner + Offset, sr, sg, sb, sa, 0);
|
||||
vertices[2] = new Vertex(end + corner + Offset, er, eg, eb, ea, 0);
|
||||
vertices[3] = new Vertex(end - corner + Offset, er, eg, eb, ea, 0);
|
||||
|
||||
parent.DrawRGBAVertices(vertices, blendMode);
|
||||
parent.DrawRGBAQuad(vertices, blendMode);
|
||||
}
|
||||
|
||||
public void DrawLine(in float3 start, in float3 end, float width, Color color, BlendMode blendMode = BlendMode.Alpha)
|
||||
@@ -66,13 +64,11 @@ namespace OpenRA.Graphics
|
||||
var b = color.B / 255.0f;
|
||||
var a = color.A / 255.0f;
|
||||
|
||||
vertices[0] = new Vertex(start - corner + Offset, r, g, b, a, 0, 0);
|
||||
vertices[1] = new Vertex(start + corner + Offset, r, g, b, a, 0, 0);
|
||||
vertices[2] = new Vertex(end + corner + Offset, r, g, b, a, 0, 0);
|
||||
vertices[3] = new Vertex(end + corner + Offset, r, g, b, a, 0, 0);
|
||||
vertices[4] = new Vertex(end - corner + Offset, r, g, b, a, 0, 0);
|
||||
vertices[5] = new Vertex(start - corner + Offset, r, g, b, a, 0, 0);
|
||||
parent.DrawRGBAVertices(vertices, blendMode);
|
||||
vertices[0] = new Vertex(start - corner + Offset, r, g, b, a, 0);
|
||||
vertices[1] = new Vertex(start + corner + Offset, r, g, b, a, 0);
|
||||
vertices[2] = new Vertex(end + corner + Offset, r, g, b, a, 0);
|
||||
vertices[3] = new Vertex(end - corner + Offset, r, g, b, a, 0);
|
||||
parent.DrawRGBAQuad(vertices, blendMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -157,13 +153,11 @@ namespace OpenRA.Graphics
|
||||
var cd = closed || i < limit - 1 ? IntersectionOf(end - corner, dir, end - nextCorner, nextDir) : end - corner;
|
||||
|
||||
// Fill segment
|
||||
vertices[0] = new Vertex(ca + Offset, r, g, b, a, 0, 0);
|
||||
vertices[1] = new Vertex(cb + Offset, r, g, b, a, 0, 0);
|
||||
vertices[2] = new Vertex(cc + Offset, r, g, b, a, 0, 0);
|
||||
vertices[3] = new Vertex(cc + Offset, r, g, b, a, 0, 0);
|
||||
vertices[4] = new Vertex(cd + Offset, r, g, b, a, 0, 0);
|
||||
vertices[5] = new Vertex(ca + Offset, r, g, b, a, 0, 0);
|
||||
parent.DrawRGBAVertices(vertices, blendMode);
|
||||
vertices[0] = new Vertex(ca + Offset, r, g, b, a, 0);
|
||||
vertices[1] = new Vertex(cb + Offset, r, g, b, a, 0);
|
||||
vertices[2] = new Vertex(cc + Offset, r, g, b, a, 0);
|
||||
vertices[3] = new Vertex(cd + Offset, r, g, b, a, 0);
|
||||
parent.DrawRGBAQuad(vertices, blendMode);
|
||||
|
||||
// Advance line segment
|
||||
end = next;
|
||||
@@ -200,20 +194,6 @@ namespace OpenRA.Graphics
|
||||
DrawPolygon(new[] { tl, tr, br, bl }, width, color, blendMode);
|
||||
}
|
||||
|
||||
public void FillTriangle(in float3 a, in float3 b, in float3 c, Color color, BlendMode blendMode = BlendMode.Alpha)
|
||||
{
|
||||
color = Util.PremultiplyAlpha(color);
|
||||
var cr = color.R / 255.0f;
|
||||
var cg = color.G / 255.0f;
|
||||
var cb = color.B / 255.0f;
|
||||
var ca = color.A / 255.0f;
|
||||
|
||||
vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0);
|
||||
vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0, 0);
|
||||
vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
|
||||
parent.DrawRGBAVertices(vertices, blendMode);
|
||||
}
|
||||
|
||||
public void FillRect(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha)
|
||||
{
|
||||
var tr = new float3(br.X, tl.Y, tl.Z);
|
||||
@@ -229,13 +209,11 @@ namespace OpenRA.Graphics
|
||||
var cb = color.B / 255.0f;
|
||||
var ca = color.A / 255.0f;
|
||||
|
||||
vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0);
|
||||
vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0, 0);
|
||||
vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
|
||||
vertices[3] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
|
||||
vertices[4] = new Vertex(d + Offset, cr, cg, cb, ca, 0, 0);
|
||||
vertices[5] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0);
|
||||
parent.DrawRGBAVertices(vertices, blendMode);
|
||||
vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0);
|
||||
vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0);
|
||||
vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0);
|
||||
vertices[3] = new Vertex(d + Offset, cr, cg, cb, ca, 0);
|
||||
parent.DrawRGBAQuad(vertices, blendMode);
|
||||
}
|
||||
|
||||
public void FillRect(in float3 a, in float3 b, in float3 c, in float3 d, Color topLeftColor, Color topRightColor, Color bottomRightColor, Color bottomLeftColor, BlendMode blendMode = BlendMode.Alpha)
|
||||
@@ -243,11 +221,9 @@ namespace OpenRA.Graphics
|
||||
vertices[0] = VertexWithColor(a + Offset, topLeftColor);
|
||||
vertices[1] = VertexWithColor(b + Offset, topRightColor);
|
||||
vertices[2] = VertexWithColor(c + Offset, bottomRightColor);
|
||||
vertices[3] = VertexWithColor(c + Offset, bottomRightColor);
|
||||
vertices[4] = VertexWithColor(d + Offset, bottomLeftColor);
|
||||
vertices[5] = VertexWithColor(a + Offset, topLeftColor);
|
||||
vertices[3] = VertexWithColor(d + Offset, bottomLeftColor);
|
||||
|
||||
parent.DrawRGBAVertices(vertices, blendMode);
|
||||
parent.DrawRGBAQuad(vertices, blendMode);
|
||||
}
|
||||
|
||||
static Vertex VertexWithColor(in float3 xyz, Color color)
|
||||
@@ -258,7 +234,7 @@ namespace OpenRA.Graphics
|
||||
var cb = color.B / 255.0f;
|
||||
var ca = color.A / 255.0f;
|
||||
|
||||
return new Vertex(xyz, cr, cg, cb, ca, 0, 0);
|
||||
return new Vertex(xyz, cr, cg, cb, ca, 0);
|
||||
}
|
||||
|
||||
public void FillEllipse(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha)
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace OpenRA.Graphics
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
// Nodes starting with ^ are inheritable but never loaded directly
|
||||
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal))
|
||||
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix))
|
||||
continue;
|
||||
|
||||
images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node);
|
||||
|
||||
70
OpenRA.Game/Graphics/ShaderBindings.cs
Normal file
70
OpenRA.Game/Graphics/ShaderBindings.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public enum ShaderVertexAttributeType
|
||||
{
|
||||
// Assign the underlying OpenGL type values
|
||||
// to simplify enum use in the shader
|
||||
Float = 0x1406, // GL_FLOAT
|
||||
Int = 0x1404, // GL_INT
|
||||
UInt = 0x1405 // GL_UNSIGNED_INT
|
||||
}
|
||||
|
||||
public readonly struct ShaderVertexAttribute
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly ShaderVertexAttributeType Type;
|
||||
public readonly int Components;
|
||||
public readonly int Offset;
|
||||
|
||||
public ShaderVertexAttribute(string name, ShaderVertexAttributeType type, int components, int offset)
|
||||
{
|
||||
Name = name;
|
||||
Type = type;
|
||||
Components = components;
|
||||
Offset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class ShaderBindings : IShaderBindings
|
||||
{
|
||||
public string VertexShaderName { get; }
|
||||
public string VertexShaderCode { get; }
|
||||
public string FragmentShaderName { get; }
|
||||
public string FragmentShaderCode { get; }
|
||||
public int Stride { get; }
|
||||
|
||||
public abstract ShaderVertexAttribute[] Attributes { get; }
|
||||
|
||||
protected ShaderBindings(string name)
|
||||
: this(name, name) { }
|
||||
|
||||
protected ShaderBindings(string vertexName, string fragmentName)
|
||||
{
|
||||
Stride = Attributes.Sum(a => a.Components * 4);
|
||||
VertexShaderName = vertexName;
|
||||
VertexShaderCode = GetShaderCode(VertexShaderName + ".vert");
|
||||
FragmentShaderName = fragmentName;
|
||||
FragmentShaderCode = GetShaderCode(FragmentShaderName + ".frag");
|
||||
}
|
||||
|
||||
public static string GetShaderCode(string filename)
|
||||
{
|
||||
var filepath = Path.Combine(Platform.EngineDir, "glsl", filename);
|
||||
return File.ReadAllText(filepath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,16 +82,16 @@ namespace OpenRA.Graphics
|
||||
this.margin = margin;
|
||||
}
|
||||
|
||||
public Sprite Add(ISpriteFrame frame) { return Add(frame.Data, frame.Type, frame.Size, 0, frame.Offset); }
|
||||
public Sprite Add(byte[] src, SpriteFrameType type, Size size) { return Add(src, type, size, 0, float3.Zero); }
|
||||
public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset)
|
||||
public Sprite Add(ISpriteFrame frame, bool premultiplied = false) { return Add(frame.Data, frame.Type, frame.Size, 0, frame.Offset, premultiplied); }
|
||||
public Sprite Add(byte[] src, SpriteFrameType type, Size size, bool premultiplied = false) { return Add(src, type, size, 0, float3.Zero, premultiplied); }
|
||||
public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset, bool premultiplied = false)
|
||||
{
|
||||
// Don't bother allocating empty sprites
|
||||
if (size.Width == 0 || size.Height == 0)
|
||||
return new Sprite(Current, Rectangle.Empty, 0, spriteOffset, CurrentChannel, BlendMode.Alpha);
|
||||
|
||||
var rect = Allocate(size, zRamp, spriteOffset);
|
||||
Util.FastCopyIntoChannel(rect, src, type);
|
||||
Util.FastCopyIntoChannel(rect, src, type, premultiplied);
|
||||
Current.CommitBufferedData();
|
||||
return rect;
|
||||
}
|
||||
|
||||
@@ -42,11 +42,11 @@ namespace OpenRA.Graphics
|
||||
// in rendering a line of texels that sample outside the sprite rectangle.
|
||||
// Insetting the texture coordinates by a small fraction of a pixel avoids this
|
||||
// with negligible impact on the 1:1 rendering case.
|
||||
var inset = 1 / 128f;
|
||||
Left = (Math.Min(bounds.Left, bounds.Right) + inset) / sheet.Size.Width;
|
||||
Top = (Math.Min(bounds.Top, bounds.Bottom) + inset) / sheet.Size.Height;
|
||||
Right = (Math.Max(bounds.Left, bounds.Right) - inset) / sheet.Size.Width;
|
||||
Bottom = (Math.Max(bounds.Top, bounds.Bottom) - inset) / sheet.Size.Height;
|
||||
const float Inset = 1 / 128f;
|
||||
Left = (Math.Min(bounds.Left, bounds.Right) + Inset) / sheet.Size.Width;
|
||||
Top = (Math.Min(bounds.Top, bounds.Bottom) + Inset) / sheet.Size.Height;
|
||||
Right = (Math.Max(bounds.Left, bounds.Right) - Inset) / sheet.Size.Width;
|
||||
Bottom = (Math.Max(bounds.Top, bounds.Bottom) - Inset) / sheet.Size.Height;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,11 +24,9 @@ namespace OpenRA.Graphics
|
||||
readonly ISpriteLoader[] loaders;
|
||||
readonly IReadOnlyFileSystem fileSystem;
|
||||
|
||||
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> spriteReservations = new();
|
||||
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> frameReservations = new();
|
||||
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location, Func<ISpriteFrame, ISpriteFrame> AdjustFrame, bool Premultiplied)> spriteReservations = new();
|
||||
readonly Dictionary<string, List<int>> reservationsByFilename = new();
|
||||
|
||||
readonly Dictionary<int, ISpriteFrame[]> resolvedFrames = new();
|
||||
readonly Dictionary<int, Sprite[]> resolvedSprites = new();
|
||||
|
||||
readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = new();
|
||||
@@ -47,18 +45,10 @@ namespace OpenRA.Graphics
|
||||
this.loaders = loaders;
|
||||
}
|
||||
|
||||
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
|
||||
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location, Func<ISpriteFrame, ISpriteFrame> adjustFrame = null, bool premultiplied = false)
|
||||
{
|
||||
var token = nextReservationToken++;
|
||||
spriteReservations[token] = (frames?.ToArray(), location);
|
||||
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
|
||||
return token;
|
||||
}
|
||||
|
||||
public int ReserveFrames(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
|
||||
{
|
||||
var token = nextReservationToken++;
|
||||
frameReservations[token] = (frames?.ToArray(), location);
|
||||
spriteReservations[token] = (frames?.ToArray(), location, adjustFrame, premultiplied);
|
||||
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
|
||||
return token;
|
||||
}
|
||||
@@ -84,60 +74,73 @@ namespace OpenRA.Graphics
|
||||
foreach (var sb in SheetBuilders.Values)
|
||||
sb.Current.CreateBuffer();
|
||||
|
||||
var spriteCache = new Dictionary<int, Sprite>();
|
||||
var pendingResolve = new List<(
|
||||
string Filename,
|
||||
int FrameIndex,
|
||||
bool Premultiplied,
|
||||
Func<ISpriteFrame, ISpriteFrame> AdjustFrame,
|
||||
ISpriteFrame Frame,
|
||||
Sprite[] SpritesForToken)>();
|
||||
foreach (var (filename, tokens) in reservationsByFilename)
|
||||
{
|
||||
modData.LoadScreen?.Display();
|
||||
var loadedFrames = GetFrames(fileSystem, filename, loaders, out _);
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (frameReservations.TryGetValue(token, out var r))
|
||||
{
|
||||
if (loadedFrames != null)
|
||||
{
|
||||
if (r.Frames != null)
|
||||
{
|
||||
var resolved = new ISpriteFrame[loadedFrames.Length];
|
||||
foreach (var i in r.Frames)
|
||||
resolved[i] = loadedFrames[i];
|
||||
resolvedFrames[token] = resolved;
|
||||
}
|
||||
else
|
||||
resolvedFrames[token] = loadedFrames;
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedFrames[token] = null;
|
||||
missingFiles[token] = (filename, r.Location);
|
||||
}
|
||||
}
|
||||
|
||||
if (spriteReservations.TryGetValue(token, out r))
|
||||
if (spriteReservations.TryGetValue(token, out var rs))
|
||||
{
|
||||
if (loadedFrames != null)
|
||||
{
|
||||
var resolved = new Sprite[loadedFrames.Length];
|
||||
var frames = r.Frames ?? Enumerable.Range(0, loadedFrames.Length);
|
||||
foreach (var i in frames)
|
||||
resolved[i] = spriteCache.GetOrAdd(i,
|
||||
f => SheetBuilders[SheetBuilder.FrameTypeToSheetType(loadedFrames[f].Type)].Add(loadedFrames[f]));
|
||||
|
||||
resolvedSprites[token] = resolved;
|
||||
var frames = rs.Frames ?? Enumerable.Range(0, loadedFrames.Length);
|
||||
|
||||
foreach (var i in frames)
|
||||
{
|
||||
var frame = loadedFrames[i];
|
||||
if (rs.AdjustFrame != null)
|
||||
frame = rs.AdjustFrame(frame);
|
||||
pendingResolve.Add((filename, i, rs.Premultiplied, rs.AdjustFrame, frame, resolved));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedSprites[token] = null;
|
||||
missingFiles[token] = (filename, r.Location);
|
||||
missingFiles[token] = (filename, rs.Location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spriteCache.Clear();
|
||||
}
|
||||
|
||||
spriteReservations.Clear();
|
||||
frameReservations.Clear();
|
||||
spriteReservations.TrimExcess();
|
||||
reservationsByFilename.Clear();
|
||||
reservationsByFilename.TrimExcess();
|
||||
|
||||
// When the sheet builder is adding sprites, it reserves height for the tallest sprite seen along the row.
|
||||
// We can achieve better sheet packing by keeping sprites with similar heights together.
|
||||
var orderedPendingResolve = pendingResolve.OrderBy(x => x.Frame.Size.Height);
|
||||
|
||||
var spriteCache = new Dictionary<(
|
||||
string Filename,
|
||||
int FrameIndex,
|
||||
bool Premultiplied,
|
||||
Func<ISpriteFrame, ISpriteFrame> AdjustFrame),
|
||||
Sprite>(pendingResolve.Count);
|
||||
foreach (var (filename, frameIndex, premultiplied, adjustFrame, frame, spritesForToken) in orderedPendingResolve)
|
||||
{
|
||||
// Premultiplied and non-premultiplied sprites must be cached separately
|
||||
// to cover the case where the same image is requested in both versions.
|
||||
spritesForToken[frameIndex] = spriteCache.GetOrAdd(
|
||||
(filename, frameIndex, premultiplied, adjustFrame),
|
||||
_ =>
|
||||
{
|
||||
var sheetBuilder = SheetBuilders[SheetBuilder.FrameTypeToSheetType(frame.Type)];
|
||||
return sheetBuilder.Add(frame, premultiplied);
|
||||
});
|
||||
|
||||
modData.LoadScreen?.Display();
|
||||
}
|
||||
|
||||
foreach (var sb in SheetBuilders.Values)
|
||||
sb.Current.ReleaseBuffer();
|
||||
@@ -145,18 +148,11 @@ namespace OpenRA.Graphics
|
||||
|
||||
public Sprite[] ResolveSprites(int token)
|
||||
{
|
||||
var resolved = resolvedSprites[token];
|
||||
resolvedSprites.Remove(token);
|
||||
if (missingFiles.TryGetValue(token, out var r))
|
||||
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);
|
||||
if (!resolvedSprites.Remove(token, out var resolved))
|
||||
throw new InvalidOperationException($"{nameof(token)} {token} has either already been resolved, or was never reserved via {nameof(ReserveSprites)}");
|
||||
|
||||
return resolved;
|
||||
}
|
||||
resolvedSprites.TrimExcess();
|
||||
|
||||
public ISpriteFrame[] ResolveFrames(int token)
|
||||
{
|
||||
var resolved = resolvedFrames[token];
|
||||
resolvedFrames.Remove(token);
|
||||
if (missingFiles.TryGetValue(token, out var r))
|
||||
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);
|
||||
|
||||
|
||||
@@ -22,20 +22,30 @@ namespace OpenRA.Graphics
|
||||
/// </summary>
|
||||
public enum SpriteFrameType
|
||||
{
|
||||
// 8 bit index into an external palette
|
||||
/// <summary>
|
||||
/// 8 bit index into an external palette.
|
||||
/// </summary>
|
||||
Indexed8,
|
||||
|
||||
// 32 bit color such as returned by Color.ToArgb() or the bmp file format
|
||||
// (remember that little-endian systems place the little bits in the first byte!)
|
||||
/// <summary>
|
||||
/// 32 bit color such as returned by Color.ToArgb() or the bmp file format
|
||||
/// (remember that little-endian systems place the little bits in the first byte).
|
||||
/// </summary>
|
||||
Bgra32,
|
||||
|
||||
// Like BGRA, but without an alpha channel
|
||||
/// <summary>
|
||||
/// Like BGRA, but without an alpha channel.
|
||||
/// </summary>
|
||||
Bgr24,
|
||||
|
||||
// 32 bit color in big-endian format, like png
|
||||
/// <summary>
|
||||
/// 32 bit color in big-endian format, like png.
|
||||
/// </summary>
|
||||
Rgba32,
|
||||
|
||||
// Like RGBA, but without an alpha channel
|
||||
/// <summary>
|
||||
/// Like RGBA, but without an alpha channel.
|
||||
/// </summary>
|
||||
Rgb24
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
@@ -19,6 +20,7 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
public const int SheetCount = 8;
|
||||
static readonly string[] SheetIndexToTextureName = Exts.MakeArray(SheetCount, i => $"Texture{i}");
|
||||
static readonly int UintSize = Marshal.SizeOf(typeof(uint));
|
||||
|
||||
readonly Renderer renderer;
|
||||
readonly IShader shader;
|
||||
@@ -27,21 +29,21 @@ namespace OpenRA.Graphics
|
||||
readonly Sheet[] sheets = new Sheet[SheetCount];
|
||||
|
||||
BlendMode currentBlend = BlendMode.Alpha;
|
||||
int nv = 0;
|
||||
int ns = 0;
|
||||
int vertexCount = 0;
|
||||
int sheetCount = 0;
|
||||
|
||||
public SpriteRenderer(Renderer renderer, IShader shader)
|
||||
{
|
||||
this.renderer = renderer;
|
||||
this.shader = shader;
|
||||
vertices = renderer.Context.CreateVertices(renderer.TempBufferSize);
|
||||
vertices = renderer.Context.CreateVertices<Vertex>(renderer.TempVertexBufferSize);
|
||||
}
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
if (nv > 0)
|
||||
if (vertexCount > 0)
|
||||
{
|
||||
for (var i = 0; i < ns; i++)
|
||||
for (var i = 0; i < sheetCount; i++)
|
||||
{
|
||||
shader.SetTexture(SheetIndexToTextureName[i], sheets[i].GetTexture());
|
||||
sheets[i] = null;
|
||||
@@ -50,12 +52,11 @@ namespace OpenRA.Graphics
|
||||
renderer.Context.SetBlendMode(currentBlend);
|
||||
shader.PrepareRender();
|
||||
|
||||
// PERF: The renderer may choose to replace vertices with a different temporary buffer.
|
||||
renderer.DrawBatch(ref vertices, nv, PrimitiveType.TriangleList);
|
||||
renderer.DrawQuadBatch(ref vertices, shader, vertexCount);
|
||||
renderer.Context.SetBlendMode(BlendMode.None);
|
||||
|
||||
nv = 0;
|
||||
ns = 0;
|
||||
vertexCount = 0;
|
||||
sheetCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ namespace OpenRA.Graphics
|
||||
{
|
||||
renderer.CurrentBatchRenderer = this;
|
||||
|
||||
if (s.BlendMode != currentBlend || nv + 6 > renderer.TempBufferSize)
|
||||
if (s.BlendMode != currentBlend || vertexCount + 4 > renderer.TempVertexBufferSize)
|
||||
Flush();
|
||||
|
||||
currentBlend = s.BlendMode;
|
||||
@@ -71,7 +72,7 @@ namespace OpenRA.Graphics
|
||||
// Check if the sheet (or secondary data sheet) have already been mapped
|
||||
var sheet = s.Sheet;
|
||||
var sheetIndex = 0;
|
||||
for (; sheetIndex < ns; sheetIndex++)
|
||||
for (; sheetIndex < sheetCount; sheetIndex++)
|
||||
if (sheets[sheetIndex] == sheet)
|
||||
break;
|
||||
|
||||
@@ -80,7 +81,7 @@ namespace OpenRA.Graphics
|
||||
if (ss != null)
|
||||
{
|
||||
var secondarySheet = ss.SecondarySheet;
|
||||
for (; secondarySheetIndex < ns; secondarySheetIndex++)
|
||||
for (; secondarySheetIndex < sheetCount; secondarySheetIndex++)
|
||||
if (sheets[secondarySheetIndex] == secondarySheet)
|
||||
break;
|
||||
|
||||
@@ -99,22 +100,22 @@ namespace OpenRA.Graphics
|
||||
secondarySheetIndex = ss != null && ss.SecondarySheet != sheet ? 1 : 0;
|
||||
}
|
||||
|
||||
if (sheetIndex >= ns)
|
||||
if (sheetIndex >= sheetCount)
|
||||
{
|
||||
sheets[sheetIndex] = sheet;
|
||||
ns++;
|
||||
sheetCount++;
|
||||
}
|
||||
|
||||
if (secondarySheetIndex >= ns && ss != null)
|
||||
if (secondarySheetIndex >= sheetCount && ss != null)
|
||||
{
|
||||
sheets[secondarySheetIndex] = ss.SecondarySheet;
|
||||
ns++;
|
||||
sheetCount++;
|
||||
}
|
||||
|
||||
return new int2(sheetIndex, secondarySheetIndex);
|
||||
}
|
||||
|
||||
static float ResolveTextureIndex(Sprite s, PaletteReference pal)
|
||||
static int ResolveTextureIndex(Sprite s, PaletteReference pal)
|
||||
{
|
||||
if (pal == null)
|
||||
return 0;
|
||||
@@ -128,20 +129,20 @@ namespace OpenRA.Graphics
|
||||
return pal.TextureIndex;
|
||||
}
|
||||
|
||||
internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, in float3 scale, float rotation = 0f)
|
||||
internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 location, in float3 scale, float rotation = 0f)
|
||||
{
|
||||
var samplers = SetRenderStateForSprite(s);
|
||||
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, float3.Ones,
|
||||
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, vertexCount, scale * s.Size, float3.Ones,
|
||||
1f, rotation);
|
||||
nv += 6;
|
||||
vertexCount += 4;
|
||||
}
|
||||
|
||||
internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, float scale, float rotation = 0f)
|
||||
internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 location, float scale, float rotation = 0f)
|
||||
{
|
||||
var samplers = SetRenderStateForSprite(s);
|
||||
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, float3.Ones,
|
||||
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, vertexCount, scale * s.Size, float3.Ones,
|
||||
1f, rotation);
|
||||
nv += 6;
|
||||
vertexCount += 4;
|
||||
}
|
||||
|
||||
public void DrawSprite(Sprite s, PaletteReference pal, in float3 location, float scale = 1f, float rotation = 0f)
|
||||
@@ -149,13 +150,13 @@ namespace OpenRA.Graphics
|
||||
DrawSprite(s, ResolveTextureIndex(s, pal), location, scale, rotation);
|
||||
}
|
||||
|
||||
internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, float scale, in float3 tint, float alpha,
|
||||
internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 location, float scale, in float3 tint, float alpha,
|
||||
float rotation = 0f)
|
||||
{
|
||||
var samplers = SetRenderStateForSprite(s);
|
||||
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, tint, alpha,
|
||||
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, vertexCount, scale * s.Size, tint, alpha,
|
||||
rotation);
|
||||
nv += 6;
|
||||
vertexCount += 4;
|
||||
}
|
||||
|
||||
public void DrawSprite(Sprite s, PaletteReference pal, in float3 location, float scale, in float3 tint, float alpha,
|
||||
@@ -164,14 +165,14 @@ namespace OpenRA.Graphics
|
||||
DrawSprite(s, ResolveTextureIndex(s, pal), location, scale, tint, alpha, rotation);
|
||||
}
|
||||
|
||||
internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 a, in float3 b, in float3 c, in float3 d, in float3 tint, float alpha)
|
||||
internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 a, in float3 b, in float3 c, in float3 d, in float3 tint, float alpha)
|
||||
{
|
||||
var samplers = SetRenderStateForSprite(s);
|
||||
Util.FastCreateQuad(vertices, a, b, c, d, s, samplers, paletteTextureIndex, tint, alpha, nv);
|
||||
nv += 6;
|
||||
Util.FastCreateQuad(vertices, a, b, c, d, s, samplers, paletteTextureIndex, tint, alpha, vertexCount);
|
||||
vertexCount += 4;
|
||||
}
|
||||
|
||||
public void DrawVertexBuffer(IVertexBuffer<Vertex> buffer, int start, int length, PrimitiveType type, IEnumerable<Sheet> sheets, BlendMode blendMode)
|
||||
public void DrawVertexBuffer(IVertexBuffer<Vertex> buffer, IIndexBuffer indices, int start, int length, IEnumerable<Sheet> sheets, BlendMode blendMode)
|
||||
{
|
||||
var i = 0;
|
||||
foreach (var s in sheets)
|
||||
@@ -185,7 +186,7 @@ namespace OpenRA.Graphics
|
||||
|
||||
renderer.Context.SetBlendMode(blendMode);
|
||||
shader.PrepareRender();
|
||||
renderer.DrawBatch(buffer, start, length, type);
|
||||
renderer.DrawQuadBatch(buffer, indices, shader, length, UintSize * start);
|
||||
renderer.Context.SetBlendMode(BlendMode.None);
|
||||
}
|
||||
|
||||
@@ -196,29 +197,32 @@ namespace OpenRA.Graphics
|
||||
}
|
||||
|
||||
// For RGBAColorRenderer
|
||||
internal void DrawRGBAVertices(Vertex[] v, BlendMode blendMode)
|
||||
internal void DrawRGBAQuad(Vertex[] v, BlendMode blendMode)
|
||||
{
|
||||
renderer.CurrentBatchRenderer = this;
|
||||
|
||||
if (currentBlend != blendMode || nv + v.Length > renderer.TempBufferSize)
|
||||
if (currentBlend != blendMode || vertexCount + 4 > renderer.TempVertexBufferSize)
|
||||
Flush();
|
||||
|
||||
currentBlend = blendMode;
|
||||
Array.Copy(v, 0, vertices, nv, v.Length);
|
||||
nv += v.Length;
|
||||
|
||||
Array.Copy(v, 0, vertices, vertexCount, v.Length);
|
||||
vertexCount += 4;
|
||||
}
|
||||
|
||||
public void SetPalette(ITexture palette, ITexture colorShifts)
|
||||
public void SetPalette(HardwarePalette palette)
|
||||
{
|
||||
shader.SetTexture("Palette", palette);
|
||||
shader.SetTexture("ColorShifts", colorShifts);
|
||||
shader.SetTexture("Palette", palette.Texture);
|
||||
shader.SetTexture("ColorShifts", palette.ColorShifts);
|
||||
shader.SetVec("PaletteRows", palette.Height);
|
||||
}
|
||||
|
||||
public void SetViewportParams(Size sheetSize, int downscale, float depthMargin, int2 scroll)
|
||||
{
|
||||
// Calculate the scale (r1) and offset (r2) that convert from OpenRA viewport pixels
|
||||
// to OpenGL normalized device coordinates (NDC). OpenGL expects coordinates to vary from [-1, 1],
|
||||
// so we rescale viewport pixels to the range [0, 2] using r1 then subtract 1 using r2.
|
||||
// OpenGL only renders x and y coordinates inside [-1, 1] range. We project world coordinates
|
||||
// using p1 to values [0, 2] and then subtract by 1 using p2, where p stands for projection. It's
|
||||
// standard practice for shaders to use a projection matrix, but as we project orthographically
|
||||
// we are able to send less data to the GPU.
|
||||
var width = 2f / (downscale * sheetSize.Width);
|
||||
var height = 2f / (downscale * sheetSize.Height);
|
||||
|
||||
@@ -239,8 +243,8 @@ namespace OpenRA.Graphics
|
||||
var depth = depthMargin != 0f ? 2f / (downscale * (sheetSize.Height + depthMargin)) : 0;
|
||||
shader.SetVec("DepthTextureScale", 128 * depth);
|
||||
shader.SetVec("Scroll", scroll.X, scroll.Y, depthMargin != 0f ? scroll.Y : 0);
|
||||
shader.SetVec("r1", width, height, -depth);
|
||||
shader.SetVec("r2", -1, -1, depthMargin != 0f ? 1 : 0);
|
||||
shader.SetVec("p1", width, height, -depth);
|
||||
shader.SetVec("p2", -1, -1, depthMargin != 0f ? 1 : 0);
|
||||
}
|
||||
|
||||
public void SetDepthPreview(bool enabled, float contrast, float offset)
|
||||
|
||||
@@ -12,12 +12,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public sealed class TerrainSpriteLayer : IDisposable
|
||||
{
|
||||
static readonly int[] CornerVertexMap = { 0, 1, 2, 2, 3, 0 };
|
||||
// PERF: we can reuse the IndexBuffer as all layers have the same size.
|
||||
static readonly ConditionalWeakTable<World, IndexBufferRc> IndexBuffers = new();
|
||||
readonly IndexBufferRc indexBufferWrapper;
|
||||
|
||||
public readonly BlendMode BlendMode;
|
||||
|
||||
@@ -28,7 +31,8 @@ namespace OpenRA.Graphics
|
||||
readonly Vertex[] vertices;
|
||||
readonly bool[] ignoreTint;
|
||||
readonly HashSet<int> dirtyRows = new();
|
||||
readonly int rowStride;
|
||||
readonly int indexRowStride;
|
||||
readonly int vertexRowStride;
|
||||
readonly bool restrictToBounds;
|
||||
|
||||
readonly WorldRenderer worldRenderer;
|
||||
@@ -43,19 +47,25 @@ namespace OpenRA.Graphics
|
||||
this.emptySprite = emptySprite;
|
||||
sheets = new Sheet[SpriteRenderer.SheetCount];
|
||||
BlendMode = blendMode;
|
||||
|
||||
map = world.Map;
|
||||
rowStride = 6 * map.MapSize.X;
|
||||
|
||||
vertices = new Vertex[rowStride * map.MapSize.Y];
|
||||
vertexRowStride = 4 * map.MapSize.X;
|
||||
vertices = new Vertex[vertexRowStride * map.MapSize.Y];
|
||||
vertexBuffer = Game.Renderer.Context.CreateVertexBuffer<Vertex>(vertices.Length);
|
||||
|
||||
indexRowStride = 6 * map.MapSize.X;
|
||||
lock (IndexBuffers)
|
||||
{
|
||||
indexBufferWrapper = IndexBuffers.GetValue(world, world => new IndexBufferRc(world));
|
||||
indexBufferWrapper.AddRef();
|
||||
}
|
||||
|
||||
palettes = new PaletteReference[map.MapSize.X * map.MapSize.Y];
|
||||
vertexBuffer = Game.Renderer.Context.CreateVertexBuffer(vertices.Length);
|
||||
|
||||
wr.PaletteInvalidated += UpdatePaletteIndices;
|
||||
|
||||
if (wr.TerrainLighting != null)
|
||||
{
|
||||
ignoreTint = new bool[rowStride * map.MapSize.Y];
|
||||
ignoreTint = new bool[vertexRowStride * map.MapSize.Y];
|
||||
wr.TerrainLighting.CellChanged += UpdateTint;
|
||||
}
|
||||
}
|
||||
@@ -65,8 +75,9 @@ namespace OpenRA.Graphics
|
||||
for (var i = 0; i < vertices.Length; i++)
|
||||
{
|
||||
var v = vertices[i];
|
||||
var p = palettes[i / 6]?.TextureIndex ?? 0;
|
||||
vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, p, v.C, v.R, v.G, v.B, v.A);
|
||||
var p = palettes[i / 4]?.TextureIndex ?? 0;
|
||||
var c = (uint)((p & 0xFFFF) << 16) | (v.C & 0xFFFF);
|
||||
vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, c, v.R, v.G, v.B, v.A);
|
||||
}
|
||||
|
||||
for (var row = 0; row < map.MapSize.Y; row++)
|
||||
@@ -97,13 +108,13 @@ namespace OpenRA.Graphics
|
||||
|
||||
void UpdateTint(MPos uv)
|
||||
{
|
||||
var offset = rowStride * uv.V + 6 * uv.U;
|
||||
var offset = vertexRowStride * uv.V + 4 * uv.U;
|
||||
if (ignoreTint[offset])
|
||||
{
|
||||
for (var i = 0; i < 6; i++)
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
var v = vertices[offset + i];
|
||||
vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.P, v.C, v.A * float3.Ones, v.A);
|
||||
vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.C, v.A * float3.Ones, v.A);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -125,10 +136,10 @@ namespace OpenRA.Graphics
|
||||
|
||||
// Apply tint directly to the underlying vertices
|
||||
// This saves us from having to re-query the sprite information, which has not changed
|
||||
for (var i = 0; i < 6; i++)
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
var v = vertices[offset + i];
|
||||
vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.P, v.C, v.A * weights[CornerVertexMap[i]], v.A);
|
||||
vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.C, v.A * weights[i], v.A);
|
||||
}
|
||||
|
||||
dirtyRows.Add(uv.V);
|
||||
@@ -180,7 +191,7 @@ namespace OpenRA.Graphics
|
||||
if (!map.Tiles.Contains(uv))
|
||||
return;
|
||||
|
||||
var offset = rowStride * uv.V + 6 * uv.U;
|
||||
var offset = vertexRowStride * uv.V + 4 * uv.U;
|
||||
Util.FastCreateQuad(vertices, pos, sprite, samplers, palette?.TextureIndex ?? 0, offset, scale * sprite.Size, alpha * float3.Ones, alpha);
|
||||
palettes[uv.V * map.MapSize.X + uv.U] = palette;
|
||||
|
||||
@@ -209,13 +220,13 @@ namespace OpenRA.Graphics
|
||||
if (!dirtyRows.Remove(row))
|
||||
continue;
|
||||
|
||||
var rowOffset = rowStride * row;
|
||||
vertexBuffer.SetData(vertices, rowOffset, rowOffset, rowStride);
|
||||
var rowOffset = vertexRowStride * row;
|
||||
vertexBuffer.SetData(vertices, rowOffset, rowOffset, vertexRowStride);
|
||||
}
|
||||
|
||||
Game.Renderer.WorldSpriteRenderer.DrawVertexBuffer(
|
||||
vertexBuffer, rowStride * firstRow, rowStride * (lastRow - firstRow),
|
||||
PrimitiveType.TriangleList, sheets, BlendMode);
|
||||
vertexBuffer, indexBufferWrapper.Buffer, indexRowStride * firstRow,
|
||||
indexRowStride * (lastRow - firstRow), sheets, BlendMode);
|
||||
|
||||
Game.Renderer.Flush();
|
||||
}
|
||||
@@ -227,6 +238,29 @@ namespace OpenRA.Graphics
|
||||
worldRenderer.TerrainLighting.CellChanged -= UpdateTint;
|
||||
|
||||
vertexBuffer.Dispose();
|
||||
|
||||
lock (IndexBuffers)
|
||||
indexBufferWrapper.Dispose();
|
||||
}
|
||||
|
||||
sealed class IndexBufferRc : IDisposable
|
||||
{
|
||||
public IIndexBuffer Buffer;
|
||||
int count;
|
||||
|
||||
public IndexBufferRc(World world)
|
||||
{
|
||||
Buffer = Game.Renderer.Context.CreateIndexBuffer(Util.CreateQuadIndices(world.Map.MapSize.X * world.Map.MapSize.Y));
|
||||
}
|
||||
|
||||
public void AddRef() { count++; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
count--;
|
||||
if (count == 0)
|
||||
Buffer.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using OpenRA.FileFormats;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
@@ -20,7 +21,17 @@ namespace OpenRA.Graphics
|
||||
// yes, our channel order is nuts.
|
||||
static readonly int[] ChannelMasks = { 2, 1, 0, 3 };
|
||||
|
||||
public static void FastCreateQuad(Vertex[] vertices, in float3 o, Sprite r, int2 samplers, float paletteTextureIndex, int nv,
|
||||
public static uint[] CreateQuadIndices(int quads)
|
||||
{
|
||||
var indices = new uint[quads * 6];
|
||||
ReadOnlySpan<uint> cornerVertexMap = stackalloc uint[] { 0, 1, 2, 2, 3, 0 };
|
||||
for (var i = 0; i < indices.Length; i++)
|
||||
indices[i] = cornerVertexMap[i % 6] + (uint)(4 * (i / 6));
|
||||
|
||||
return indices;
|
||||
}
|
||||
|
||||
public static void FastCreateQuad(Vertex[] vertices, in float3 o, Sprite r, int2 samplers, int paletteTextureIndex, int nv,
|
||||
in float3 size, in float3 tint, float alpha, float rotation = 0f)
|
||||
{
|
||||
float3 a, b, c, d;
|
||||
@@ -62,7 +73,7 @@ namespace OpenRA.Graphics
|
||||
|
||||
public static void FastCreateQuad(Vertex[] vertices,
|
||||
in float3 a, in float3 b, in float3 c, in float3 d,
|
||||
Sprite r, int2 samplers, float paletteTextureIndex,
|
||||
Sprite r, int2 samplers, int paletteTextureIndex,
|
||||
in float3 tint, float alpha, int nv)
|
||||
{
|
||||
float sl = 0;
|
||||
@@ -84,76 +95,33 @@ namespace OpenRA.Graphics
|
||||
attribC |= samplers.Y << 9;
|
||||
}
|
||||
|
||||
var fAttribC = (float)attribC;
|
||||
vertices[nv] = new Vertex(a, r.Left, r.Top, sl, st, paletteTextureIndex, fAttribC, tint, alpha);
|
||||
vertices[nv + 1] = new Vertex(b, r.Right, r.Top, sr, st, paletteTextureIndex, fAttribC, tint, alpha);
|
||||
vertices[nv + 2] = new Vertex(c, r.Right, r.Bottom, sr, sb, paletteTextureIndex, fAttribC, tint, alpha);
|
||||
vertices[nv + 3] = new Vertex(c, r.Right, r.Bottom, sr, sb, paletteTextureIndex, fAttribC, tint, alpha);
|
||||
vertices[nv + 4] = new Vertex(d, r.Left, r.Bottom, sl, sb, paletteTextureIndex, fAttribC, tint, alpha);
|
||||
vertices[nv + 5] = new Vertex(a, r.Left, r.Top, sl, st, paletteTextureIndex, fAttribC, tint, alpha);
|
||||
attribC |= (paletteTextureIndex & 0xFFFF) << 16;
|
||||
|
||||
var uAttribC = (uint)attribC;
|
||||
vertices[nv] = new Vertex(a, r.Left, r.Top, sl, st, uAttribC, tint, alpha);
|
||||
vertices[nv + 1] = new Vertex(b, r.Right, r.Top, sr, st, uAttribC, tint, alpha);
|
||||
vertices[nv + 2] = new Vertex(c, r.Right, r.Bottom, sr, sb, uAttribC, tint, alpha);
|
||||
vertices[nv + 3] = new Vertex(d, r.Left, r.Bottom, sl, sb, uAttribC, tint, alpha);
|
||||
}
|
||||
|
||||
public static void FastCopyIntoChannel(Sprite dest, byte[] src, SpriteFrameType srcType)
|
||||
public static void FastCopyIntoChannel(Sprite dest, byte[] src, SpriteFrameType srcType, bool premultiplied = false)
|
||||
{
|
||||
var destData = dest.Sheet.GetData();
|
||||
var stride = dest.Sheet.Size.Width;
|
||||
var x = dest.Bounds.Left;
|
||||
var y = dest.Bounds.Top;
|
||||
var width = dest.Bounds.Width;
|
||||
var height = dest.Bounds.Height;
|
||||
|
||||
if (dest.Channel == TextureChannel.RGBA)
|
||||
{
|
||||
var destStride = dest.Sheet.Size.Width;
|
||||
unsafe
|
||||
{
|
||||
// Cast the data to an int array so we can copy the src data directly
|
||||
fixed (byte* bd = &destData[0])
|
||||
{
|
||||
var data = (int*)bd;
|
||||
var x = dest.Bounds.Left;
|
||||
var y = dest.Bounds.Top;
|
||||
|
||||
var k = 0;
|
||||
for (var j = 0; j < height; j++)
|
||||
{
|
||||
for (var i = 0; i < width; i++)
|
||||
{
|
||||
byte r, g, b, a;
|
||||
switch (srcType)
|
||||
{
|
||||
case SpriteFrameType.Bgra32:
|
||||
case SpriteFrameType.Bgr24:
|
||||
{
|
||||
b = src[k++];
|
||||
g = src[k++];
|
||||
r = src[k++];
|
||||
a = srcType == SpriteFrameType.Bgra32 ? src[k++] : (byte)255;
|
||||
break;
|
||||
}
|
||||
|
||||
case SpriteFrameType.Rgba32:
|
||||
case SpriteFrameType.Rgb24:
|
||||
{
|
||||
r = src[k++];
|
||||
g = src[k++];
|
||||
b = src[k++];
|
||||
a = srcType == SpriteFrameType.Rgba32 ? src[k++] : (byte)255;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown SpriteFrameType {srcType}");
|
||||
}
|
||||
|
||||
var cc = Color.FromArgb(a, r, g, b);
|
||||
data[(y + j) * destStride + x + i] = PremultiplyAlpha(cc).ToArgb();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CopyIntoRgba(src, srcType, premultiplied, destData, x, y, width, height, stride);
|
||||
}
|
||||
else
|
||||
{
|
||||
var destStride = dest.Sheet.Size.Width * 4;
|
||||
var destOffset = destStride * dest.Bounds.Top + dest.Bounds.Left * 4 + ChannelMasks[(int)dest.Channel];
|
||||
// Copy into single channel of destination.
|
||||
var destStride = stride * 4;
|
||||
var destOffset = destStride * y + x * 4 + ChannelMasks[(int)dest.Channel];
|
||||
var destSkip = destStride - 4 * width;
|
||||
|
||||
var srcOffset = 0;
|
||||
@@ -170,56 +138,119 @@ namespace OpenRA.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
static void CopyIntoRgba(
|
||||
byte[] src, SpriteFrameType srcType, bool premultiplied, byte[] dest, int x, int y, int width, int height, int stride)
|
||||
{
|
||||
var si = 0;
|
||||
var di = y * stride + x;
|
||||
var d = MemoryMarshal.Cast<byte, uint>(dest);
|
||||
|
||||
// SpriteFrameType.Brga32 is a common source format, and it matches the destination format.
|
||||
// Provide a fast past that just performs memory copies.
|
||||
if (srcType == SpriteFrameType.Bgra32)
|
||||
{
|
||||
var s = MemoryMarshal.Cast<byte, uint>(src);
|
||||
for (var h = 0; h < height; h++)
|
||||
{
|
||||
s[si..(si + width)].CopyTo(d[di..(di + width)]);
|
||||
|
||||
if (!premultiplied)
|
||||
{
|
||||
for (var w = 0; w < width; w++)
|
||||
{
|
||||
d[di] = PremultiplyAlpha(Color.FromArgb(d[di])).ToArgb();
|
||||
di++;
|
||||
}
|
||||
|
||||
di -= width;
|
||||
}
|
||||
|
||||
si += width;
|
||||
di += stride;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (var h = 0; h < height; h++)
|
||||
{
|
||||
for (var w = 0; w < width; w++)
|
||||
{
|
||||
byte r, g, b, a;
|
||||
switch (srcType)
|
||||
{
|
||||
case SpriteFrameType.Bgra32:
|
||||
case SpriteFrameType.Bgr24:
|
||||
b = src[si++];
|
||||
g = src[si++];
|
||||
r = src[si++];
|
||||
a = srcType == SpriteFrameType.Bgra32 ? src[si++] : byte.MaxValue;
|
||||
break;
|
||||
|
||||
case SpriteFrameType.Rgba32:
|
||||
case SpriteFrameType.Rgb24:
|
||||
r = src[si++];
|
||||
g = src[si++];
|
||||
b = src[si++];
|
||||
a = srcType == SpriteFrameType.Rgba32 ? src[si++] : byte.MaxValue;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown SpriteFrameType {srcType}");
|
||||
}
|
||||
|
||||
var c = Color.FromArgb(a, r, g, b);
|
||||
if (!premultiplied)
|
||||
c = PremultiplyAlpha(c);
|
||||
d[di++] = c.ToArgb();
|
||||
}
|
||||
|
||||
di += stride - width;
|
||||
}
|
||||
}
|
||||
|
||||
public static void FastCopyIntoSprite(Sprite dest, Png src)
|
||||
{
|
||||
var destData = dest.Sheet.GetData();
|
||||
var destStride = dest.Sheet.Size.Width;
|
||||
var stride = dest.Sheet.Size.Width;
|
||||
var x = dest.Bounds.Left;
|
||||
var y = dest.Bounds.Top;
|
||||
var width = dest.Bounds.Width;
|
||||
var height = dest.Bounds.Height;
|
||||
|
||||
unsafe
|
||||
var si = 0;
|
||||
var di = y * stride + x;
|
||||
var d = MemoryMarshal.Cast<byte, uint>(destData);
|
||||
|
||||
for (var h = 0; h < height; h++)
|
||||
{
|
||||
// Cast the data to an int array so we can copy the src data directly
|
||||
fixed (byte* bd = &destData[0])
|
||||
for (var w = 0; w < width; w++)
|
||||
{
|
||||
var data = (int*)bd;
|
||||
var x = dest.Bounds.Left;
|
||||
var y = dest.Bounds.Top;
|
||||
|
||||
var k = 0;
|
||||
for (var j = 0; j < height; j++)
|
||||
Color c;
|
||||
switch (src.Type)
|
||||
{
|
||||
for (var i = 0; i < width; i++)
|
||||
{
|
||||
Color cc;
|
||||
switch (src.Type)
|
||||
{
|
||||
case SpriteFrameType.Indexed8:
|
||||
{
|
||||
cc = src.Palette[src.Data[k++]];
|
||||
break;
|
||||
}
|
||||
case SpriteFrameType.Indexed8:
|
||||
c = src.Palette[src.Data[si++]];
|
||||
break;
|
||||
|
||||
case SpriteFrameType.Rgba32:
|
||||
case SpriteFrameType.Rgb24:
|
||||
{
|
||||
var r = src.Data[k++];
|
||||
var g = src.Data[k++];
|
||||
var b = src.Data[k++];
|
||||
var a = src.Type == SpriteFrameType.Rgba32 ? src.Data[k++] : (byte)255;
|
||||
cc = Color.FromArgb(a, r, g, b);
|
||||
break;
|
||||
}
|
||||
case SpriteFrameType.Rgba32:
|
||||
case SpriteFrameType.Rgb24:
|
||||
var r = src.Data[si++];
|
||||
var g = src.Data[si++];
|
||||
var b = src.Data[si++];
|
||||
var a = src.Type == SpriteFrameType.Rgba32 ? src.Data[si++] : byte.MaxValue;
|
||||
c = Color.FromArgb(a, r, g, b);
|
||||
break;
|
||||
|
||||
// Pngs don't support BGR[A], so no need to include them here
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown SpriteFrameType {src.Type}");
|
||||
}
|
||||
|
||||
data[(y + j) * destStride + x + i] = PremultiplyAlpha(cc).ToArgb();
|
||||
}
|
||||
// PNGs don't support BGR[A], so no need to include them here
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown SpriteFrameType {src.Type}");
|
||||
}
|
||||
|
||||
d[di++] = PremultiplyAlpha(c).ToArgb();
|
||||
}
|
||||
|
||||
di += stride - width;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,239 +336,5 @@ namespace OpenRA.Graphics
|
||||
(int)((byte)(t * a2 * c2.G + 0.5f) + (1 - t) * (byte)(a1 * c1.G + 0.5f)),
|
||||
(int)((byte)(t * a2 * c2.B + 0.5f) + (1 - t) * (byte)(a1 * c1.B + 0.5f))));
|
||||
}
|
||||
|
||||
public static float[] IdentityMatrix()
|
||||
{
|
||||
return Exts.MakeArray(16, j => (j % 5 == 0) ? 1.0f : 0);
|
||||
}
|
||||
|
||||
public static float[] ScaleMatrix(float sx, float sy, float sz)
|
||||
{
|
||||
var mtx = IdentityMatrix();
|
||||
mtx[0] = sx;
|
||||
mtx[5] = sy;
|
||||
mtx[10] = sz;
|
||||
return mtx;
|
||||
}
|
||||
|
||||
public static float[] TranslationMatrix(float x, float y, float z)
|
||||
{
|
||||
var mtx = IdentityMatrix();
|
||||
mtx[12] = x;
|
||||
mtx[13] = y;
|
||||
mtx[14] = z;
|
||||
return mtx;
|
||||
}
|
||||
|
||||
public static float[] MatrixMultiply(float[] lhs, float[] rhs)
|
||||
{
|
||||
var mtx = new float[16];
|
||||
for (var i = 0; i < 4; i++)
|
||||
for (var j = 0; j < 4; j++)
|
||||
{
|
||||
mtx[4 * i + j] = 0;
|
||||
for (var k = 0; k < 4; k++)
|
||||
mtx[4 * i + j] += lhs[4 * k + j] * rhs[4 * i + k];
|
||||
}
|
||||
|
||||
return mtx;
|
||||
}
|
||||
|
||||
public static float[] MatrixVectorMultiply(float[] mtx, float[] vec)
|
||||
{
|
||||
var ret = new float[4];
|
||||
for (var j = 0; j < 4; j++)
|
||||
{
|
||||
ret[j] = 0;
|
||||
for (var k = 0; k < 4; k++)
|
||||
ret[j] += mtx[4 * k + j] * vec[k];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static float[] MatrixInverse(float[] m)
|
||||
{
|
||||
var mtx = new float[16];
|
||||
|
||||
mtx[0] = m[5] * m[10] * m[15] -
|
||||
m[5] * m[11] * m[14] -
|
||||
m[9] * m[6] * m[15] +
|
||||
m[9] * m[7] * m[14] +
|
||||
m[13] * m[6] * m[11] -
|
||||
m[13] * m[7] * m[10];
|
||||
|
||||
mtx[4] = -m[4] * m[10] * m[15] +
|
||||
m[4] * m[11] * m[14] +
|
||||
m[8] * m[6] * m[15] -
|
||||
m[8] * m[7] * m[14] -
|
||||
m[12] * m[6] * m[11] +
|
||||
m[12] * m[7] * m[10];
|
||||
|
||||
mtx[8] = m[4] * m[9] * m[15] -
|
||||
m[4] * m[11] * m[13] -
|
||||
m[8] * m[5] * m[15] +
|
||||
m[8] * m[7] * m[13] +
|
||||
m[12] * m[5] * m[11] -
|
||||
m[12] * m[7] * m[9];
|
||||
|
||||
mtx[12] = -m[4] * m[9] * m[14] +
|
||||
m[4] * m[10] * m[13] +
|
||||
m[8] * m[5] * m[14] -
|
||||
m[8] * m[6] * m[13] -
|
||||
m[12] * m[5] * m[10] +
|
||||
m[12] * m[6] * m[9];
|
||||
|
||||
mtx[1] = -m[1] * m[10] * m[15] +
|
||||
m[1] * m[11] * m[14] +
|
||||
m[9] * m[2] * m[15] -
|
||||
m[9] * m[3] * m[14] -
|
||||
m[13] * m[2] * m[11] +
|
||||
m[13] * m[3] * m[10];
|
||||
|
||||
mtx[5] = m[0] * m[10] * m[15] -
|
||||
m[0] * m[11] * m[14] -
|
||||
m[8] * m[2] * m[15] +
|
||||
m[8] * m[3] * m[14] +
|
||||
m[12] * m[2] * m[11] -
|
||||
m[12] * m[3] * m[10];
|
||||
|
||||
mtx[9] = -m[0] * m[9] * m[15] +
|
||||
m[0] * m[11] * m[13] +
|
||||
m[8] * m[1] * m[15] -
|
||||
m[8] * m[3] * m[13] -
|
||||
m[12] * m[1] * m[11] +
|
||||
m[12] * m[3] * m[9];
|
||||
|
||||
mtx[13] = m[0] * m[9] * m[14] -
|
||||
m[0] * m[10] * m[13] -
|
||||
m[8] * m[1] * m[14] +
|
||||
m[8] * m[2] * m[13] +
|
||||
m[12] * m[1] * m[10] -
|
||||
m[12] * m[2] * m[9];
|
||||
|
||||
mtx[2] = m[1] * m[6] * m[15] -
|
||||
m[1] * m[7] * m[14] -
|
||||
m[5] * m[2] * m[15] +
|
||||
m[5] * m[3] * m[14] +
|
||||
m[13] * m[2] * m[7] -
|
||||
m[13] * m[3] * m[6];
|
||||
|
||||
mtx[6] = -m[0] * m[6] * m[15] +
|
||||
m[0] * m[7] * m[14] +
|
||||
m[4] * m[2] * m[15] -
|
||||
m[4] * m[3] * m[14] -
|
||||
m[12] * m[2] * m[7] +
|
||||
m[12] * m[3] * m[6];
|
||||
|
||||
mtx[10] = m[0] * m[5] * m[15] -
|
||||
m[0] * m[7] * m[13] -
|
||||
m[4] * m[1] * m[15] +
|
||||
m[4] * m[3] * m[13] +
|
||||
m[12] * m[1] * m[7] -
|
||||
m[12] * m[3] * m[5];
|
||||
|
||||
mtx[14] = -m[0] * m[5] * m[14] +
|
||||
m[0] * m[6] * m[13] +
|
||||
m[4] * m[1] * m[14] -
|
||||
m[4] * m[2] * m[13] -
|
||||
m[12] * m[1] * m[6] +
|
||||
m[12] * m[2] * m[5];
|
||||
|
||||
mtx[3] = -m[1] * m[6] * m[11] +
|
||||
m[1] * m[7] * m[10] +
|
||||
m[5] * m[2] * m[11] -
|
||||
m[5] * m[3] * m[10] -
|
||||
m[9] * m[2] * m[7] +
|
||||
m[9] * m[3] * m[6];
|
||||
|
||||
mtx[7] = m[0] * m[6] * m[11] -
|
||||
m[0] * m[7] * m[10] -
|
||||
m[4] * m[2] * m[11] +
|
||||
m[4] * m[3] * m[10] +
|
||||
m[8] * m[2] * m[7] -
|
||||
m[8] * m[3] * m[6];
|
||||
|
||||
mtx[11] = -m[0] * m[5] * m[11] +
|
||||
m[0] * m[7] * m[9] +
|
||||
m[4] * m[1] * m[11] -
|
||||
m[4] * m[3] * m[9] -
|
||||
m[8] * m[1] * m[7] +
|
||||
m[8] * m[3] * m[5];
|
||||
|
||||
mtx[15] = m[0] * m[5] * m[10] -
|
||||
m[0] * m[6] * m[9] -
|
||||
m[4] * m[1] * m[10] +
|
||||
m[4] * m[2] * m[9] +
|
||||
m[8] * m[1] * m[6] -
|
||||
m[8] * m[2] * m[5];
|
||||
|
||||
var det = m[0] * mtx[0] + m[1] * mtx[4] + m[2] * mtx[8] + m[3] * mtx[12];
|
||||
if (det == 0)
|
||||
return null;
|
||||
|
||||
for (var i = 0; i < 16; i++)
|
||||
mtx[i] *= 1 / det;
|
||||
|
||||
return mtx;
|
||||
}
|
||||
|
||||
public static float[] MakeFloatMatrix(Int32Matrix4x4 imtx)
|
||||
{
|
||||
var multipler = 1f / imtx.M44;
|
||||
return new[]
|
||||
{
|
||||
imtx.M11 * multipler,
|
||||
imtx.M12 * multipler,
|
||||
imtx.M13 * multipler,
|
||||
imtx.M14 * multipler,
|
||||
|
||||
imtx.M21 * multipler,
|
||||
imtx.M22 * multipler,
|
||||
imtx.M23 * multipler,
|
||||
imtx.M24 * multipler,
|
||||
|
||||
imtx.M31 * multipler,
|
||||
imtx.M32 * multipler,
|
||||
imtx.M33 * multipler,
|
||||
imtx.M34 * multipler,
|
||||
|
||||
imtx.M41 * multipler,
|
||||
imtx.M42 * multipler,
|
||||
imtx.M43 * multipler,
|
||||
imtx.M44 * multipler,
|
||||
};
|
||||
}
|
||||
|
||||
public static float[] MatrixAABBMultiply(float[] mtx, float[] bounds)
|
||||
{
|
||||
// Corner offsets
|
||||
var ix = new uint[] { 0, 0, 0, 0, 3, 3, 3, 3 };
|
||||
var iy = new uint[] { 1, 1, 4, 4, 1, 1, 4, 4 };
|
||||
var iz = new uint[] { 2, 5, 2, 5, 2, 5, 2, 5 };
|
||||
|
||||
// Vectors to opposing corner
|
||||
var ret = new[]
|
||||
{
|
||||
float.MaxValue, float.MaxValue, float.MaxValue,
|
||||
float.MinValue, float.MinValue, float.MinValue
|
||||
};
|
||||
|
||||
// Transform vectors and find new bounding box
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
var vec = new[] { bounds[ix[i]], bounds[iy[i]], bounds[iz[i]], 1 };
|
||||
var tvec = MatrixVectorMultiply(mtx, vec);
|
||||
|
||||
ret[0] = Math.Min(ret[0], tvec[0] / tvec[3]);
|
||||
ret[1] = Math.Min(ret[1], tvec[1] / tvec[3]);
|
||||
ret[2] = Math.Min(ret[2], tvec[2] / tvec[3]);
|
||||
ret[3] = Math.Max(ret[3], tvec[0] / tvec[3]);
|
||||
ret[4] = Math.Max(ret[4], tvec[1] / tvec[3]);
|
||||
ret[5] = Math.Max(ret[5], tvec[2] / tvec[3]);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,27 +23,42 @@ namespace OpenRA.Graphics
|
||||
public readonly float S, T, U, V;
|
||||
|
||||
// Palette and channel flags
|
||||
public readonly float P, C;
|
||||
public readonly uint C;
|
||||
|
||||
// Color tint
|
||||
public readonly float R, G, B, A;
|
||||
|
||||
public Vertex(in float3 xyz, float s, float t, float u, float v, float p, float c)
|
||||
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, p, c, float3.Ones, 1f) { }
|
||||
public Vertex(in float3 xyz, float s, float t, float u, float v, uint c)
|
||||
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, c, float3.Ones, 1f) { }
|
||||
|
||||
public Vertex(in float3 xyz, float s, float t, float u, float v, float p, float c, in float3 tint, float a)
|
||||
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, p, c, tint.X, tint.Y, tint.Z, a) { }
|
||||
public Vertex(in float3 xyz, float s, float t, float u, float v, uint c, in float3 tint, float a)
|
||||
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, c, tint.X, tint.Y, tint.Z, a) { }
|
||||
|
||||
public Vertex(float x, float y, float z, float s, float t, float u, float v, float p, float c, in float3 tint, float a)
|
||||
: this(x, y, z, s, t, u, v, p, c, tint.X, tint.Y, tint.Z, a) { }
|
||||
public Vertex(float x, float y, float z, float s, float t, float u, float v, uint c, in float3 tint, float a)
|
||||
: this(x, y, z, s, t, u, v, c, tint.X, tint.Y, tint.Z, a) { }
|
||||
|
||||
public Vertex(float x, float y, float z, float s, float t, float u, float v, float p, float c, float r, float g, float b, float a)
|
||||
public Vertex(float x, float y, float z, float s, float t, float u, float v, uint c, float r, float g, float b, float a)
|
||||
{
|
||||
X = x; Y = y; Z = z;
|
||||
S = s; T = t;
|
||||
U = u; V = v;
|
||||
P = p; C = c;
|
||||
C = c;
|
||||
R = r; G = g; B = b; A = a;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CombinedShaderBindings : ShaderBindings
|
||||
{
|
||||
public CombinedShaderBindings()
|
||||
: base("combined")
|
||||
{ }
|
||||
|
||||
public override ShaderVertexAttribute[] Attributes { get; } = new[]
|
||||
{
|
||||
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
|
||||
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
|
||||
new ShaderVertexAttribute("aVertexAttributes", ShaderVertexAttributeType.UInt, 1, 28),
|
||||
new ShaderVertexAttribute("aVertexTint", ShaderVertexAttributeType.Float, 4, 32)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,11 +282,23 @@ namespace OpenRA.Graphics
|
||||
IEnumerable<MPos> CandidateMouseoverCells(int2 world)
|
||||
{
|
||||
var map = worldRenderer.World.Map;
|
||||
var tileScale = map.Grid.TileScale / 2;
|
||||
var minPos = worldRenderer.ProjectedPosition(world);
|
||||
|
||||
// Find all the cells that could potentially have been clicked
|
||||
var a = map.CellContaining(minPos - new WVec(1024, 0, 0)).ToMPos(map.Grid.Type);
|
||||
var b = map.CellContaining(minPos + new WVec(512, 512 * map.Grid.MaximumTerrainHeight, 0)).ToMPos(map.Grid.Type);
|
||||
// Find all the cells that could potentially have been clicked.
|
||||
MPos a;
|
||||
MPos b;
|
||||
if (map.Grid.Type == MapGridType.RectangularIsometric)
|
||||
{
|
||||
// TODO: this generates too many cells.
|
||||
a = map.CellContaining(minPos - new WVec(tileScale, 0, 0)).ToMPos(map.Grid.Type);
|
||||
b = map.CellContaining(minPos + new WVec(tileScale, tileScale * map.Grid.MaximumTerrainHeight, 0)).ToMPos(map.Grid.Type);
|
||||
}
|
||||
else
|
||||
{
|
||||
a = map.CellContaining(minPos).ToMPos(map.Grid.Type);
|
||||
b = map.CellContaining(minPos + new WVec(0, tileScale * map.Grid.MaximumTerrainHeight, 0)).ToMPos(map.Grid.Type);
|
||||
}
|
||||
|
||||
for (var v = b.V; v >= a.V; v--)
|
||||
for (var u = b.U; u >= a.U; u--)
|
||||
@@ -299,10 +311,13 @@ namespace OpenRA.Graphics
|
||||
|
||||
public void Center(IEnumerable<Actor> actors)
|
||||
{
|
||||
if (!actors.Any())
|
||||
var actorsCollection = actors as IReadOnlyCollection<Actor>;
|
||||
actorsCollection ??= actors.ToList();
|
||||
|
||||
if (actorsCollection.Count == 0)
|
||||
return;
|
||||
|
||||
Center(actors.Select(a => a.CenterPosition).Average());
|
||||
Center(actorsCollection.Select(a => a.CenterPosition).Average());
|
||||
}
|
||||
|
||||
public void Center(WPos pos)
|
||||
|
||||
@@ -44,6 +44,8 @@ namespace OpenRA.Graphics
|
||||
readonly List<IFinalizedRenderable> preparedAnnotationRenderables = new();
|
||||
|
||||
readonly List<IRenderable> renderablesBuffer = new();
|
||||
readonly IRenderer[] renderers;
|
||||
readonly IRenderPostProcessPass[] postProcessPasses;
|
||||
|
||||
internal WorldRenderer(ModData modData, World world)
|
||||
{
|
||||
@@ -66,9 +68,24 @@ namespace OpenRA.Graphics
|
||||
palette.Initialize();
|
||||
|
||||
TerrainLighting = world.WorldActor.TraitOrDefault<ITerrainLighting>();
|
||||
renderers = world.WorldActor.TraitsImplementing<IRenderer>().ToArray();
|
||||
terrainRenderer = world.WorldActor.TraitOrDefault<IRenderTerrain>();
|
||||
|
||||
debugVis = Exts.Lazy(() => world.WorldActor.TraitOrDefault<DebugVisualizations>());
|
||||
|
||||
postProcessPasses = world.WorldActor.TraitsImplementing<IRenderPostProcessPass>().ToArray();
|
||||
}
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
foreach (var r in renderers)
|
||||
r.BeginFrame();
|
||||
}
|
||||
|
||||
public void EndFrame()
|
||||
{
|
||||
foreach (var r in renderers)
|
||||
r.EndFrame();
|
||||
}
|
||||
|
||||
public void UpdatePalettesForPlayer(string internalName, Color color, bool replaceExisting)
|
||||
@@ -270,6 +287,8 @@ namespace OpenRA.Graphics
|
||||
if (enableDepthBuffer)
|
||||
Game.Renderer.ClearDepthBuffer();
|
||||
|
||||
ApplyPostProcessing(PostProcessPassType.AfterActors);
|
||||
|
||||
World.ApplyToActorsWithTrait<IRenderAboveWorld>((actor, trait) =>
|
||||
{
|
||||
if (actor.IsInWorld && !actor.Disposed)
|
||||
@@ -279,6 +298,8 @@ namespace OpenRA.Graphics
|
||||
if (enableDepthBuffer)
|
||||
Game.Renderer.ClearDepthBuffer();
|
||||
|
||||
ApplyPostProcessing(PostProcessPassType.AfterWorld);
|
||||
|
||||
World.ApplyToActorsWithTrait<IRenderShroud>((actor, trait) => trait.RenderShroud(this));
|
||||
|
||||
if (enableDepthBuffer)
|
||||
@@ -292,9 +313,23 @@ namespace OpenRA.Graphics
|
||||
foreach (var r in g)
|
||||
r.Render(this);
|
||||
|
||||
ApplyPostProcessing(PostProcessPassType.AfterShroud);
|
||||
|
||||
Game.Renderer.Flush();
|
||||
}
|
||||
|
||||
void ApplyPostProcessing(PostProcessPassType type)
|
||||
{
|
||||
foreach (var pass in postProcessPasses)
|
||||
{
|
||||
if (pass.Type != type || !pass.Enabled)
|
||||
continue;
|
||||
|
||||
Game.Renderer.Flush();
|
||||
pass.Draw(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void DrawAnnotations()
|
||||
{
|
||||
Game.Renderer.EnableAntialiasingFilter();
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
@@ -31,29 +30,26 @@ namespace OpenRA
|
||||
if (!string.IsNullOrEmpty(node.Value))
|
||||
Default = FieldLoader.GetValue<Hotkey>("value", node.Value);
|
||||
|
||||
var descriptionNode = node.Nodes.FirstOrDefault(n => n.Key == "Description");
|
||||
if (descriptionNode != null)
|
||||
Description = descriptionNode.Value.Value;
|
||||
var nodeDict = node.ToDictionary();
|
||||
|
||||
var typesNode = node.Nodes.FirstOrDefault(n => n.Key == "Types");
|
||||
if (typesNode != null)
|
||||
Types = FieldLoader.GetValue<HashSet<string>>("Types", typesNode.Value.Value);
|
||||
if (nodeDict.TryGetValue("Description", out var descriptionYaml))
|
||||
Description = descriptionYaml.Value;
|
||||
|
||||
var contextsNode = node.Nodes.FirstOrDefault(n => n.Key == "Contexts");
|
||||
if (contextsNode != null)
|
||||
Contexts = FieldLoader.GetValue<HashSet<string>>("Contexts", contextsNode.Value.Value);
|
||||
if (nodeDict.TryGetValue("Types", out var typesYaml))
|
||||
Types = FieldLoader.GetValue<HashSet<string>>("Types", typesYaml.Value);
|
||||
|
||||
var platformNode = node.Nodes.FirstOrDefault(n => n.Key == "Platform");
|
||||
if (platformNode != null)
|
||||
if (nodeDict.TryGetValue("Contexts", out var contextYaml))
|
||||
Contexts = FieldLoader.GetValue<HashSet<string>>("Contexts", contextYaml.Value);
|
||||
|
||||
if (nodeDict.TryGetValue("Platform", out var platformYaml))
|
||||
{
|
||||
var platformOverride = platformNode.Value.Nodes.FirstOrDefault(n => n.Key == Platform.CurrentPlatform.ToString());
|
||||
var platformOverride = platformYaml.NodeWithKeyOrDefault(Platform.CurrentPlatform.ToString());
|
||||
if (platformOverride != null)
|
||||
Default = FieldLoader.GetValue<Hotkey>("value", platformOverride.Value.Value);
|
||||
}
|
||||
|
||||
var readonlyNode = node.Nodes.FirstOrDefault(n => n.Key == "Readonly");
|
||||
if (readonlyNode != null)
|
||||
Readonly = FieldLoader.GetValue<bool>("Readonly", readonlyNode.Value.Value);
|
||||
if (nodeDict.TryGetValue("Readonly", out var readonlyYaml))
|
||||
Readonly = FieldLoader.GetValue<bool>("Readonly", readonlyYaml.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,10 +84,11 @@ namespace OpenRA
|
||||
{
|
||||
var client = HttpClientFactory.Create();
|
||||
|
||||
var httpResponseMessage = await client.GetAsync(playerDatabase.Profile + Fingerprint);
|
||||
var url = playerDatabase.Profile + Fingerprint;
|
||||
var httpResponseMessage = await client.GetAsync(url);
|
||||
var result = await httpResponseMessage.Content.ReadAsStreamAsync();
|
||||
|
||||
var yaml = MiniYaml.FromStream(result).First();
|
||||
var yaml = MiniYaml.FromStream(result, url).First();
|
||||
if (yaml.Key == "Player")
|
||||
{
|
||||
innerData = FieldLoader.Load<PlayerProfile>(yaml.Value);
|
||||
|
||||
@@ -43,17 +43,6 @@ namespace OpenRA
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ModelSequenceFormat : IGlobalModData
|
||||
{
|
||||
public readonly string Type;
|
||||
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
|
||||
public ModelSequenceFormat(MiniYaml yaml)
|
||||
{
|
||||
Type = yaml.Value;
|
||||
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
|
||||
}
|
||||
}
|
||||
|
||||
public class ModMetadata
|
||||
{
|
||||
public string Title;
|
||||
@@ -105,7 +94,8 @@ namespace OpenRA
|
||||
Id = modId;
|
||||
Package = package;
|
||||
|
||||
var nodes = MiniYaml.FromStream(package.GetStream("mod.yaml"), "mod.yaml");
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
var nodes = MiniYaml.FromStream(package.GetStream("mod.yaml"), $"{package.Name}:mod.yaml", stringPool: stringPool);
|
||||
for (var i = nodes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (nodes[i].Key != "Include")
|
||||
@@ -118,7 +108,7 @@ namespace OpenRA
|
||||
throw new YamlException($"{nodes[i].Location}: File `{filename}` not found.");
|
||||
|
||||
nodes.RemoveAt(i);
|
||||
nodes.InsertRange(i, MiniYaml.FromStream(contents, filename));
|
||||
nodes.InsertRange(i, MiniYaml.FromStream(contents, $"{package.Name}:{filename}", stringPool: stringPool));
|
||||
}
|
||||
|
||||
// Merge inherited overrides
|
||||
@@ -211,18 +201,18 @@ namespace OpenRA
|
||||
|
||||
static string[] YamlList(Dictionary<string, MiniYaml> yaml, string key)
|
||||
{
|
||||
if (!yaml.ContainsKey(key))
|
||||
if (!yaml.TryGetValue(key, out var value))
|
||||
return Array.Empty<string>();
|
||||
|
||||
return yaml[key].ToDictionary().Keys.ToArray();
|
||||
return value.Nodes.Select(n => n.Key).ToArray();
|
||||
}
|
||||
|
||||
static IReadOnlyDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
|
||||
{
|
||||
if (!yaml.ContainsKey(key))
|
||||
if (!yaml.TryGetValue(key, out var value))
|
||||
return new Dictionary<string, string>();
|
||||
|
||||
return yaml[key].ToDictionary(my => my.Value);
|
||||
return value.ToDictionary(my => my.Value);
|
||||
}
|
||||
|
||||
public bool Contains<T>() where T : IGlobalModData
|
||||
|
||||
@@ -175,8 +175,7 @@ namespace OpenRA
|
||||
protected CompositeActorInit(TraitInfo info)
|
||||
: base(info.InstanceName) { }
|
||||
|
||||
protected CompositeActorInit()
|
||||
: base() { }
|
||||
protected CompositeActorInit() { }
|
||||
|
||||
public virtual void Initialize(MiniYaml yaml)
|
||||
{
|
||||
|
||||
@@ -84,7 +84,7 @@ namespace OpenRA
|
||||
|
||||
public MiniYaml Save(Func<ActorInit, bool> initFilter = null)
|
||||
{
|
||||
var ret = new MiniYaml(Type);
|
||||
var nodes = new List<MiniYamlNode>();
|
||||
foreach (var o in initDict.Value)
|
||||
{
|
||||
if (o is not ActorInit init || o is ISuppressInitExport)
|
||||
@@ -98,10 +98,10 @@ namespace OpenRA
|
||||
if (!string.IsNullOrEmpty(init.InstanceName))
|
||||
initName += ActorInfo.TraitInstanceSeparator + init.InstanceName;
|
||||
|
||||
ret.Nodes.Add(new MiniYamlNode(initName, init.Save()));
|
||||
nodes.Add(new MiniYamlNode(initName, init.Save()));
|
||||
}
|
||||
|
||||
return ret;
|
||||
return new MiniYaml(Type, nodes);
|
||||
}
|
||||
|
||||
public IEnumerator<object> GetEnumerator() { return initDict.Value.GetEnumerator(); }
|
||||
@@ -139,7 +139,7 @@ namespace OpenRA
|
||||
return removed;
|
||||
}
|
||||
|
||||
public IEnumerable<T> GetAll<T>() where T : ActorInit
|
||||
public IReadOnlyCollection<T> GetAll<T>() where T : ActorInit
|
||||
{
|
||||
return initDict.Value.WithInterface<T>();
|
||||
}
|
||||
@@ -152,8 +152,9 @@ namespace OpenRA
|
||||
// If a more specific init is not available, fall back to an unnamed init.
|
||||
// If duplicate inits are defined, take the last to match standard yaml override expectations
|
||||
if (info != null && !string.IsNullOrEmpty(info.InstanceName))
|
||||
return inits.LastOrDefault(i => i.InstanceName == info.InstanceName) ??
|
||||
inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName));
|
||||
return
|
||||
inits.LastOrDefault(i => i.InstanceName == info.InstanceName) ??
|
||||
inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName));
|
||||
|
||||
// Untagged traits will only use untagged inits
|
||||
return inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName));
|
||||
|
||||
90
OpenRA.Game/Map/CellCoordsRegion.cs
Normal file
90
OpenRA.Game/Map/CellCoordsRegion.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright (c) The OpenRA Developers and Contributors
|
||||
* This file is part of OpenRA, which is free software. It is made
|
||||
* available to you under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version. For more
|
||||
* information, see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
public readonly struct CellCoordsRegion : IEnumerable<CPos>
|
||||
{
|
||||
public struct CellCoordsEnumerator : IEnumerator<CPos>
|
||||
{
|
||||
readonly CellCoordsRegion r;
|
||||
|
||||
public CellCoordsEnumerator(CellCoordsRegion region)
|
||||
: this()
|
||||
{
|
||||
r = region;
|
||||
Reset();
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
var x = Current.X + 1;
|
||||
var y = Current.Y;
|
||||
|
||||
// Check for column overflow.
|
||||
if (x > r.BottomRight.X)
|
||||
{
|
||||
y++;
|
||||
x = r.TopLeft.X;
|
||||
|
||||
// Check for row overflow.
|
||||
if (y > r.BottomRight.Y)
|
||||
return false;
|
||||
}
|
||||
|
||||
Current = new CPos(x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Current = new CPos(r.TopLeft.X - 1, r.TopLeft.Y);
|
||||
}
|
||||
|
||||
public CPos Current { get; private set; }
|
||||
|
||||
readonly object IEnumerator.Current => Current;
|
||||
public readonly void Dispose() { }
|
||||
}
|
||||
|
||||
public CellCoordsRegion(CPos topLeft, CPos bottomRight)
|
||||
{
|
||||
TopLeft = topLeft;
|
||||
BottomRight = bottomRight;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{TopLeft}->{BottomRight}";
|
||||
}
|
||||
|
||||
public CellCoordsEnumerator GetEnumerator()
|
||||
{
|
||||
return new CellCoordsEnumerator(this);
|
||||
}
|
||||
|
||||
IEnumerator<CPos> IEnumerable<CPos>.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public CPos TopLeft { get; }
|
||||
public CPos BottomRight { get; }
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
@@ -64,9 +63,9 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
/// <summary>Returns the minimal region that covers at least the specified cells.</summary>
|
||||
public static CellRegion BoundingRegion(MapGridType shape, IEnumerable<CPos> cells)
|
||||
public static CellRegion BoundingRegion(MapGridType shape, IReadOnlyCollection<CPos> cells)
|
||||
{
|
||||
if (cells == null || !cells.Any())
|
||||
if (cells == null || cells.Count == 0)
|
||||
throw new ArgumentException("cells must not be null or empty.", nameof(cells));
|
||||
|
||||
var minU = int.MaxValue;
|
||||
@@ -103,6 +102,7 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public MapCoordsRegion MapCoords => new(mapTopLeft, mapBottomRight);
|
||||
public CellCoordsRegion CellCoords => new(TopLeft, BottomRight);
|
||||
|
||||
public CellRegionEnumerator GetEnumerator()
|
||||
{
|
||||
@@ -136,12 +136,12 @@ namespace OpenRA
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
u += 1;
|
||||
u++;
|
||||
|
||||
// Check for column overflow
|
||||
if (u > r.mapBottomRight.U)
|
||||
{
|
||||
v += 1;
|
||||
v++;
|
||||
u = r.mapTopLeft.U;
|
||||
|
||||
// Check for row overflow
|
||||
@@ -162,8 +162,8 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public CPos Current { get; private set; }
|
||||
object IEnumerator.Current => Current;
|
||||
public void Dispose() { }
|
||||
readonly object IEnumerator.Current => Current;
|
||||
public readonly void Dispose() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
@@ -90,13 +91,13 @@ namespace OpenRA
|
||||
throw new InvalidOperationException("Map does not have a field/property " + fieldName);
|
||||
|
||||
var t = field != null ? field.FieldType : property.PropertyType;
|
||||
type = t == typeof(List<MiniYamlNode>) ? Type.NodeList :
|
||||
type = t == typeof(IReadOnlyCollection<MiniYamlNode>) ? Type.NodeList :
|
||||
t == typeof(MiniYaml) ? Type.MiniYaml : Type.Normal;
|
||||
}
|
||||
|
||||
public void Deserialize(Map map, List<MiniYamlNode> nodes)
|
||||
public void Deserialize(Map map, MiniYaml yaml)
|
||||
{
|
||||
var node = nodes.FirstOrDefault(n => n.Key == key);
|
||||
var node = yaml.NodeWithKeyOrDefault(key);
|
||||
if (node == null)
|
||||
{
|
||||
if (required)
|
||||
@@ -130,14 +131,14 @@ namespace OpenRA
|
||||
var value = field != null ? field.GetValue(map) : property.GetValue(map, null);
|
||||
if (type == Type.NodeList)
|
||||
{
|
||||
var listValue = (List<MiniYamlNode>)value;
|
||||
var listValue = (IReadOnlyCollection<MiniYamlNode>)value;
|
||||
if (required || listValue.Count > 0)
|
||||
nodes.Add(new MiniYamlNode(key, null, listValue));
|
||||
}
|
||||
else if (type == Type.MiniYaml)
|
||||
{
|
||||
var yamlValue = (MiniYaml)value;
|
||||
if (required || (yamlValue != null && (yamlValue.Value != null || yamlValue.Nodes.Count > 0)))
|
||||
if (required || (yamlValue != null && (yamlValue.Value != null || yamlValue.Nodes.Length > 0)))
|
||||
nodes.Add(new MiniYamlNode(key, yamlValue));
|
||||
}
|
||||
else
|
||||
@@ -158,26 +159,26 @@ namespace OpenRA
|
||||
/// <summary>Defines the order of the fields in map.yaml.</summary>
|
||||
static readonly MapField[] YamlFields =
|
||||
{
|
||||
new MapField("MapFormat"),
|
||||
new MapField("RequiresMod"),
|
||||
new MapField("Title"),
|
||||
new MapField("Author"),
|
||||
new MapField("Tileset"),
|
||||
new MapField("MapSize"),
|
||||
new MapField("Bounds"),
|
||||
new MapField("Visibility"),
|
||||
new MapField("Categories"),
|
||||
new MapField("LockPreview", required: false, ignoreIfValue: "False"),
|
||||
new MapField("Players", "PlayerDefinitions"),
|
||||
new MapField("Actors", "ActorDefinitions"),
|
||||
new MapField("Rules", "RuleDefinitions", required: false),
|
||||
new MapField("Translations", "TranslationDefinitions", required: false),
|
||||
new MapField("Sequences", "SequenceDefinitions", required: false),
|
||||
new MapField("ModelSequences", "ModelSequenceDefinitions", required: false),
|
||||
new MapField("Weapons", "WeaponDefinitions", required: false),
|
||||
new MapField("Voices", "VoiceDefinitions", required: false),
|
||||
new MapField("Music", "MusicDefinitions", required: false),
|
||||
new MapField("Notifications", "NotificationDefinitions", required: false),
|
||||
new("MapFormat"),
|
||||
new("RequiresMod"),
|
||||
new("Title"),
|
||||
new("Author"),
|
||||
new("Tileset"),
|
||||
new("MapSize"),
|
||||
new("Bounds"),
|
||||
new("Visibility"),
|
||||
new("Categories"),
|
||||
new("LockPreview", required: false, ignoreIfValue: "False"),
|
||||
new("Players", "PlayerDefinitions"),
|
||||
new("Actors", "ActorDefinitions"),
|
||||
new("Rules", "RuleDefinitions", required: false),
|
||||
new("Translations", "TranslationDefinitions", required: false),
|
||||
new("Sequences", "SequenceDefinitions", required: false),
|
||||
new("ModelSequences", "ModelSequenceDefinitions", required: false),
|
||||
new("Weapons", "WeaponDefinitions", required: false),
|
||||
new("Voices", "VoiceDefinitions", required: false),
|
||||
new("Music", "MusicDefinitions", required: false),
|
||||
new("Notifications", "NotificationDefinitions", required: false),
|
||||
};
|
||||
|
||||
// Format versions
|
||||
@@ -197,18 +198,18 @@ namespace OpenRA
|
||||
public int2 MapSize { get; private set; }
|
||||
|
||||
// Player and actor yaml. Public for access by the map importers and lint checks.
|
||||
public List<MiniYamlNode> PlayerDefinitions = new();
|
||||
public List<MiniYamlNode> ActorDefinitions = new();
|
||||
public IReadOnlyCollection<MiniYamlNode> PlayerDefinitions = ImmutableArray<MiniYamlNode>.Empty;
|
||||
public IReadOnlyCollection<MiniYamlNode> ActorDefinitions = ImmutableArray<MiniYamlNode>.Empty;
|
||||
|
||||
// Custom map yaml. Public for access by the map importers and lint checks
|
||||
public readonly MiniYaml RuleDefinitions;
|
||||
public readonly MiniYaml TranslationDefinitions;
|
||||
public readonly MiniYaml SequenceDefinitions;
|
||||
public readonly MiniYaml ModelSequenceDefinitions;
|
||||
public readonly MiniYaml WeaponDefinitions;
|
||||
public readonly MiniYaml VoiceDefinitions;
|
||||
public readonly MiniYaml MusicDefinitions;
|
||||
public readonly MiniYaml NotificationDefinitions;
|
||||
public MiniYaml RuleDefinitions;
|
||||
public MiniYaml TranslationDefinitions;
|
||||
public MiniYaml SequenceDefinitions;
|
||||
public MiniYaml ModelSequenceDefinitions;
|
||||
public MiniYaml WeaponDefinitions;
|
||||
public MiniYaml VoiceDefinitions;
|
||||
public MiniYaml MusicDefinitions;
|
||||
public MiniYaml NotificationDefinitions;
|
||||
|
||||
public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = new();
|
||||
|
||||
@@ -274,7 +275,10 @@ namespace OpenRA
|
||||
try
|
||||
{
|
||||
foreach (var filename in contents)
|
||||
if (filename.EndsWith(".yaml") || filename.EndsWith(".bin") || filename.EndsWith(".lua") || (format >= 12 && filename == "map.png"))
|
||||
if (filename.EndsWith(".yaml", StringComparison.Ordinal) ||
|
||||
filename.EndsWith(".bin", StringComparison.Ordinal) ||
|
||||
filename.EndsWith(".lua", StringComparison.Ordinal) ||
|
||||
(format >= 12 && filename == "map.png"))
|
||||
streams.Add(package.GetStream(filename));
|
||||
|
||||
// Take the SHA1
|
||||
@@ -357,15 +361,15 @@ namespace OpenRA
|
||||
if (!Package.Contains("map.yaml") || !Package.Contains("map.bin"))
|
||||
throw new InvalidDataException($"Not a valid map\n File: {package.Name}");
|
||||
|
||||
var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), package.Name));
|
||||
var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), $"{package.Name}:map.yaml"));
|
||||
foreach (var field in YamlFields)
|
||||
field.Deserialize(this, yaml.Nodes);
|
||||
field.Deserialize(this, yaml);
|
||||
|
||||
if (MapFormat < SupportedMapFormat)
|
||||
throw new InvalidDataException($"Map format {MapFormat} is not supported.\n File: {package.Name}");
|
||||
|
||||
PlayerDefinitions = MiniYaml.NodesOrEmpty(yaml, "Players");
|
||||
ActorDefinitions = MiniYaml.NodesOrEmpty(yaml, "Actors");
|
||||
PlayerDefinitions = yaml.NodeWithKeyOrDefault("Players")?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty;
|
||||
ActorDefinitions = yaml.NodeWithKeyOrDefault("Actors")?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty;
|
||||
|
||||
Grid = modData.Manifest.Get<MapGrid>();
|
||||
|
||||
@@ -607,7 +611,7 @@ namespace OpenRA
|
||||
|
||||
// Odd-height ramps get bumped up a level to the next even height layer
|
||||
if ((height & 1) == 1 && Ramp[uv] != 0)
|
||||
height += 1;
|
||||
height++;
|
||||
|
||||
var candidates = new List<PPos>();
|
||||
|
||||
@@ -646,21 +650,24 @@ namespace OpenRA
|
||||
foreach (var file in Package.Contents)
|
||||
toPackage.Update(file, Package.GetStream(file).ReadAllBytes());
|
||||
|
||||
if (!LockPreview)
|
||||
void UpdatePackage(string filename, byte[] data)
|
||||
{
|
||||
var previewData = SavePreview();
|
||||
if (Package != toPackage || !Enumerable.SequenceEqual(previewData, Package.GetStream("map.png").ReadAllBytes()))
|
||||
toPackage.Update("map.png", previewData);
|
||||
if (Package != toPackage)
|
||||
toPackage.Update(filename, data);
|
||||
else
|
||||
{
|
||||
var stream = Package.GetStream(filename);
|
||||
if (stream == null || !Enumerable.SequenceEqual(data, stream.ReadAllBytes()))
|
||||
toPackage.Update(filename, data);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the package with the new map data
|
||||
var textData = Encoding.UTF8.GetBytes(root.WriteToString());
|
||||
if (Package != toPackage || !Enumerable.SequenceEqual(textData, Package.GetStream("map.yaml").ReadAllBytes()))
|
||||
toPackage.Update("map.yaml", textData);
|
||||
if (!LockPreview)
|
||||
UpdatePackage("map.png", SavePreview());
|
||||
|
||||
var binaryData = SaveBinaryData();
|
||||
if (Package != toPackage || !Enumerable.SequenceEqual(binaryData, Package.GetStream("map.bin").ReadAllBytes()))
|
||||
toPackage.Update("map.bin", binaryData);
|
||||
// Update the package with the new map data
|
||||
UpdatePackage("map.yaml", Encoding.UTF8.GetBytes(root.WriteToString()));
|
||||
UpdatePackage("map.bin", SaveBinaryData());
|
||||
|
||||
Package = toPackage;
|
||||
|
||||
@@ -681,16 +688,16 @@ namespace OpenRA
|
||||
writer.Write((ushort)MapSize.Y);
|
||||
|
||||
// Data offsets
|
||||
var tilesOffset = 17;
|
||||
const int TilesOffset = 17;
|
||||
var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.X * MapSize.Y + 17 : 0;
|
||||
var resourcesOffset = (Grid.MaximumTerrainHeight > 0 ? 4 : 3) * MapSize.X * MapSize.Y + 17;
|
||||
|
||||
writer.Write((uint)tilesOffset);
|
||||
writer.Write((uint)TilesOffset);
|
||||
writer.Write((uint)heightsOffset);
|
||||
writer.Write((uint)resourcesOffset);
|
||||
|
||||
// Tile data
|
||||
if (tilesOffset != 0)
|
||||
if (TilesOffset != 0)
|
||||
{
|
||||
for (var i = 0; i < MapSize.X; i++)
|
||||
{
|
||||
@@ -772,19 +779,10 @@ namespace OpenRA
|
||||
|
||||
if (Grid.MaximumTerrainHeight > 0)
|
||||
{
|
||||
// The minimap is drawn in cell space, so we need to
|
||||
// unproject the PPos bounds to find the MPos boundaries.
|
||||
// This matches the calculation in RadarWidget that is used ingame
|
||||
for (var x = Bounds.Left; x < Bounds.Right; x++)
|
||||
{
|
||||
var allTop = Unproject(new PPos(x, Bounds.Top));
|
||||
var allBottom = Unproject(new PPos(x, Bounds.Bottom));
|
||||
if (allTop.Count > 0)
|
||||
top = Math.Min(top, allTop.MinBy(uv => uv.V).V);
|
||||
(top, bottom) = GetCellSpaceBounds();
|
||||
|
||||
if (allBottom.Count > 0)
|
||||
bottom = Math.Max(bottom, allBottom.MaxBy(uv => uv.V).V);
|
||||
}
|
||||
if (top == int.MaxValue || bottom == int.MinValue)
|
||||
throw new InvalidDataException("The map has invalid boundaries");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -801,7 +799,7 @@ namespace OpenRA
|
||||
bitmapWidth = 2 * bitmapWidth - 1;
|
||||
|
||||
var stride = bitmapWidth * 4;
|
||||
var pxStride = 4;
|
||||
const int PxStride = 4;
|
||||
var minimapData = new byte[stride * height];
|
||||
(Color Left, Color Right) terrainColor = default;
|
||||
|
||||
@@ -823,10 +821,10 @@ namespace OpenRA
|
||||
{
|
||||
// Odd rows are shifted right by 1px
|
||||
var dx = uv.V & 1;
|
||||
var xOffset = pxStride * (2 * x + dx);
|
||||
var xOffset = PxStride * (2 * x + dx);
|
||||
if (x + dx > 0)
|
||||
{
|
||||
var z = y * stride + xOffset - pxStride;
|
||||
var z = y * stride + xOffset - PxStride;
|
||||
var c = actorColor.A == 0 ? terrainColor.Left : actorColor;
|
||||
minimapData[z++] = c.R;
|
||||
minimapData[z++] = c.G;
|
||||
@@ -846,7 +844,7 @@ namespace OpenRA
|
||||
}
|
||||
else
|
||||
{
|
||||
var z = y * stride + pxStride * x;
|
||||
var z = y * stride + PxStride * x;
|
||||
var c = actorColor.A == 0 ? terrainColor.Left : actorColor;
|
||||
minimapData[z++] = c.R;
|
||||
minimapData[z++] = c.G;
|
||||
@@ -860,6 +858,28 @@ namespace OpenRA
|
||||
return png.Save();
|
||||
}
|
||||
|
||||
public (int Top, int Bottom) GetCellSpaceBounds()
|
||||
{
|
||||
var top = int.MaxValue;
|
||||
var bottom = int.MinValue;
|
||||
|
||||
// The minimap is drawn in cell space, so we need to
|
||||
// unproject the PPos bounds to find the MPos boundaries.
|
||||
// This matches the calculation in RadarWidget that is used ingame
|
||||
for (var x = Bounds.Left; x < Bounds.Right; x++)
|
||||
{
|
||||
var allTop = Unproject(new PPos(x, Bounds.Top));
|
||||
var allBottom = Unproject(new PPos(x, Bounds.Bottom));
|
||||
if (allTop.Count > 0)
|
||||
top = Math.Min(top, allTop.MinBy(uv => uv.V).V);
|
||||
|
||||
if (allBottom.Count > 0)
|
||||
bottom = Math.Max(bottom, allBottom.MaxBy(uv => uv.V).V);
|
||||
}
|
||||
|
||||
return (top, bottom);
|
||||
}
|
||||
|
||||
public bool Contains(CPos cell)
|
||||
{
|
||||
if (Grid.Type == MapGridType.RectangularIsometric)
|
||||
@@ -1180,7 +1200,7 @@ namespace OpenRA
|
||||
// Project this guessed cell and take the first available cell
|
||||
// If it is projected outside the layer, then make another guess.
|
||||
var allProjected = ProjectedCellsCovering(uv);
|
||||
var projected = allProjected.Length > 0 ? allProjected.First()
|
||||
var projected = allProjected.Length > 0 ? allProjected[0]
|
||||
: new PPos(uv.U, uv.V.Clamp(Bounds.Top, Bounds.Bottom));
|
||||
|
||||
// Clamp the projected cell to the map area
|
||||
@@ -1249,7 +1269,7 @@ namespace OpenRA
|
||||
PPos edge;
|
||||
if (allProjected.Length > 0)
|
||||
{
|
||||
var puv = allProjected.First();
|
||||
var puv = allProjected[0];
|
||||
var horizontalBound = (puv.U - Bounds.Left < Bounds.Width / 2) ? Bounds.Left : Bounds.Right;
|
||||
var verticalBound = (puv.V - Bounds.Top < Bounds.Height / 2) ? Bounds.Top : Bounds.Bottom;
|
||||
|
||||
@@ -1349,13 +1369,18 @@ namespace OpenRA
|
||||
throw new ArgumentOutOfRangeException(nameof(maxRange),
|
||||
$"The requested range ({maxRange}) cannot exceed the value of MaximumTileSearchRange ({Grid.MaximumTileSearchRange})");
|
||||
|
||||
for (var i = minRange; i <= maxRange; i++)
|
||||
return FindTilesInAnnulus();
|
||||
|
||||
IEnumerable<CPos> FindTilesInAnnulus()
|
||||
{
|
||||
foreach (var offset in Grid.TilesByDistance[i])
|
||||
for (var i = minRange; i <= maxRange; i++)
|
||||
{
|
||||
var t = offset + center;
|
||||
if (allowOutsideBounds ? Tiles.Contains(t) : Contains(t))
|
||||
yield return t;
|
||||
foreach (var offset in Grid.TilesByDistance[i])
|
||||
{
|
||||
var t = offset + center;
|
||||
if (allowOutsideBounds ? Tiles.Contains(t) : Contains(t))
|
||||
yield return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace OpenRA
|
||||
readonly object syncRoot = new();
|
||||
readonly Queue<MapPreview> generateMinimap = new();
|
||||
|
||||
public Dictionary<string, string> StringPool { get; } = new Dictionary<string, string>();
|
||||
public HashSet<string> StringPool { get; } = new();
|
||||
|
||||
readonly List<MapDirectoryTracker> mapDirectoryTrackers = new();
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace OpenRA
|
||||
? MapClassification.Unknown : Enum<MapClassification>.Parse(kv.Value);
|
||||
|
||||
IReadOnlyPackage package;
|
||||
var optional = name.StartsWith("~", StringComparison.Ordinal);
|
||||
var optional = name.StartsWith('~');
|
||||
if (optional)
|
||||
name = name[1..];
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace OpenRA
|
||||
// HACK: If the path is inside the support directory then we may need to create it
|
||||
// Assume that the path is a directory if there is not an existing file with the same name
|
||||
var resolved = Platform.ResolvePath(name);
|
||||
if (resolved.StartsWith(Platform.SupportDir) && !File.Exists(resolved))
|
||||
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && !File.Exists(resolved))
|
||||
Directory.CreateDirectory(resolved);
|
||||
|
||||
package = modData.ModFiles.OpenPackage(name);
|
||||
@@ -128,7 +128,10 @@ namespace OpenRA
|
||||
foreach (var kv in MapLocations)
|
||||
{
|
||||
foreach (var map in kv.Key.Contents)
|
||||
{
|
||||
LoadMapInternal(map, kv.Key, kv.Value, mapGrid, null, modDataRules);
|
||||
GC.Collect();
|
||||
}
|
||||
}
|
||||
|
||||
// We only want to track maps in runtime, not at loadtime
|
||||
@@ -148,11 +151,16 @@ namespace OpenRA
|
||||
using (new PerfTimer(map))
|
||||
{
|
||||
mapPackage = package.OpenPackage(map, modData.ModFiles);
|
||||
Log.Write("debug", $"Loading Map: {map} FROM {mapPackage.Name}");
|
||||
Console.WriteLine($"Loading Map: {map} FROM {mapPackage.Name}");
|
||||
if (mapPackage != null)
|
||||
{
|
||||
var uid = Map.ComputeUID(mapPackage);
|
||||
previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type, modDataRules);
|
||||
|
||||
// Freeing the package to save memory if there is a lot of Maps
|
||||
previews[uid].PackageDispose();
|
||||
|
||||
if (oldMap != uid)
|
||||
{
|
||||
LastModifiedMap = uid;
|
||||
@@ -168,9 +176,13 @@ namespace OpenRA
|
||||
Console.WriteLine($"Failed to load map: {map}");
|
||||
Console.WriteLine("Details:");
|
||||
Console.WriteLine(e);
|
||||
Console.WriteLine("StackTrace:");
|
||||
Console.WriteLine(System.Environment.StackTrace);
|
||||
Log.Write("debug", $"Failed to load map: {map}");
|
||||
Log.Write("debug", "Details:");
|
||||
Log.Write("debug", e);
|
||||
Log.Write("debug", "StackTrace:");
|
||||
Log.Write("debug", System.Environment.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,13 +202,13 @@ namespace OpenRA
|
||||
continue;
|
||||
|
||||
var name = kv.Key;
|
||||
var optional = name.StartsWith("~", StringComparison.Ordinal);
|
||||
var optional = name.StartsWith('~');
|
||||
if (optional)
|
||||
name = name[1..];
|
||||
|
||||
// Don't try to open the map directory in the support directory if it doesn't exist
|
||||
var resolved = Platform.ResolvePath(name);
|
||||
if (resolved.StartsWith(Platform.SupportDir) && (!Directory.Exists(resolved) || !File.Exists(resolved)))
|
||||
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && (!Directory.Exists(resolved) || !File.Exists(resolved)))
|
||||
continue;
|
||||
|
||||
using (var package = (IReadWritePackage)modData.ModFiles.OpenPackage(name))
|
||||
@@ -233,11 +245,12 @@ namespace OpenRA
|
||||
.ToList();
|
||||
|
||||
foreach (var uid in queryUids)
|
||||
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null);
|
||||
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null, null);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var client = HttpClientFactory.Create();
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
|
||||
// Limit each query to 50 maps at a time to avoid request size limits
|
||||
for (var i = 0; i < queryUids.Count; i += 50)
|
||||
@@ -249,15 +262,15 @@ namespace OpenRA
|
||||
var httpResponseMessage = await client.GetAsync(url);
|
||||
var result = await httpResponseMessage.Content.ReadAsStreamAsync();
|
||||
|
||||
var yaml = MiniYaml.FromStream(result);
|
||||
var yaml = MiniYaml.FromStream(result, url, stringPool: stringPool);
|
||||
foreach (var kv in yaml)
|
||||
previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, mapDetailsReceived);
|
||||
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);
|
||||
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -269,7 +282,7 @@ namespace OpenRA
|
||||
foreach (var uid in batchUids)
|
||||
{
|
||||
var p = previews[uid];
|
||||
p.UpdateRemoteSearch(MapStatus.Unavailable, null);
|
||||
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null);
|
||||
mapQueryFailed?.Invoke(p);
|
||||
}
|
||||
}
|
||||
@@ -282,11 +295,11 @@ namespace OpenRA
|
||||
Log.Write("debug", "MapCache.LoadAsyncInternal started");
|
||||
|
||||
// Milliseconds to wait on one loop when nothing to do
|
||||
var emptyDelay = 50;
|
||||
const int EmptyDelay = 50;
|
||||
|
||||
// Keep the thread alive for at least 5 seconds after the last minimap generation
|
||||
var maxKeepAlive = 5000 / emptyDelay;
|
||||
var keepAlive = maxKeepAlive;
|
||||
const int MaxKeepAlive = 5000 / EmptyDelay;
|
||||
var keepAlive = MaxKeepAlive;
|
||||
|
||||
while (true)
|
||||
{
|
||||
@@ -306,11 +319,11 @@ namespace OpenRA
|
||||
|
||||
if (todo.Count == 0)
|
||||
{
|
||||
Thread.Sleep(emptyDelay);
|
||||
Thread.Sleep(EmptyDelay);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
keepAlive = maxKeepAlive;
|
||||
keepAlive = MaxKeepAlive;
|
||||
|
||||
// Render the minimap into the shared sheet
|
||||
foreach (var p in todo)
|
||||
@@ -348,8 +361,8 @@ namespace OpenRA
|
||||
|
||||
while (this[uid].Status != MapStatus.Available)
|
||||
{
|
||||
if (mapUpdates.ContainsKey(uid))
|
||||
uid = mapUpdates[uid];
|
||||
if (mapUpdates.TryGetValue(uid, out var newUid))
|
||||
uid = newUid;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
@@ -372,7 +385,6 @@ namespace OpenRA
|
||||
{
|
||||
// Wait for any existing thread to exit before starting a new one.
|
||||
previewLoaderThread?.Join();
|
||||
|
||||
previewLoaderThread = new Thread(LoadAsyncInternal)
|
||||
{
|
||||
Name = "Map Preview Loader",
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace OpenRA
|
||||
// Check for column overflow
|
||||
if (u > r.BottomRight.U)
|
||||
{
|
||||
v += 1;
|
||||
v++;
|
||||
u = r.TopLeft.U;
|
||||
|
||||
// Check for row overflow
|
||||
@@ -53,8 +53,8 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public MPos Current { get; private set; }
|
||||
object IEnumerator.Current => Current;
|
||||
public void Dispose() { }
|
||||
readonly object IEnumerator.Current => Current;
|
||||
public readonly void Dispose() { }
|
||||
}
|
||||
|
||||
public MapCoordsRegion(MPos mapTopLeft, MPos mapBottomRight)
|
||||
|
||||
@@ -116,12 +116,12 @@ namespace OpenRA
|
||||
|
||||
public readonly WVec[] SubCellOffsets =
|
||||
{
|
||||
new WVec(0, 0, 0), // full cell - index 0
|
||||
new WVec(-299, -256, 0), // top left - index 1
|
||||
new WVec(256, -256, 0), // top right - index 2
|
||||
new WVec(0, 0, 0), // center - index 3
|
||||
new WVec(-299, 256, 0), // bottom left - index 4
|
||||
new WVec(256, 256, 0), // bottom right - index 5
|
||||
new(0, 0, 0), // full cell - index 0
|
||||
new(-299, -256, 0), // top left - index 1
|
||||
new(256, -256, 0), // top right - index 2
|
||||
new(0, 0, 0), // center - index 3
|
||||
new(-299, 256, 0), // bottom left - index 4
|
||||
new(256, 256, 0), // bottom right - index 5
|
||||
};
|
||||
|
||||
public CellRamp[] Ramps { get; }
|
||||
|
||||
@@ -59,6 +59,7 @@ namespace OpenRA
|
||||
public readonly string rules;
|
||||
public readonly string players_block;
|
||||
public readonly int mapformat;
|
||||
public readonly string game_mod;
|
||||
}
|
||||
|
||||
public sealed class MapPreview : IDisposable, IReadOnlyFileSystem
|
||||
@@ -139,10 +140,11 @@ namespace OpenRA
|
||||
files = files.Append(mapFiles);
|
||||
}
|
||||
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
var sources =
|
||||
modDataRules.Select(x => x.Where(IsLoadableRuleDefinition).ToList())
|
||||
.Concat(files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s).Where(IsLoadableRuleDefinition).ToList()));
|
||||
if (RuleDefinitions.Nodes.Count > 0)
|
||||
.Concat(files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool).Where(IsLoadableRuleDefinition).ToList()));
|
||||
if (RuleDefinitions.Nodes.Length > 0)
|
||||
sources = sources.Append(RuleDefinitions.Nodes.Where(IsLoadableRuleDefinition).ToList());
|
||||
|
||||
var yamlNodes = MiniYaml.Merge(sources);
|
||||
@@ -172,7 +174,23 @@ namespace OpenRA
|
||||
readonly ModData modData;
|
||||
|
||||
public readonly string Uid;
|
||||
public IReadOnlyPackage Package { get; private set; }
|
||||
public string PackageName { get; private set; }
|
||||
IReadOnlyPackage pPackage;
|
||||
public IReadOnlyPackage Package
|
||||
{
|
||||
get
|
||||
{
|
||||
if (pPackage == null)
|
||||
{
|
||||
pPackage = parentPackage.OpenPackage(PackageName, modData.ModFiles);
|
||||
}
|
||||
|
||||
return pPackage;
|
||||
}
|
||||
|
||||
private set => pPackage = value;
|
||||
}
|
||||
|
||||
IReadOnlyPackage parentPackage;
|
||||
|
||||
volatile InnerData innerData;
|
||||
@@ -256,6 +274,7 @@ namespace OpenRA
|
||||
{
|
||||
this.cache = cache;
|
||||
this.modData = modData;
|
||||
this.pPackage = null;
|
||||
|
||||
Uid = uid;
|
||||
innerData = new InnerData
|
||||
@@ -282,9 +301,10 @@ namespace OpenRA
|
||||
{
|
||||
this.modData = modData;
|
||||
cache = modData.MapCache;
|
||||
this.pPackage = null;
|
||||
|
||||
Uid = map.Uid;
|
||||
Package = map.Package;
|
||||
PackageName = map.Package.Name;
|
||||
|
||||
var mapPlayers = new MapPlayers(map.PlayerDefinitions);
|
||||
var spawns = new List<CPos>();
|
||||
@@ -333,10 +353,10 @@ namespace OpenRA
|
||||
if (yamlStream == null)
|
||||
throw new FileNotFoundException("Required file map.yaml not present in this map");
|
||||
|
||||
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, "map.yaml", stringPool: cache.StringPool)).ToDictionary();
|
||||
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, $"{p.Name}:map.yaml", stringPool: cache.StringPool)).ToDictionary();
|
||||
}
|
||||
|
||||
Package = p;
|
||||
PackageName = p.Name;
|
||||
parentPackage = parent;
|
||||
|
||||
var newData = innerData.Clone();
|
||||
@@ -427,7 +447,7 @@ namespace OpenRA
|
||||
innerData = newData;
|
||||
}
|
||||
|
||||
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action<MapPreview> parseMetadata = null)
|
||||
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, string[] mapCompatibility, Action<MapPreview> parseMetadata = null)
|
||||
{
|
||||
var newData = innerData.Clone();
|
||||
newData.Status = status;
|
||||
@@ -474,11 +494,19 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
var playersString = Encoding.UTF8.GetString(Convert.FromBase64String(r.players_block));
|
||||
newData.Players = new MapPlayers(MiniYaml.FromString(playersString));
|
||||
newData.Players = new MapPlayers(MiniYaml.FromString(playersString,
|
||||
$"{yaml.NodeWithKey(nameof(r.players_block)).Location.Name}:{nameof(r.players_block)}"));
|
||||
|
||||
var rulesString = Encoding.UTF8.GetString(Convert.FromBase64String(r.rules));
|
||||
var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString)).ToDictionary();
|
||||
var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString,
|
||||
$"{yaml.NodeWithKey(nameof(r.rules)).Location.Name}:{nameof(r.rules)}")).ToDictionary();
|
||||
newData.SetCustomRules(modData, this, rulesYaml, null);
|
||||
|
||||
// Map is for a different mod: update its information so it can be displayed
|
||||
// in the cross-mod server browser UI, but mark it as unavailable so it can't
|
||||
// be selected in a server for the current mod.
|
||||
if (!mapCompatibility.Contains(r.game_mod))
|
||||
newData.Status = MapStatus.Unavailable;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -577,10 +605,15 @@ namespace OpenRA
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Package != null)
|
||||
PackageDispose();
|
||||
}
|
||||
|
||||
public void PackageDispose()
|
||||
{
|
||||
if (pPackage != null)
|
||||
{
|
||||
Package.Dispose();
|
||||
Package = null;
|
||||
pPackage.Dispose();
|
||||
pPackage = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using OpenRA.Primitives;
|
||||
|
||||
namespace OpenRA
|
||||
@@ -53,5 +54,15 @@ namespace OpenRA
|
||||
{
|
||||
return Bounds.Contains(uv.U, uv.V);
|
||||
}
|
||||
|
||||
public int IndexOf(T value, int startIndex)
|
||||
{
|
||||
return Array.IndexOf(Entries, value, startIndex);
|
||||
}
|
||||
|
||||
public void SetAll(T value)
|
||||
{
|
||||
Array.Fill(Entries, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,12 +93,12 @@ namespace OpenRA
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
u += 1;
|
||||
u++;
|
||||
|
||||
// Check for column overflow
|
||||
if (u > r.BottomRight.U)
|
||||
{
|
||||
v += 1;
|
||||
v++;
|
||||
u = r.TopLeft.U;
|
||||
|
||||
// Check for row overflow
|
||||
@@ -118,8 +118,8 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public PPos Current { get; private set; }
|
||||
object IEnumerator.Current => Current;
|
||||
public void Dispose() { }
|
||||
readonly object IEnumerator.Current => Current;
|
||||
public readonly void Dispose() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,18 +20,36 @@ namespace OpenRA
|
||||
{
|
||||
public static class MiniYamlExts
|
||||
{
|
||||
public static void WriteToFile(this List<MiniYamlNode> y, string filename)
|
||||
public static void WriteToFile(this IEnumerable<MiniYamlNode> y, string filename)
|
||||
{
|
||||
File.WriteAllLines(filename, y.ToLines().Select(x => x.TrimEnd()).ToArray());
|
||||
}
|
||||
|
||||
public static string WriteToString(this List<MiniYamlNode> y)
|
||||
public static string WriteToString(this IEnumerable<MiniYamlNode> y)
|
||||
{
|
||||
// Remove all trailing newlines and restore the final EOF newline
|
||||
return y.ToLines().JoinWith("\n").TrimEnd('\n') + "\n";
|
||||
}
|
||||
|
||||
public static IEnumerable<string> ToLines(this List<MiniYamlNode> y)
|
||||
public static IEnumerable<string> ToLines(this IEnumerable<MiniYamlNode> y)
|
||||
{
|
||||
foreach (var kv in y)
|
||||
foreach (var line in kv.Value.ToLines(kv.Key, kv.Comment))
|
||||
yield return line;
|
||||
}
|
||||
|
||||
public static void WriteToFile(this IEnumerable<MiniYamlNodeBuilder> y, string filename)
|
||||
{
|
||||
File.WriteAllLines(filename, y.ToLines().Select(x => x.TrimEnd()).ToArray());
|
||||
}
|
||||
|
||||
public static string WriteToString(this IEnumerable<MiniYamlNodeBuilder> y)
|
||||
{
|
||||
// Remove all trailing newlines and restore the final EOF newline
|
||||
return y.ToLines().JoinWith("\n").TrimEnd('\n') + "\n";
|
||||
}
|
||||
|
||||
public static IEnumerable<string> ToLines(this IEnumerable<MiniYamlNodeBuilder> y)
|
||||
{
|
||||
foreach (var kv in y)
|
||||
foreach (var line in kv.Value.ToLines(kv.Key, kv.Comment))
|
||||
@@ -43,22 +61,29 @@ namespace OpenRA
|
||||
{
|
||||
public readonly struct SourceLocation
|
||||
{
|
||||
public readonly string Filename;
|
||||
public readonly string Name;
|
||||
public readonly int Line;
|
||||
|
||||
public SourceLocation(string filename, int line)
|
||||
public SourceLocation(string name, int line)
|
||||
{
|
||||
Filename = filename;
|
||||
Name = name;
|
||||
Line = line;
|
||||
}
|
||||
|
||||
public override string ToString() { return $"{Filename}:{Line}"; }
|
||||
public override string ToString() { return $"{Name}:{Line}"; }
|
||||
}
|
||||
|
||||
public SourceLocation Location;
|
||||
public string Key;
|
||||
public MiniYaml Value;
|
||||
public string Comment;
|
||||
public readonly SourceLocation Location;
|
||||
public readonly string Key;
|
||||
public readonly MiniYaml Value;
|
||||
public readonly string Comment;
|
||||
|
||||
public MiniYamlNode WithValue(MiniYaml value)
|
||||
{
|
||||
if (Value == value)
|
||||
return this;
|
||||
return new MiniYamlNode(Key, value, Comment, Location);
|
||||
}
|
||||
|
||||
public MiniYamlNode(string k, MiniYaml v, string c = null)
|
||||
{
|
||||
@@ -74,26 +99,15 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public MiniYamlNode(string k, string v, string c = null)
|
||||
: this(k, v, c, null) { }
|
||||
: this(k, new MiniYaml(v, Enumerable.Empty<MiniYamlNode>()), c) { }
|
||||
|
||||
public MiniYamlNode(string k, string v, List<MiniYamlNode> n)
|
||||
public MiniYamlNode(string k, string v, IEnumerable<MiniYamlNode> n)
|
||||
: this(k, new MiniYaml(v, n), null) { }
|
||||
|
||||
public MiniYamlNode(string k, string v, string c, List<MiniYamlNode> n)
|
||||
: this(k, new MiniYaml(v, n), c) { }
|
||||
|
||||
public MiniYamlNode(string k, string v, string c, List<MiniYamlNode> n, SourceLocation loc)
|
||||
: this(k, new MiniYaml(v, n), c, loc) { }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{{YamlNode: {Key} @ {Location}}}";
|
||||
}
|
||||
|
||||
public MiniYamlNode Clone()
|
||||
{
|
||||
return new MiniYamlNode(Key, Value.Clone(), Comment, Location);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MiniYaml
|
||||
@@ -101,15 +115,58 @@ namespace OpenRA
|
||||
const int SpacesPerLevel = 4;
|
||||
static readonly Func<string, string> StringIdentity = s => s;
|
||||
static readonly Func<MiniYaml, MiniYaml> MiniYamlIdentity = my => my;
|
||||
public string Value;
|
||||
public List<MiniYamlNode> Nodes;
|
||||
|
||||
public MiniYaml Clone()
|
||||
public readonly string Value;
|
||||
public readonly ImmutableArray<MiniYamlNode> Nodes;
|
||||
|
||||
public MiniYaml WithValue(string value)
|
||||
{
|
||||
var clonedNodes = new List<MiniYamlNode>(Nodes.Count);
|
||||
if (Value == value)
|
||||
return this;
|
||||
return new MiniYaml(value, Nodes);
|
||||
}
|
||||
|
||||
public MiniYaml WithNodes(IEnumerable<MiniYamlNode> nodes)
|
||||
{
|
||||
if (nodes is ImmutableArray<MiniYamlNode> n && Nodes == n)
|
||||
return this;
|
||||
return new MiniYaml(Value, nodes);
|
||||
}
|
||||
|
||||
public MiniYaml WithNodesAppended(IEnumerable<MiniYamlNode> nodes)
|
||||
{
|
||||
var newNodes = Nodes.AddRange(nodes);
|
||||
if (Nodes == newNodes)
|
||||
return this;
|
||||
return new MiniYaml(Value, newNodes);
|
||||
}
|
||||
|
||||
public MiniYamlNode NodeWithKey(string key)
|
||||
{
|
||||
var result = NodeWithKeyOrDefault(key);
|
||||
if (result == null)
|
||||
throw new InvalidDataException($"No node with key '{key}'");
|
||||
return result;
|
||||
}
|
||||
|
||||
public MiniYamlNode NodeWithKeyOrDefault(string key)
|
||||
{
|
||||
// PERF: Avoid LINQ.
|
||||
var first = true;
|
||||
MiniYamlNode result = null;
|
||||
foreach (var node in Nodes)
|
||||
clonedNodes.Add(node.Clone());
|
||||
return new MiniYaml(Value, clonedNodes);
|
||||
{
|
||||
if (node.Key != key)
|
||||
continue;
|
||||
|
||||
if (!first)
|
||||
throw new InvalidDataException($"Duplicate key '{node.Key}' in {node.Location}");
|
||||
|
||||
first = false;
|
||||
result = node;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Dictionary<string, MiniYaml> ToDictionary()
|
||||
@@ -125,7 +182,7 @@ namespace OpenRA
|
||||
public Dictionary<TKey, TElement> ToDictionary<TKey, TElement>(
|
||||
Func<string, TKey> keySelector, Func<MiniYaml, TElement> elementSelector)
|
||||
{
|
||||
var ret = new Dictionary<TKey, TElement>(Nodes.Count);
|
||||
var ret = new Dictionary<TKey, TElement>(Nodes.Length);
|
||||
foreach (var y in Nodes)
|
||||
{
|
||||
var key = keySelector(y.Key);
|
||||
@@ -138,28 +195,27 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
public MiniYaml(string value)
|
||||
: this(value, null) { }
|
||||
: this(value, Enumerable.Empty<MiniYamlNode>()) { }
|
||||
|
||||
public MiniYaml(string value, List<MiniYamlNode> nodes)
|
||||
public MiniYaml(string value, IEnumerable<MiniYamlNode> nodes)
|
||||
{
|
||||
Value = value;
|
||||
Nodes = nodes ?? new List<MiniYamlNode>();
|
||||
Nodes = ImmutableArray.CreateRange(nodes);
|
||||
}
|
||||
|
||||
public static List<MiniYamlNode> NodesOrEmpty(MiniYaml y, string s)
|
||||
static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string name, bool discardCommentsAndWhitespace, HashSet<string> stringPool)
|
||||
{
|
||||
var nd = y.ToDictionary();
|
||||
return nd.TryGetValue(s, out var v) ? v.Nodes : new List<MiniYamlNode>();
|
||||
}
|
||||
// YAML config often contains repeated strings for key, values, comments.
|
||||
// Pool these strings so we only need one copy of each unique string.
|
||||
// This saves on long-term memory usage as parsed values can often live a long time.
|
||||
// A caller can also provide a pool as input, allowing de-duplication across multiple parses.
|
||||
stringPool ??= new HashSet<string>();
|
||||
|
||||
static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string filename, bool discardCommentsAndWhitespace, Dictionary<string, string> stringPool)
|
||||
{
|
||||
stringPool ??= new Dictionary<string, string>();
|
||||
|
||||
var levels = new List<List<MiniYamlNode>>
|
||||
var result = new List<List<MiniYamlNode>>
|
||||
{
|
||||
new List<MiniYamlNode>()
|
||||
new()
|
||||
};
|
||||
var parsedLines = new List<(int Level, string Key, string Value, string Comment, MiniYamlNode.SourceLocation Location)>();
|
||||
|
||||
var lineNo = 0;
|
||||
foreach (var ll in lines)
|
||||
@@ -175,7 +231,7 @@ namespace OpenRA
|
||||
ReadOnlySpan<char> key = default;
|
||||
ReadOnlySpan<char> value = default;
|
||||
ReadOnlySpan<char> comment = default;
|
||||
var location = new MiniYamlNode.SourceLocation(filename, lineNo);
|
||||
var location = new MiniYamlNode.SourceLocation(name, lineNo);
|
||||
|
||||
if (line.Length > 0)
|
||||
{
|
||||
@@ -206,15 +262,6 @@ namespace OpenRA
|
||||
}
|
||||
}
|
||||
|
||||
if (levels.Count <= level)
|
||||
throw new YamlException($"Bad indent in miniyaml at {location}");
|
||||
|
||||
while (levels.Count > level + 1)
|
||||
{
|
||||
levels[^1].TrimExcess();
|
||||
levels.RemoveAt(levels.Count - 1);
|
||||
}
|
||||
|
||||
// Extract key, value, comment from line as `<key>: <value>#<comment>`
|
||||
// The # character is allowed in the value if escaped (\#).
|
||||
// Leading and trailing whitespace is always trimmed from keys.
|
||||
@@ -236,7 +283,7 @@ namespace OpenRA
|
||||
if (commentStart < 0 && line[i] == '#' && (i == 0 || line[i - 1] != '\\'))
|
||||
{
|
||||
commentStart = i + 1;
|
||||
if (commentStart <= keyLength)
|
||||
if (i <= keyStart + keyLength)
|
||||
keyLength = i - keyStart;
|
||||
else
|
||||
valueLength = i - valueStart;
|
||||
@@ -274,6 +321,12 @@ namespace OpenRA
|
||||
|
||||
if (!key.IsEmpty || !discardCommentsAndWhitespace)
|
||||
{
|
||||
if (parsedLines.Count > 0 && parsedLines[^1].Level < level - 1)
|
||||
throw new YamlException($"Bad indent in miniyaml at {location}");
|
||||
|
||||
while (parsedLines.Count > 0 && parsedLines[^1].Level > level)
|
||||
BuildCompletedSubNode(level);
|
||||
|
||||
var keyString = key.IsEmpty ? null : key.ToString();
|
||||
var valueString = value.IsEmpty ? null : value.ToString();
|
||||
|
||||
@@ -281,39 +334,68 @@ namespace OpenRA
|
||||
// (i.e. a lone # at the end of a line) can be correctly re-serialized
|
||||
var commentString = comment == default ? null : comment.ToString();
|
||||
|
||||
keyString = keyString == null ? null : stringPool.GetOrAdd(keyString, keyString);
|
||||
valueString = valueString == null ? null : stringPool.GetOrAdd(valueString, valueString);
|
||||
commentString = commentString == null ? null : stringPool.GetOrAdd(commentString, commentString);
|
||||
keyString = keyString == null ? null : stringPool.GetOrAdd(keyString);
|
||||
valueString = valueString == null ? null : stringPool.GetOrAdd(valueString);
|
||||
commentString = commentString == null ? null : stringPool.GetOrAdd(commentString);
|
||||
|
||||
var nodes = new List<MiniYamlNode>();
|
||||
levels[level].Add(new MiniYamlNode(keyString, valueString, commentString, nodes, location));
|
||||
|
||||
levels.Add(nodes);
|
||||
parsedLines.Add((level, keyString, valueString, commentString, location));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var nodes in levels)
|
||||
nodes.TrimExcess();
|
||||
if (parsedLines.Count > 0)
|
||||
BuildCompletedSubNode(0);
|
||||
|
||||
return levels[0];
|
||||
return result[0];
|
||||
|
||||
void BuildCompletedSubNode(int level)
|
||||
{
|
||||
var lastLevel = parsedLines[^1].Level;
|
||||
while (lastLevel >= result.Count)
|
||||
result.Add(new List<MiniYamlNode>());
|
||||
|
||||
while (parsedLines.Count > 0 && parsedLines[^1].Level >= level)
|
||||
{
|
||||
var parent = parsedLines[^1];
|
||||
var startOfRange = parsedLines.Count - 1;
|
||||
while (startOfRange > 0 && parsedLines[startOfRange - 1].Level == parent.Level)
|
||||
startOfRange--;
|
||||
|
||||
for (var i = startOfRange; i < parsedLines.Count - 1; i++)
|
||||
{
|
||||
var sibling = parsedLines[i];
|
||||
result[parent.Level].Add(
|
||||
new MiniYamlNode(sibling.Key, new MiniYaml(sibling.Value), sibling.Comment, sibling.Location));
|
||||
}
|
||||
|
||||
var childNodes = parent.Level + 1 < result.Count ? result[parent.Level + 1] : null;
|
||||
result[parent.Level].Add(new MiniYamlNode(
|
||||
parent.Key,
|
||||
new MiniYaml(parent.Value, childNodes ?? Enumerable.Empty<MiniYamlNode>()),
|
||||
parent.Comment,
|
||||
parent.Location));
|
||||
childNodes?.Clear();
|
||||
|
||||
parsedLines.RemoveRange(startOfRange, parsedLines.Count - startOfRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
|
||||
public static List<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
|
||||
{
|
||||
return FromStream(File.OpenRead(path), path, discardCommentsAndWhitespace, stringPool);
|
||||
}
|
||||
|
||||
public static List<MiniYamlNode> FromStream(Stream s, string fileName = "<no filename available>", bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
|
||||
public static List<MiniYamlNode> FromStream(Stream s, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
|
||||
{
|
||||
return FromLines(s.ReadAllLinesAsMemory(), fileName, discardCommentsAndWhitespace, stringPool);
|
||||
return FromLines(s.ReadAllLinesAsMemory(), name, discardCommentsAndWhitespace, stringPool);
|
||||
}
|
||||
|
||||
public static List<MiniYamlNode> FromString(string text, string fileName = "<no filename available>", bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
|
||||
public static List<MiniYamlNode> FromString(string text, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
|
||||
{
|
||||
return FromLines(text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(s => s.AsMemory()), fileName, discardCommentsAndWhitespace, stringPool);
|
||||
return FromLines(text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(s => s.AsMemory()), name, discardCommentsAndWhitespace, stringPool);
|
||||
}
|
||||
|
||||
public static List<MiniYamlNode> Merge(IEnumerable<List<MiniYamlNode>> sources)
|
||||
public static List<MiniYamlNode> Merge(IEnumerable<IReadOnlyCollection<MiniYamlNode>> sources)
|
||||
{
|
||||
var sourcesList = sources.ToList();
|
||||
if (sourcesList.Count == 0)
|
||||
@@ -336,7 +418,7 @@ namespace OpenRA
|
||||
}
|
||||
|
||||
// Resolve any top-level removals (e.g. removing whole actor blocks)
|
||||
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)).ToList());
|
||||
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)));
|
||||
return ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty);
|
||||
}
|
||||
|
||||
@@ -345,19 +427,23 @@ namespace OpenRA
|
||||
{
|
||||
if (existingNodeKeys.Add(overrideNode.Key))
|
||||
{
|
||||
existingNodes.Add(overrideNode.Clone());
|
||||
existingNodes.Add(overrideNode);
|
||||
return;
|
||||
}
|
||||
|
||||
var existingNode = existingNodes.Find(n => n.Key == overrideNode.Key);
|
||||
existingNode.Value = MergePartial(existingNode.Value, overrideNode.Value);
|
||||
existingNode.Value.Nodes = ResolveInherits(existingNode.Value, tree, inherited);
|
||||
var existingNodeIndex = IndexOfKey(existingNodes, overrideNode.Key);
|
||||
var existingNode = existingNodes[existingNodeIndex];
|
||||
var value = MergePartial(existingNode.Value, overrideNode.Value);
|
||||
var nodes = ResolveInherits(value, tree, inherited);
|
||||
if (!value.Nodes.SequenceEqual(nodes))
|
||||
value = value.WithNodes(nodes);
|
||||
existingNodes[existingNodeIndex] = existingNode.WithValue(value);
|
||||
}
|
||||
|
||||
static List<MiniYamlNode> ResolveInherits(MiniYaml node, Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
|
||||
{
|
||||
var resolved = new List<MiniYamlNode>(node.Nodes.Count);
|
||||
var resolvedKeys = new HashSet<string>(node.Nodes.Count);
|
||||
var resolved = new List<MiniYamlNode>(node.Nodes.Length);
|
||||
var resolvedKeys = new HashSet<string>(node.Nodes.Length);
|
||||
|
||||
foreach (var n in node.Nodes)
|
||||
{
|
||||
@@ -379,7 +465,7 @@ namespace OpenRA
|
||||
foreach (var r in ResolveInherits(parent, tree, inherited))
|
||||
MergeIntoResolved(r, resolved, resolvedKeys, tree, inherited);
|
||||
}
|
||||
else if (n.Key.StartsWith("-", StringComparison.Ordinal))
|
||||
else if (n.Key.StartsWith('-'))
|
||||
{
|
||||
var removed = n.Key[1..];
|
||||
if (resolved.RemoveAll(r => r.Key == removed) == 0)
|
||||
@@ -390,7 +476,6 @@ namespace OpenRA
|
||||
MergeIntoResolved(n, resolved, resolvedKeys, tree, inherited);
|
||||
}
|
||||
|
||||
resolved.TrimExcess();
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@@ -398,7 +483,7 @@ namespace OpenRA
|
||||
/// Merges any duplicate keys that are defined within the same set of nodes.
|
||||
/// Does not resolve inheritance or node removals.
|
||||
/// </summary>
|
||||
static List<MiniYamlNode> MergeSelfPartial(List<MiniYamlNode> existingNodes)
|
||||
static IReadOnlyCollection<MiniYamlNode> MergeSelfPartial(IReadOnlyCollection<MiniYamlNode> existingNodes)
|
||||
{
|
||||
var keys = new HashSet<string>(existingNodes.Count);
|
||||
var ret = new List<MiniYamlNode>(existingNodes.Count);
|
||||
@@ -409,12 +494,12 @@ namespace OpenRA
|
||||
else
|
||||
{
|
||||
// Node with the same key has already been added: merge new node over the existing one
|
||||
var original = ret.First(r => r.Key == n.Key);
|
||||
original.Value = MergePartial(original.Value, n.Value);
|
||||
var originalIndex = IndexOfKey(ret, n.Key);
|
||||
var original = ret[originalIndex];
|
||||
ret[originalIndex] = original.WithValue(MergePartial(original.Value, n.Value));
|
||||
}
|
||||
}
|
||||
|
||||
ret.TrimExcess();
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -432,7 +517,7 @@ namespace OpenRA
|
||||
return new MiniYaml(overrideNodes.Value ?? existingNodes.Value, MergePartial(existingNodes.Nodes, overrideNodes.Nodes));
|
||||
}
|
||||
|
||||
static List<MiniYamlNode> MergePartial(List<MiniYamlNode> existingNodes, List<MiniYamlNode> overrideNodes)
|
||||
static IReadOnlyCollection<MiniYamlNode> MergePartial(IReadOnlyCollection<MiniYamlNode> existingNodes, IReadOnlyCollection<MiniYamlNode> overrideNodes)
|
||||
{
|
||||
if (existingNodes.Count == 0)
|
||||
return overrideNodes;
|
||||
@@ -452,7 +537,7 @@ namespace OpenRA
|
||||
{
|
||||
// Append Removal nodes to the result.
|
||||
// Therefore: we know the remainder of the method deals with a plain node.
|
||||
if (node.Key.StartsWith("-", StringComparison.Ordinal))
|
||||
if (node.Key.StartsWith('-'))
|
||||
{
|
||||
ret.Add(node);
|
||||
return;
|
||||
@@ -468,9 +553,8 @@ namespace OpenRA
|
||||
// A Removal node is closer than the previous node.
|
||||
// We should not merge the new node, as the data being merged will jump before the Removal.
|
||||
// Instead, append it so the previous node is applied, then removed, then the new node is applied.
|
||||
var removalKey = $"-{node.Key}";
|
||||
var previousNodeIndex = ret.FindLastIndex(n => n.Key == node.Key);
|
||||
var previousRemovalNodeIndex = ret.FindLastIndex(n => n.Key == removalKey);
|
||||
var previousNodeIndex = LastIndexOfKey(ret, node.Key);
|
||||
var previousRemovalNodeIndex = LastIndexOfKey(ret, $"-{node.Key}");
|
||||
if (previousRemovalNodeIndex != -1 && previousRemovalNodeIndex > previousNodeIndex)
|
||||
{
|
||||
ret.Add(node);
|
||||
@@ -479,13 +563,30 @@ namespace OpenRA
|
||||
|
||||
// A previous node is present with no intervening Removal.
|
||||
// We should merge the new one into it, in place.
|
||||
ret[previousNodeIndex] = new MiniYamlNode(node.Key, MergePartial(ret[previousNodeIndex].Value, node.Value), node.Comment, node.Location);
|
||||
ret[previousNodeIndex] = node.WithValue(MergePartial(ret[previousNodeIndex].Value, node.Value));
|
||||
}
|
||||
|
||||
ret.TrimExcess();
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int IndexOfKey(List<MiniYamlNode> nodes, string key)
|
||||
{
|
||||
// PERF: Avoid LINQ.
|
||||
for (var i = 0; i < nodes.Count; i++)
|
||||
if (nodes[i].Key == key)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int LastIndexOfKey(List<MiniYamlNode> nodes, string key)
|
||||
{
|
||||
// PERF: Avoid LINQ.
|
||||
for (var i = nodes.Count - 1; i >= 0; i--)
|
||||
if (nodes[i].Key == key)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public IEnumerable<string> ToLines(string key, string comment = null)
|
||||
{
|
||||
var hasKey = !string.IsNullOrEmpty(key);
|
||||
@@ -508,14 +609,100 @@ namespace OpenRA
|
||||
files = files.Append(mapFiles);
|
||||
}
|
||||
|
||||
var yaml = files.Select(s => FromStream(fileSystem.Open(s), s));
|
||||
if (mapRules != null && mapRules.Nodes.Count > 0)
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
IEnumerable<IReadOnlyCollection<MiniYamlNode>> yaml = files.Select(s => FromStream(fileSystem.Open(s), s, stringPool: stringPool));
|
||||
if (mapRules != null && mapRules.Nodes.Length > 0)
|
||||
yaml = yaml.Append(mapRules.Nodes);
|
||||
|
||||
return Merge(yaml);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MiniYamlNodeBuilder
|
||||
{
|
||||
public MiniYamlNode.SourceLocation Location;
|
||||
public string Key;
|
||||
public MiniYamlBuilder Value;
|
||||
public string Comment;
|
||||
|
||||
public MiniYamlNodeBuilder(MiniYamlNode node)
|
||||
{
|
||||
Location = node.Location;
|
||||
Key = node.Key;
|
||||
Value = new MiniYamlBuilder(node.Value);
|
||||
Comment = node.Comment;
|
||||
}
|
||||
|
||||
public MiniYamlNodeBuilder(string k, MiniYamlBuilder v, string c = null)
|
||||
{
|
||||
Key = k;
|
||||
Value = v;
|
||||
Comment = c;
|
||||
}
|
||||
|
||||
public MiniYamlNodeBuilder(string k, MiniYamlBuilder v, string c, MiniYamlNode.SourceLocation loc)
|
||||
: this(k, v, c)
|
||||
{
|
||||
Location = loc;
|
||||
}
|
||||
|
||||
public MiniYamlNodeBuilder(string k, string v, string c = null)
|
||||
: this(k, new MiniYamlBuilder(v, null), c) { }
|
||||
|
||||
public MiniYamlNodeBuilder(string k, string v, List<MiniYamlNode> n)
|
||||
: this(k, new MiniYamlBuilder(v, n), null) { }
|
||||
|
||||
public MiniYamlNode Build()
|
||||
{
|
||||
return new MiniYamlNode(Key, Value.Build(), Comment, Location);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MiniYamlBuilder
|
||||
{
|
||||
public string Value;
|
||||
public List<MiniYamlNodeBuilder> Nodes;
|
||||
|
||||
public MiniYamlBuilder(MiniYaml yaml)
|
||||
{
|
||||
Value = yaml.Value;
|
||||
Nodes = yaml.Nodes.Select(n => new MiniYamlNodeBuilder(n)).ToList();
|
||||
}
|
||||
|
||||
public MiniYamlBuilder(string value)
|
||||
: this(value, null) { }
|
||||
|
||||
public MiniYamlBuilder(string value, List<MiniYamlNode> nodes)
|
||||
{
|
||||
Value = value;
|
||||
Nodes = nodes == null ? new List<MiniYamlNodeBuilder>() : nodes.ConvertAll(x => new MiniYamlNodeBuilder(x));
|
||||
}
|
||||
|
||||
public MiniYaml Build()
|
||||
{
|
||||
return new MiniYaml(Value, Nodes.Select(n => n.Build()));
|
||||
}
|
||||
|
||||
public IEnumerable<string> ToLines(string key, string comment = null)
|
||||
{
|
||||
var hasKey = !string.IsNullOrEmpty(key);
|
||||
var hasValue = !string.IsNullOrEmpty(Value);
|
||||
var hasComment = comment != null;
|
||||
yield return (hasKey ? key + ":" : "")
|
||||
+ (hasValue ? " " + Value.Replace("#", "\\#") : "")
|
||||
+ (hasComment ? (hasKey || hasValue ? " " : "") + "#" + comment : "");
|
||||
|
||||
if (Nodes != null)
|
||||
foreach (var line in Nodes.ToLines())
|
||||
yield return "\t" + line;
|
||||
}
|
||||
|
||||
public MiniYamlNodeBuilder NodeWithKeyOrDefault(string key)
|
||||
{
|
||||
return Nodes.SingleOrDefault(n => n.Key == key);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class YamlException : Exception
|
||||
{
|
||||
|
||||
@@ -33,7 +33,6 @@ namespace OpenRA
|
||||
public readonly ISpriteLoader[] SpriteLoaders;
|
||||
public readonly ITerrainLoader TerrainLoader;
|
||||
public readonly ISpriteSequenceLoader SpriteSequenceLoader;
|
||||
public readonly IModelSequenceLoader ModelSequenceLoader;
|
||||
public readonly IVideoLoader[] VideoLoaders;
|
||||
public readonly HotkeyManager Hotkeys;
|
||||
public ILoadScreen LoadScreen { get; }
|
||||
@@ -90,15 +89,6 @@ namespace OpenRA
|
||||
|
||||
SpriteSequenceLoader = (ISpriteSequenceLoader)sequenceCtor.Invoke(new[] { this });
|
||||
|
||||
var modelFormat = Manifest.Get<ModelSequenceFormat>();
|
||||
var modelLoader = ObjectCreator.FindType(modelFormat.Type + "Loader");
|
||||
var modelCtor = modelLoader?.GetConstructor(new[] { typeof(ModData) });
|
||||
if (modelLoader == null || !modelLoader.GetInterfaces().Contains(typeof(IModelSequenceLoader)) || modelCtor == null)
|
||||
throw new InvalidOperationException($"Unable to find a model loader for type '{modelFormat.Type}'.");
|
||||
|
||||
ModelSequenceLoader = (IModelSequenceLoader)modelCtor.Invoke(new[] { this });
|
||||
ModelSequenceLoader.OnMissingModelError = s => Log.Write("debug", s);
|
||||
|
||||
Hotkeys = new HotkeyManager(ModFiles, Game.Settings.Keys, Manifest);
|
||||
|
||||
defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this));
|
||||
@@ -168,7 +158,8 @@ namespace OpenRA
|
||||
|
||||
public List<MiniYamlNode>[] GetRulesYaml()
|
||||
{
|
||||
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s)).ToArray();
|
||||
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
|
||||
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s, stringPool: stringPool)).ToArray();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -260,15 +260,15 @@ namespace OpenRA.Network
|
||||
try
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
ms.WriteArray(BitConverter.GetBytes(packet.Length));
|
||||
ms.WriteArray(packet);
|
||||
ms.Write(packet.Length);
|
||||
ms.Write(packet);
|
||||
|
||||
foreach (var s in queuedSyncPackets)
|
||||
{
|
||||
var q = OrderIO.SerializeSync(s);
|
||||
|
||||
ms.WriteArray(BitConverter.GetBytes(q.Length));
|
||||
ms.WriteArray(q);
|
||||
ms.Write(q.Length);
|
||||
ms.Write(q);
|
||||
|
||||
sentSync.Enqueue(s);
|
||||
}
|
||||
|
||||
@@ -122,10 +122,10 @@ namespace OpenRA.Network
|
||||
LastSyncFrame = rs.ReadInt32();
|
||||
lastSyncPacket = rs.ReadBytes(Order.SyncHashOrderLength);
|
||||
|
||||
var globalSettings = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
|
||||
var globalSettings = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:globalSettings");
|
||||
GlobalSettings = Session.Global.Deserialize(globalSettings[0].Value);
|
||||
|
||||
var slots = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
|
||||
var slots = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:slots");
|
||||
Slots = new Dictionary<string, Session.Slot>();
|
||||
foreach (var s in slots)
|
||||
{
|
||||
@@ -133,7 +133,7 @@ namespace OpenRA.Network
|
||||
Slots.Add(slot.PlayerReference, slot);
|
||||
}
|
||||
|
||||
var slotClients = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
|
||||
var slotClients = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:slotClients");
|
||||
SlotClients = new Dictionary<string, SlotClient>();
|
||||
foreach (var s in slotClients)
|
||||
{
|
||||
@@ -144,9 +144,9 @@ namespace OpenRA.Network
|
||||
if (rs.Position != traitDataOffset || rs.ReadInt32() != TraitDataMarker)
|
||||
throw new InvalidDataException("Invalid orasav file");
|
||||
|
||||
var traitData = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
|
||||
var traitData = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:traitData");
|
||||
foreach (var td in traitData)
|
||||
TraitData.Add(int.Parse(td.Key), td.Value);
|
||||
TraitData.Add(Exts.ParseInt32Invariant(td.Key), td.Value);
|
||||
|
||||
rs.Seek(0, SeekOrigin.Begin);
|
||||
ordersStream.Write(rs.ReadBytes(metadataOffset), 0, metadataOffset);
|
||||
@@ -226,10 +226,10 @@ namespace OpenRA.Network
|
||||
clientSlot = firstBotSlotIndex;
|
||||
}
|
||||
|
||||
ordersStream.WriteArray(BitConverter.GetBytes(data.Length + 8));
|
||||
ordersStream.WriteArray(BitConverter.GetBytes(frame));
|
||||
ordersStream.WriteArray(BitConverter.GetBytes(clientSlot));
|
||||
ordersStream.WriteArray(data);
|
||||
ordersStream.Write(data.Length + 8);
|
||||
ordersStream.Write(frame);
|
||||
ordersStream.Write(clientSlot);
|
||||
ordersStream.Write(data);
|
||||
LastOrdersFrame = frame;
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ namespace OpenRA.Network
|
||||
// Send the trait data first to guarantee that it is available when needed
|
||||
foreach (var kv in TraitData)
|
||||
{
|
||||
var data = new List<MiniYamlNode>() { new MiniYamlNode(kv.Key.ToString(), kv.Value) }.WriteToString();
|
||||
var data = new List<MiniYamlNode>() { new(kv.Key.ToStringInvariant(), kv.Value) }.WriteToString();
|
||||
packetFn(0, 0, Order.FromTargetString("SaveTraitData", data, true).Serialize());
|
||||
}
|
||||
|
||||
@@ -288,35 +288,35 @@ namespace OpenRA.Network
|
||||
{
|
||||
ordersStream.Seek(0, SeekOrigin.Begin);
|
||||
ordersStream.CopyTo(file);
|
||||
file.Write(BitConverter.GetBytes(MetadataMarker), 0, 4);
|
||||
file.Write(BitConverter.GetBytes(LastOrdersFrame), 0, 4);
|
||||
file.Write(BitConverter.GetBytes(LastSyncFrame), 0, 4);
|
||||
file.Write(MetadataMarker);
|
||||
file.Write(LastOrdersFrame);
|
||||
file.Write(LastSyncFrame);
|
||||
file.Write(lastSyncPacket, 0, Order.SyncHashOrderLength);
|
||||
|
||||
var globalSettingsNodes = new List<MiniYamlNode>() { GlobalSettings.Serialize() };
|
||||
file.WriteString(Encoding.UTF8, globalSettingsNodes.WriteToString());
|
||||
file.WriteLengthPrefixedString(Encoding.UTF8, globalSettingsNodes.WriteToString());
|
||||
|
||||
var slotNodes = Slots
|
||||
.Select(s => s.Value.Serialize())
|
||||
.ToList();
|
||||
file.WriteString(Encoding.UTF8, slotNodes.WriteToString());
|
||||
file.WriteLengthPrefixedString(Encoding.UTF8, slotNodes.WriteToString());
|
||||
|
||||
var slotClientNodes = SlotClients
|
||||
.Select(s => s.Value.Serialize(s.Key))
|
||||
.ToList();
|
||||
file.WriteString(Encoding.UTF8, slotClientNodes.WriteToString());
|
||||
file.WriteLengthPrefixedString(Encoding.UTF8, slotClientNodes.WriteToString());
|
||||
|
||||
var traitDataOffset = file.Length;
|
||||
file.Write(BitConverter.GetBytes(TraitDataMarker), 0, 4);
|
||||
file.Write(TraitDataMarker);
|
||||
|
||||
var traitDataNodes = TraitData
|
||||
.Select(kv => new MiniYamlNode(kv.Key.ToString(), kv.Value))
|
||||
.Select(kv => new MiniYamlNode(kv.Key.ToStringInvariant(), kv.Value))
|
||||
.ToList();
|
||||
file.WriteString(Encoding.UTF8, traitDataNodes.WriteToString());
|
||||
file.WriteLengthPrefixedString(Encoding.UTF8, traitDataNodes.WriteToString());
|
||||
|
||||
file.Write(BitConverter.GetBytes(ordersStream.Length), 0, 4);
|
||||
file.Write(BitConverter.GetBytes(traitDataOffset), 0, 4);
|
||||
file.Write(BitConverter.GetBytes(EOFMarker), 0, 4);
|
||||
file.Write((int)ordersStream.Length);
|
||||
file.Write((int)traitDataOffset);
|
||||
file.Write(EOFMarker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace OpenRA.Network
|
||||
static object LoadClients(MiniYaml yaml)
|
||||
{
|
||||
var clients = new List<GameClient>();
|
||||
var clientsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Clients");
|
||||
var clientsNode = yaml.NodeWithKeyOrDefault("Clients");
|
||||
if (clientsNode != null)
|
||||
{
|
||||
var regex = new Regex(@"Client@\d+");
|
||||
@@ -159,7 +159,7 @@ namespace OpenRA.Network
|
||||
// Games advertised using the old API used a single Mods field
|
||||
if (Mod == null || Version == null)
|
||||
{
|
||||
var modsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Mods");
|
||||
var modsNode = yaml.NodeWithKeyOrDefault("Mods");
|
||||
if (modsNode != null)
|
||||
{
|
||||
var modVersion = modsNode.Value.Value.Split('@');
|
||||
@@ -169,9 +169,8 @@ namespace OpenRA.Network
|
||||
}
|
||||
|
||||
// Games advertised using the old API calculated the play time locally
|
||||
if (State == 2 && PlayTime < 0)
|
||||
if (DateTime.TryParse(Started, out var startTime))
|
||||
PlayTime = (int)(DateTime.UtcNow - startTime).TotalSeconds;
|
||||
if (State == 2 && PlayTime < 0 && DateTime.TryParse(Started, out var startTime))
|
||||
PlayTime = (int)(DateTime.UtcNow - startTime).TotalSeconds;
|
||||
|
||||
var externalKey = ExternalMod.MakeKey(Mod, Version);
|
||||
if (Game.ExternalMods.TryGetValue(externalKey, out var external) && external.Version == Version)
|
||||
@@ -217,7 +216,7 @@ namespace OpenRA.Network
|
||||
Name = server.Settings.Name;
|
||||
|
||||
// IP address will be replaced with a real value by the master server / receiving LAN client
|
||||
Address = "0.0.0.0:" + server.Settings.ListenPort.ToString();
|
||||
Address = "0.0.0.0:" + server.Settings.ListenPort.ToStringInvariant();
|
||||
State = (int)server.State;
|
||||
MaxPlayers = server.LobbyInfo.Slots.Count(s => !s.Value.Closed) - server.LobbyInfo.Clients.Count(c1 => c1.Bot != null);
|
||||
Map = server.Map.Uid;
|
||||
@@ -234,7 +233,7 @@ namespace OpenRA.Network
|
||||
|
||||
public string ToPOSTData(bool lanGame)
|
||||
{
|
||||
var root = new List<MiniYamlNode>() { new MiniYamlNode("Protocol", ProtocolVersion.ToString()) };
|
||||
var root = new List<MiniYamlNode>() { new("Protocol", ProtocolVersion.ToStringInvariant()) };
|
||||
foreach (var field in SerializeFields)
|
||||
root.Add(FieldSaver.SaveField(this, field));
|
||||
|
||||
@@ -243,18 +242,16 @@ namespace OpenRA.Network
|
||||
// Add fields that are normally generated by the master server
|
||||
// LAN games overload the Id with a GUID string (rather than an ID) to allow deduplication
|
||||
root.Add(new MiniYamlNode("Id", Platform.SessionGUID.ToString()));
|
||||
root.Add(new MiniYamlNode("Players", Clients.Count(c => !c.IsBot && !c.IsSpectator).ToString()));
|
||||
root.Add(new MiniYamlNode("Spectators", Clients.Count(c => c.IsSpectator).ToString()));
|
||||
root.Add(new MiniYamlNode("Bots", Clients.Count(c => c.IsBot).ToString()));
|
||||
root.Add(new MiniYamlNode("Players", Clients.Count(c => !c.IsBot && !c.IsSpectator).ToStringInvariant()));
|
||||
root.Add(new MiniYamlNode("Spectators", Clients.Count(c => c.IsSpectator).ToStringInvariant()));
|
||||
root.Add(new MiniYamlNode("Bots", Clients.Count(c => c.IsBot).ToStringInvariant()));
|
||||
|
||||
// Included for backwards compatibility with older clients that don't support separated Mod/Version.
|
||||
root.Add(new MiniYamlNode("Mods", Mod + "@" + Version));
|
||||
}
|
||||
|
||||
var clientsNode = new MiniYaml("");
|
||||
var i = 0;
|
||||
foreach (var c in Clients)
|
||||
clientsNode.Nodes.Add(new MiniYamlNode("Client@" + i++.ToString(), FieldSaver.Save(c)));
|
||||
var clientsNode = new MiniYaml("", Clients.Select((c, i) =>
|
||||
new MiniYamlNode("Client@" + i, FieldSaver.Save(c))));
|
||||
|
||||
root.Add(new MiniYamlNode("Clients", clientsNode));
|
||||
return new MiniYaml("", root)
|
||||
|
||||
@@ -20,16 +20,16 @@ namespace OpenRA.Network
|
||||
public string Version;
|
||||
public string AuthToken;
|
||||
|
||||
public static HandshakeRequest Deserialize(string data)
|
||||
public static HandshakeRequest Deserialize(string data, string name)
|
||||
{
|
||||
var handshake = new HandshakeRequest();
|
||||
FieldLoader.Load(handshake, MiniYaml.FromString(data).First().Value);
|
||||
FieldLoader.Load(handshake, MiniYaml.FromString(data, name).First().Value);
|
||||
return handshake;
|
||||
}
|
||||
|
||||
public string Serialize()
|
||||
{
|
||||
var data = new List<MiniYamlNode> { new MiniYamlNode("Handshake", FieldSaver.Save(this)) };
|
||||
var data = new List<MiniYamlNode> { new("Handshake", FieldSaver.Save(this)) };
|
||||
return data.WriteToString();
|
||||
}
|
||||
}
|
||||
@@ -51,14 +51,14 @@ namespace OpenRA.Network
|
||||
[FieldLoader.Ignore]
|
||||
public Session.Client Client;
|
||||
|
||||
public static HandshakeResponse Deserialize(string data)
|
||||
public static HandshakeResponse Deserialize(string data, string name)
|
||||
{
|
||||
var handshake = new HandshakeResponse
|
||||
{
|
||||
Client = new Session.Client()
|
||||
};
|
||||
|
||||
var ys = MiniYaml.FromString(data);
|
||||
var ys = MiniYaml.FromString(data, name);
|
||||
foreach (var y in ys)
|
||||
{
|
||||
switch (y.Key)
|
||||
@@ -79,9 +79,9 @@ namespace OpenRA.Network
|
||||
{
|
||||
var data = new List<MiniYamlNode>
|
||||
{
|
||||
new MiniYamlNode("Handshake", null,
|
||||
new("Handshake", null,
|
||||
new[] { "Mod", "Version", "Password", "Fingerprint", "AuthSignature", "OrdersProtocol" }.Select(p => FieldSaver.SaveField(this, p)).ToList()),
|
||||
new MiniYamlNode("Client", FieldSaver.Save(Client))
|
||||
new("Client", FieldSaver.Save(Client))
|
||||
};
|
||||
|
||||
return data.WriteToString();
|
||||
|
||||
@@ -9,10 +9,8 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Linguini.Shared.Types.Bundle;
|
||||
|
||||
namespace OpenRA.Network
|
||||
@@ -57,61 +55,51 @@ namespace OpenRA.Network
|
||||
public readonly string Key = string.Empty;
|
||||
|
||||
[FieldLoader.LoadUsing(nameof(LoadArguments))]
|
||||
public readonly FluentArgument[] Arguments = Array.Empty<FluentArgument>();
|
||||
|
||||
public string TranslatedText { get; }
|
||||
public readonly Dictionary<string, object> Arguments;
|
||||
|
||||
static object LoadArguments(MiniYaml yaml)
|
||||
{
|
||||
var arguments = new List<FluentArgument>();
|
||||
var argumentsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Arguments");
|
||||
var arguments = new Dictionary<string, object>();
|
||||
var argumentsNode = yaml.NodeWithKeyOrDefault("Arguments");
|
||||
if (argumentsNode != null)
|
||||
{
|
||||
var regex = new Regex(@"Argument@\d+");
|
||||
foreach (var argument in argumentsNode.Value.Nodes)
|
||||
if (regex.IsMatch(argument.Key))
|
||||
arguments.Add(FieldLoader.Load<FluentArgument>(argument.Value));
|
||||
foreach (var argumentNode in argumentsNode.Value.Nodes)
|
||||
{
|
||||
var argument = FieldLoader.Load<FluentArgument>(argumentNode.Value);
|
||||
if (argument.Type == FluentArgument.FluentArgumentType.Number)
|
||||
{
|
||||
if (!double.TryParse(argument.Value, out var number))
|
||||
Log.Write("debug", $"Failed to parse {argument.Value}");
|
||||
|
||||
arguments.Add(argument.Key, number);
|
||||
}
|
||||
else
|
||||
arguments.Add(argument.Key, argument.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return arguments.ToArray();
|
||||
return arguments;
|
||||
}
|
||||
|
||||
public LocalizedMessage(MiniYaml yaml)
|
||||
{
|
||||
// Let the FieldLoader do the dirty work of loading the public fields.
|
||||
FieldLoader.Load(this, yaml);
|
||||
|
||||
var argumentDictionary = new Dictionary<string, object>();
|
||||
foreach (var argument in Arguments)
|
||||
{
|
||||
if (argument.Type == FluentArgument.FluentArgumentType.Number)
|
||||
{
|
||||
if (!double.TryParse(argument.Value, out var number))
|
||||
Log.Write("debug", $"Failed to parse {argument.Value}");
|
||||
|
||||
argumentDictionary.Add(argument.Key, number);
|
||||
}
|
||||
else
|
||||
argumentDictionary.Add(argument.Key, argument.Value);
|
||||
}
|
||||
|
||||
TranslatedText = TranslationProvider.GetString(Key, argumentDictionary);
|
||||
}
|
||||
|
||||
public static string Serialize(string key, Dictionary<string, object> arguments = null)
|
||||
{
|
||||
var root = new List<MiniYamlNode>
|
||||
{
|
||||
new MiniYamlNode("Protocol", ProtocolVersion.ToString()),
|
||||
new MiniYamlNode("Key", key)
|
||||
new("Protocol", ProtocolVersion.ToStringInvariant()),
|
||||
new("Key", key)
|
||||
};
|
||||
|
||||
if (arguments != null)
|
||||
{
|
||||
var argumentsNode = new MiniYaml("");
|
||||
var i = 0;
|
||||
foreach (var argument in arguments.Select(a => new FluentArgument(a.Key, a.Value)))
|
||||
argumentsNode.Nodes.Add(new MiniYamlNode("Argument@" + i++, FieldSaver.Save(argument)));
|
||||
var argumentsNode = new MiniYaml("", arguments
|
||||
.Select(a => new FluentArgument(a.Key, a.Value))
|
||||
.Select((argument, i) => new MiniYamlNode("Argument@" + i, FieldSaver.Save(argument))));
|
||||
|
||||
root.Add(new MiniYamlNode("Arguments", argumentsNode));
|
||||
}
|
||||
|
||||
@@ -156,7 +156,18 @@ namespace OpenRA
|
||||
else
|
||||
{
|
||||
var pos = new WPos(r.ReadInt32(), r.ReadInt32(), r.ReadInt32());
|
||||
target = Target.FromPos(pos);
|
||||
|
||||
var numberOfTerrainPositions = r.ReadInt16();
|
||||
if (numberOfTerrainPositions == -1)
|
||||
target = Target.FromPos(pos);
|
||||
else
|
||||
{
|
||||
var terrainPositions = new WPos[numberOfTerrainPositions];
|
||||
for (var i = 0; i < numberOfTerrainPositions; i++)
|
||||
terrainPositions[i] = new WPos(r.ReadInt32(), r.ReadInt32(), r.ReadInt32());
|
||||
|
||||
target = Target.FromSerializedTerrainPosition(pos, terrainPositions);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -388,6 +399,21 @@ namespace OpenRA
|
||||
w.Write(targetState.Pos.X);
|
||||
w.Write(targetState.Pos.Y);
|
||||
w.Write(targetState.Pos.Z);
|
||||
|
||||
// Don't send extra data over the network that will be restored by the Target ctor
|
||||
var terrainPositions = targetState.TerrainPositions.Length;
|
||||
if (terrainPositions == 1 && targetState.TerrainPositions[0] == targetState.Pos)
|
||||
w.Write((short)-1);
|
||||
else
|
||||
{
|
||||
w.Write((short)terrainPositions);
|
||||
foreach (var position in targetState.TerrainPositions)
|
||||
{
|
||||
w.Write(position.X);
|
||||
w.Write(position.Y);
|
||||
w.Write(position.Z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace OpenRA.Network
|
||||
// the Order objects directly on the local client.
|
||||
data = new MemoryStream();
|
||||
foreach (var o in orders)
|
||||
data.WriteArray(o.Serialize());
|
||||
data.Write(o.Serialize());
|
||||
}
|
||||
|
||||
public OrderPacket(MemoryStream data)
|
||||
@@ -55,7 +55,7 @@ namespace OpenRA.Network
|
||||
public byte[] Serialize(int frame)
|
||||
{
|
||||
var ms = new MemoryStream((int)data.Length + 4);
|
||||
ms.WriteArray(BitConverter.GetBytes(frame));
|
||||
ms.Write(frame);
|
||||
|
||||
data.Position = 0;
|
||||
data.CopyTo(ms);
|
||||
@@ -83,19 +83,19 @@ namespace OpenRA.Network
|
||||
public static byte[] SerializeSync((int Frame, int SyncHash, ulong DefeatState) data)
|
||||
{
|
||||
var ms = new MemoryStream(4 + Order.SyncHashOrderLength);
|
||||
ms.WriteArray(BitConverter.GetBytes(data.Frame));
|
||||
ms.Write(data.Frame);
|
||||
ms.WriteByte((byte)OrderType.SyncHash);
|
||||
ms.WriteArray(BitConverter.GetBytes(data.SyncHash));
|
||||
ms.WriteArray(BitConverter.GetBytes(data.DefeatState));
|
||||
ms.Write(data.SyncHash);
|
||||
ms.Write(data.DefeatState);
|
||||
return ms.GetBuffer();
|
||||
}
|
||||
|
||||
public static byte[] SerializePingResponse(long timestamp, byte queueLength)
|
||||
{
|
||||
var ms = new MemoryStream(14);
|
||||
ms.WriteArray(BitConverter.GetBytes(0));
|
||||
ms.Write(0);
|
||||
ms.WriteByte((byte)OrderType.Ping);
|
||||
ms.WriteArray(BitConverter.GetBytes(timestamp));
|
||||
ms.Write(timestamp);
|
||||
ms.WriteByte(queueLength);
|
||||
return ms.GetBuffer();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace OpenRA.Network
|
||||
{
|
||||
const OrderPacket ClientDisconnected = null;
|
||||
|
||||
[TranslationReference("frame")]
|
||||
const string DesyncCompareLogs = "notification-desync-compare-logs";
|
||||
|
||||
readonly SyncReport syncReport;
|
||||
readonly Dictionary<int, Queue<(int Frame, OrderPacket Orders)>> pendingOrders = new();
|
||||
readonly Dictionary<int, (int SyncHash, ulong DefeatState)> syncForFrame = new();
|
||||
@@ -36,6 +39,9 @@ namespace OpenRA.Network
|
||||
public string ServerError = null;
|
||||
public bool AuthenticationFailed = false;
|
||||
|
||||
// The default null means "no map restriction" while an empty set means "all maps restricted"
|
||||
public HashSet<string> ServerMapPool = null;
|
||||
|
||||
public int NetFrameNumber { get; private set; }
|
||||
public int LocalFrameNumber;
|
||||
|
||||
@@ -70,7 +76,7 @@ namespace OpenRA.Network
|
||||
public int Client;
|
||||
public Order Order;
|
||||
|
||||
public override string ToString()
|
||||
public override readonly string ToString()
|
||||
{
|
||||
return $"ClientId: {Client} {Order}";
|
||||
}
|
||||
@@ -85,7 +91,7 @@ namespace OpenRA.Network
|
||||
World.OutOfSync();
|
||||
IsOutOfSync = true;
|
||||
|
||||
TextNotificationsManager.AddSystemLine($"Out of sync in frame {frame}.\nCompare syncreport.log with other players.");
|
||||
TextNotificationsManager.AddSystemLine(DesyncCompareLogs, Translation.Arguments("frame", frame));
|
||||
}
|
||||
|
||||
public void StartGame()
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace OpenRA.Network
|
||||
if (o.OrderString == "StartGame")
|
||||
IsValid = true;
|
||||
else if (o.OrderString == "SyncInfo" && !IsValid)
|
||||
LobbyInfo = Session.Deserialize(o.TargetString);
|
||||
LobbyInfo = Session.Deserialize(o.TargetString, o.OrderString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace OpenRA.Network
|
||||
}
|
||||
}
|
||||
|
||||
file.WriteArray(initialContent);
|
||||
file.Write(initialContent);
|
||||
writer = new BinaryWriter(file);
|
||||
}
|
||||
|
||||
@@ -92,8 +92,8 @@ namespace OpenRA.Network
|
||||
public void ReceiveFrame(int clientID, int frame, byte[] data)
|
||||
{
|
||||
var ms = new MemoryStream(4 + data.Length);
|
||||
ms.WriteArray(BitConverter.GetBytes(frame));
|
||||
ms.WriteArray(data);
|
||||
ms.Write(frame);
|
||||
ms.Write(data);
|
||||
Receive(clientID, ms.GetBuffer());
|
||||
}
|
||||
|
||||
|
||||
@@ -41,13 +41,13 @@ namespace OpenRA.Network
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Session Deserialize(string data)
|
||||
public static Session Deserialize(string data, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
var session = new Session();
|
||||
|
||||
var nodes = MiniYaml.FromString(data);
|
||||
var nodes = MiniYaml.FromString(data, name);
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var strings = node.Key.Split('@');
|
||||
@@ -227,7 +227,7 @@ namespace OpenRA.Network
|
||||
{
|
||||
var gs = FieldLoader.Load<Global>(data);
|
||||
|
||||
var optionsNode = data.Nodes.FirstOrDefault(n => n.Key == "Options");
|
||||
var optionsNode = data.NodeWithKeyOrDefault("Options");
|
||||
if (optionsNode != null)
|
||||
foreach (var n in optionsNode.Value.Nodes)
|
||||
gs.LobbyOptions[n.Key] = FieldLoader.Load<LobbyOptionState>(n.Value);
|
||||
@@ -238,8 +238,9 @@ namespace OpenRA.Network
|
||||
public MiniYamlNode Serialize()
|
||||
{
|
||||
var data = new MiniYamlNode("GlobalSettings", FieldSaver.Save(this));
|
||||
var options = LobbyOptions.Select(kv => new MiniYamlNode(kv.Key, FieldSaver.Save(kv.Value))).ToList();
|
||||
data.Value.Nodes.Add(new MiniYamlNode("Options", new MiniYaml(null, options)));
|
||||
var options = LobbyOptions.Select(kv => new MiniYamlNode(kv.Key, FieldSaver.Save(kv.Value)));
|
||||
data = data.WithValue(data.Value.WithNodesAppended(
|
||||
new[] { new MiniYamlNode("Options", new MiniYaml(null, options)) }));
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -264,7 +265,7 @@ namespace OpenRA.Network
|
||||
{
|
||||
var sessionData = new List<MiniYamlNode>()
|
||||
{
|
||||
new MiniYamlNode("DisabledSpawnPoints", FieldSaver.FormatValue(DisabledSpawnPoints))
|
||||
new("DisabledSpawnPoints", FieldSaver.FormatValue(DisabledSpawnPoints))
|
||||
};
|
||||
|
||||
foreach (var client in Clients)
|
||||
|
||||
@@ -201,8 +201,12 @@ namespace OpenRA.Network
|
||||
public TypeInfo(Type type)
|
||||
{
|
||||
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
var fields = type.GetFields(Flags).Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>());
|
||||
var properties = type.GetProperties(Flags).Where(pi => pi.HasAttribute<SyncAttribute>());
|
||||
var fields = type.GetFields(Flags)
|
||||
.Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>())
|
||||
.ToList();
|
||||
var properties = type.GetProperties(Flags)
|
||||
.Where(pi => pi.HasAttribute<SyncAttribute>())
|
||||
.ToList();
|
||||
|
||||
foreach (var prop in properties)
|
||||
if (!prop.CanRead || prop.GetIndexParameters().Length > 0)
|
||||
@@ -300,7 +304,7 @@ namespace OpenRA.Network
|
||||
|
||||
public object this[int index]
|
||||
{
|
||||
get
|
||||
readonly get
|
||||
{
|
||||
if (item2OrSentinel == Sentinel)
|
||||
return ((object[])item1OrArray)[index];
|
||||
|
||||
@@ -20,6 +20,24 @@ namespace OpenRA.Network
|
||||
{
|
||||
public const int ChatMessageMaxLength = 2500;
|
||||
|
||||
[TranslationReference("player")]
|
||||
const string Joined = "notification-joined";
|
||||
|
||||
[TranslationReference("player")]
|
||||
const string Left = "notification-lobby-disconnected";
|
||||
|
||||
[TranslationReference]
|
||||
const string GameStarted = "notification-game-has-started";
|
||||
|
||||
[TranslationReference]
|
||||
const string GameSaved = "notification-game-saved";
|
||||
|
||||
[TranslationReference("player")]
|
||||
const string GamePaused = "notification-game-paused";
|
||||
|
||||
[TranslationReference("player")]
|
||||
const string GameUnpaused = "notification-game-unpaused";
|
||||
|
||||
public static int? KickVoteTarget { get; internal set; }
|
||||
|
||||
static Player FindPlayerByClient(this World world, Session.Client c)
|
||||
@@ -40,342 +58,350 @@ namespace OpenRA.Network
|
||||
|
||||
// Client side translated server message
|
||||
case "LocalizedMessage":
|
||||
{
|
||||
if (string.IsNullOrEmpty(order.TargetString))
|
||||
break;
|
||||
|
||||
var yaml = MiniYaml.FromString(order.TargetString);
|
||||
foreach (var node in yaml)
|
||||
{
|
||||
var localizedMessage = new LocalizedMessage(node.Value);
|
||||
TextNotificationsManager.AddSystemLine(localizedMessage.TranslatedText);
|
||||
}
|
||||
|
||||
{
|
||||
if (string.IsNullOrEmpty(order.TargetString))
|
||||
break;
|
||||
|
||||
var yaml = MiniYaml.FromString(order.TargetString, order.OrderString);
|
||||
foreach (var node in yaml)
|
||||
{
|
||||
var localizedMessage = new LocalizedMessage(node.Value);
|
||||
if (localizedMessage.Key == Joined)
|
||||
TextNotificationsManager.AddPlayerJoinedLine(localizedMessage.Key, localizedMessage.Arguments);
|
||||
else if (localizedMessage.Key == Left)
|
||||
TextNotificationsManager.AddPlayerLeftLine(localizedMessage.Key, localizedMessage.Arguments);
|
||||
else
|
||||
TextNotificationsManager.AddSystemLine(localizedMessage.Key, localizedMessage.Arguments);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "DisableChatEntry":
|
||||
{
|
||||
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
|
||||
break;
|
||||
|
||||
// Server may send MaxValue to indicate that it is disabled until further notice
|
||||
if (order.ExtraData == uint.MaxValue)
|
||||
TextNotificationsManager.ChatDisabledUntil = uint.MaxValue;
|
||||
else
|
||||
TextNotificationsManager.ChatDisabledUntil = Game.RunTime + order.ExtraData;
|
||||
|
||||
{
|
||||
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
|
||||
break;
|
||||
}
|
||||
|
||||
// Server may send MaxValue to indicate that it is disabled until further notice
|
||||
if (order.ExtraData == uint.MaxValue)
|
||||
TextNotificationsManager.ChatDisabledUntil = uint.MaxValue;
|
||||
else
|
||||
TextNotificationsManager.ChatDisabledUntil = Game.RunTime + order.ExtraData;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "StartKickVote":
|
||||
{
|
||||
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
|
||||
break;
|
||||
|
||||
KickVoteTarget = (int)order.ExtraData;
|
||||
{
|
||||
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
|
||||
break;
|
||||
}
|
||||
|
||||
KickVoteTarget = (int)order.ExtraData;
|
||||
break;
|
||||
}
|
||||
|
||||
case "EndKickVote":
|
||||
{
|
||||
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
|
||||
break;
|
||||
|
||||
if (KickVoteTarget == (int)order.ExtraData)
|
||||
KickVoteTarget = null;
|
||||
|
||||
{
|
||||
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
|
||||
break;
|
||||
}
|
||||
|
||||
if (KickVoteTarget == (int)order.ExtraData)
|
||||
KickVoteTarget = null;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Chat":
|
||||
{
|
||||
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
|
||||
if (client == null)
|
||||
break;
|
||||
|
||||
// Cut chat messages to the hard limit to avoid exploits
|
||||
var message = order.TargetString;
|
||||
if (message.Length > ChatMessageMaxLength)
|
||||
message = order.TargetString[..ChatMessageMaxLength];
|
||||
|
||||
// ExtraData 0 means this is a normal chat order, everything else is team chat
|
||||
if (order.ExtraData == 0)
|
||||
{
|
||||
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
|
||||
if (client == null)
|
||||
break;
|
||||
var p = world?.FindPlayerByClient(client);
|
||||
var suffix = (p != null && p.WinState == WinState.Lost) ? " (Dead)" : "";
|
||||
suffix = client.IsObserver ? " (Spectator)" : suffix;
|
||||
|
||||
// Cut chat messages to the hard limit to avoid exploits
|
||||
var message = order.TargetString;
|
||||
if (message.Length > ChatMessageMaxLength)
|
||||
message = order.TargetString[..ChatMessageMaxLength];
|
||||
if (orderManager.LocalClient != null && client != orderManager.LocalClient && client.Team > 0 && client.Team == orderManager.LocalClient.Team)
|
||||
suffix += " (Ally)";
|
||||
|
||||
// ExtraData 0 means this is a normal chat order, everything else is team chat
|
||||
if (order.ExtraData == 0)
|
||||
{
|
||||
var p = world?.FindPlayerByClient(client);
|
||||
var suffix = (p != null && p.WinState == WinState.Lost) ? " (Dead)" : "";
|
||||
suffix = client.IsObserver ? " (Spectator)" : suffix;
|
||||
TextNotificationsManager.AddChatLine(clientId, client.Name + suffix, message, client.Color);
|
||||
break;
|
||||
}
|
||||
|
||||
if (orderManager.LocalClient != null && client != orderManager.LocalClient && client.Team > 0 && client.Team == orderManager.LocalClient.Team)
|
||||
suffix += " (Ally)";
|
||||
|
||||
TextNotificationsManager.AddChatLine(clientId, client.Name + suffix, message, client.Color);
|
||||
break;
|
||||
}
|
||||
|
||||
// We are still in the lobby
|
||||
if (world == null)
|
||||
{
|
||||
var prefix = order.ExtraData == uint.MaxValue ? "[Spectators] " : "[Team] ";
|
||||
if (orderManager.LocalClient != null && client.Team == orderManager.LocalClient.Team)
|
||||
TextNotificationsManager.AddChatLine(clientId, prefix + client.Name, message, client.Color);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var player = world.FindPlayerByClient(client);
|
||||
var localClientIsObserver = world.IsReplay || (orderManager.LocalClient != null && orderManager.LocalClient.IsObserver)
|
||||
|| (world.LocalPlayer != null && world.LocalPlayer.WinState != WinState.Undefined);
|
||||
|
||||
// ExtraData gives us the team number, uint.MaxValue means Spectators
|
||||
if (order.ExtraData == uint.MaxValue && localClientIsObserver)
|
||||
{
|
||||
// Validate before adding the line
|
||||
if (client.IsObserver || (player != null && player.WinState != WinState.Undefined))
|
||||
TextNotificationsManager.AddChatLine(clientId, "[Spectators] " + client.Name, message, client.Color);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var valid = client.Team == order.ExtraData && player != null && player.WinState == WinState.Undefined;
|
||||
var isSameTeam = orderManager.LocalClient != null && order.ExtraData == orderManager.LocalClient.Team
|
||||
&& world.LocalPlayer != null && world.LocalPlayer.WinState == WinState.Undefined;
|
||||
|
||||
if (valid && (isSameTeam || world.IsReplay))
|
||||
TextNotificationsManager.AddChatLine(clientId, "[Team" + (world.IsReplay ? " " + order.ExtraData : "") + "] " + client.Name, message, client.Color);
|
||||
// We are still in the lobby
|
||||
if (world == null)
|
||||
{
|
||||
var prefix = order.ExtraData == uint.MaxValue ? "[Spectators] " : "[Team] ";
|
||||
if (orderManager.LocalClient != null && client.Team == orderManager.LocalClient.Team)
|
||||
TextNotificationsManager.AddChatLine(clientId, prefix + client.Name, message, client.Color);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var player = world.FindPlayerByClient(client);
|
||||
var localClientIsObserver = world.IsReplay || (orderManager.LocalClient != null && orderManager.LocalClient.IsObserver)
|
||||
|| (world.LocalPlayer != null && world.LocalPlayer.WinState != WinState.Undefined);
|
||||
|
||||
// ExtraData gives us the team number, uint.MaxValue means Spectators
|
||||
if (order.ExtraData == uint.MaxValue && localClientIsObserver)
|
||||
{
|
||||
// Validate before adding the line
|
||||
if (client.IsObserver || (player != null && player.WinState != WinState.Undefined))
|
||||
TextNotificationsManager.AddChatLine(clientId, "[Spectators] " + client.Name, message, client.Color);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var valid = client.Team == order.ExtraData && player != null && player.WinState == WinState.Undefined;
|
||||
var isSameTeam = orderManager.LocalClient != null && order.ExtraData == orderManager.LocalClient.Team
|
||||
&& world.LocalPlayer != null && world.LocalPlayer.WinState == WinState.Undefined;
|
||||
|
||||
if (valid && (isSameTeam || world.IsReplay))
|
||||
TextNotificationsManager.AddChatLine(clientId, "[Team" + (world.IsReplay ? " " + order.ExtraData : "") + "] " + client.Name, message, client.Color);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "StartGame":
|
||||
{
|
||||
if (Game.ModData.MapCache[orderManager.LobbyInfo.GlobalSettings.Map].Status != MapStatus.Available)
|
||||
{
|
||||
if (Game.ModData.MapCache[orderManager.LobbyInfo.GlobalSettings.Map].Status != MapStatus.Available)
|
||||
{
|
||||
Game.Disconnect();
|
||||
Game.LoadShellMap();
|
||||
Game.Disconnect();
|
||||
Game.LoadShellMap();
|
||||
|
||||
// TODO: After adding a startup error dialog, notify the replay load failure.
|
||||
break;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(order.TargetString))
|
||||
{
|
||||
var data = MiniYaml.FromString(order.TargetString);
|
||||
var saveLastOrdersFrame = data.FirstOrDefault(n => n.Key == "SaveLastOrdersFrame");
|
||||
if (saveLastOrdersFrame != null)
|
||||
orderManager.GameSaveLastFrame =
|
||||
FieldLoader.GetValue<int>("saveLastOrdersFrame", saveLastOrdersFrame.Value.Value);
|
||||
|
||||
var saveSyncFrame = data.FirstOrDefault(n => n.Key == "SaveSyncFrame");
|
||||
if (saveSyncFrame != null)
|
||||
orderManager.GameSaveLastSyncFrame =
|
||||
FieldLoader.GetValue<int>("SaveSyncFrame", saveSyncFrame.Value.Value);
|
||||
}
|
||||
else
|
||||
TextNotificationsManager.AddSystemLine("The game has started.");
|
||||
|
||||
Game.StartGame(orderManager.LobbyInfo.GlobalSettings.Map, WorldType.Regular);
|
||||
// TODO: After adding a startup error dialog, notify the replay load failure.
|
||||
break;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(order.TargetString))
|
||||
{
|
||||
var data = MiniYaml.FromString(order.TargetString, order.OrderString);
|
||||
var saveLastOrdersFrame = data.FirstOrDefault(n => n.Key == "SaveLastOrdersFrame");
|
||||
if (saveLastOrdersFrame != null)
|
||||
orderManager.GameSaveLastFrame =
|
||||
FieldLoader.GetValue<int>("saveLastOrdersFrame", saveLastOrdersFrame.Value.Value);
|
||||
|
||||
var saveSyncFrame = data.FirstOrDefault(n => n.Key == "SaveSyncFrame");
|
||||
if (saveSyncFrame != null)
|
||||
orderManager.GameSaveLastSyncFrame =
|
||||
FieldLoader.GetValue<int>("SaveSyncFrame", saveSyncFrame.Value.Value);
|
||||
}
|
||||
else
|
||||
TextNotificationsManager.AddSystemLine(GameStarted);
|
||||
|
||||
Game.StartGame(orderManager.LobbyInfo.GlobalSettings.Map, WorldType.Regular);
|
||||
break;
|
||||
}
|
||||
|
||||
case "SaveTraitData":
|
||||
{
|
||||
var data = MiniYaml.FromString(order.TargetString)[0];
|
||||
var traitIndex = int.Parse(data.Key);
|
||||
{
|
||||
var data = MiniYaml.FromString(order.TargetString, order.OrderString)[0];
|
||||
var traitIndex = Exts.ParseInt32Invariant(data.Key);
|
||||
|
||||
world?.AddGameSaveTraitData(traitIndex, data.Value);
|
||||
world?.AddGameSaveTraitData(traitIndex, data.Value);
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "GameSaved":
|
||||
if (!orderManager.World.IsReplay)
|
||||
TextNotificationsManager.AddSystemLine("Game saved");
|
||||
TextNotificationsManager.AddSystemLine(GameSaved);
|
||||
|
||||
foreach (var nsr in orderManager.World.WorldActor.TraitsImplementing<INotifyGameSaved>())
|
||||
nsr.GameSaved(orderManager.World);
|
||||
break;
|
||||
|
||||
case "PauseGame":
|
||||
{
|
||||
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
|
||||
if (client != null)
|
||||
{
|
||||
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
|
||||
if (client != null)
|
||||
{
|
||||
var pause = order.TargetString == "Pause";
|
||||
var pause = order.TargetString == "Pause";
|
||||
|
||||
// Prevent injected unpause orders from restarting a finished game
|
||||
if (orderManager.World.IsGameOver && !pause)
|
||||
break;
|
||||
// Prevent injected unpause orders from restarting a finished game
|
||||
if (orderManager.World.IsGameOver && !pause)
|
||||
break;
|
||||
|
||||
if (orderManager.World.Paused != pause && world != null && world.LobbyInfo.NonBotClients.Count() > 1)
|
||||
{
|
||||
var pausetext = $"The game is {(pause ? "paused" : "un-paused")} by {client.Name}";
|
||||
TextNotificationsManager.AddSystemLine(pausetext);
|
||||
}
|
||||
if (orderManager.World.Paused != pause && world != null && world.LobbyInfo.NonBotClients.Count() > 1)
|
||||
TextNotificationsManager.AddSystemLine(pause ? GamePaused : GameUnpaused, Translation.Arguments("player", client.Name));
|
||||
|
||||
orderManager.World.Paused = pause;
|
||||
orderManager.World.PredictedPaused = pause;
|
||||
}
|
||||
|
||||
break;
|
||||
orderManager.World.Paused = pause;
|
||||
orderManager.World.PredictedPaused = pause;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "HandshakeRequest":
|
||||
{
|
||||
// Switch to the server's mod if we need and are able to
|
||||
var mod = Game.ModData.Manifest;
|
||||
var request = HandshakeRequest.Deserialize(order.TargetString, order.OrderString);
|
||||
|
||||
var externalKey = ExternalMod.MakeKey(request.Mod, request.Version);
|
||||
if ((request.Mod != mod.Id || request.Version != mod.Metadata.Version) &&
|
||||
Game.ExternalMods.TryGetValue(externalKey, out var external))
|
||||
{
|
||||
// Switch to the server's mod if we need and are able to
|
||||
var mod = Game.ModData.Manifest;
|
||||
var request = HandshakeRequest.Deserialize(order.TargetString);
|
||||
|
||||
var externalKey = ExternalMod.MakeKey(request.Mod, request.Version);
|
||||
if ((request.Mod != mod.Id || request.Version != mod.Metadata.Version) &&
|
||||
Game.ExternalMods.TryGetValue(externalKey, out var external))
|
||||
{
|
||||
// The ConnectionFailedLogic will prompt the user to switch mods
|
||||
CurrentServerSettings.ServerExternalMod = external;
|
||||
orderManager.Connection.Dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
Game.Settings.Player.Name = Settings.SanitizedPlayerName(Game.Settings.Player.Name);
|
||||
Game.Settings.Save();
|
||||
|
||||
// Otherwise send the handshake with our current settings and let the server reject us
|
||||
var info = new Session.Client()
|
||||
{
|
||||
Name = Game.Settings.Player.Name,
|
||||
PreferredColor = Game.Settings.Player.Color,
|
||||
Color = Game.Settings.Player.Color,
|
||||
Faction = "Random",
|
||||
SpawnPoint = 0,
|
||||
Team = 0,
|
||||
State = Session.ClientState.Invalid
|
||||
};
|
||||
|
||||
var localProfile = Game.LocalPlayerProfile;
|
||||
var response = new HandshakeResponse()
|
||||
{
|
||||
Client = info,
|
||||
Mod = mod.Id,
|
||||
Version = mod.Metadata.Version,
|
||||
Password = CurrentServerSettings.Password,
|
||||
Fingerprint = localProfile.Fingerprint,
|
||||
OrdersProtocol = ProtocolVersion.Orders
|
||||
};
|
||||
|
||||
if (request.AuthToken != null && response.Fingerprint != null)
|
||||
response.AuthSignature = localProfile.Sign(request.AuthToken);
|
||||
|
||||
orderManager.IssueOrder(new Order("HandshakeResponse", null, false)
|
||||
{
|
||||
Type = OrderType.Handshake,
|
||||
IsImmediate = true,
|
||||
TargetString = response.Serialize()
|
||||
});
|
||||
|
||||
// The ConnectionFailedLogic will prompt the user to switch mods
|
||||
CurrentServerSettings.ServerExternalMod = external;
|
||||
orderManager.Connection.Dispose();
|
||||
break;
|
||||
}
|
||||
|
||||
Game.Settings.Player.Name = Settings.SanitizedPlayerName(Game.Settings.Player.Name);
|
||||
Game.Settings.Save();
|
||||
|
||||
// Otherwise send the handshake with our current settings and let the server reject us
|
||||
var info = new Session.Client()
|
||||
{
|
||||
Name = Game.Settings.Player.Name,
|
||||
PreferredColor = Game.Settings.Player.Color,
|
||||
Color = Game.Settings.Player.Color,
|
||||
Faction = "Random",
|
||||
SpawnPoint = 0,
|
||||
Team = 0,
|
||||
State = Session.ClientState.Invalid
|
||||
};
|
||||
|
||||
var localProfile = Game.LocalPlayerProfile;
|
||||
var response = new HandshakeResponse()
|
||||
{
|
||||
Client = info,
|
||||
Mod = mod.Id,
|
||||
Version = mod.Metadata.Version,
|
||||
Password = CurrentServerSettings.Password,
|
||||
Fingerprint = localProfile.Fingerprint,
|
||||
OrdersProtocol = ProtocolVersion.Orders
|
||||
};
|
||||
|
||||
if (request.AuthToken != null && response.Fingerprint != null)
|
||||
response.AuthSignature = localProfile.Sign(request.AuthToken);
|
||||
|
||||
orderManager.IssueOrder(new Order("HandshakeResponse", null, false)
|
||||
{
|
||||
Type = OrderType.Handshake,
|
||||
IsImmediate = true,
|
||||
TargetString = response.Serialize()
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "ServerError":
|
||||
{
|
||||
orderManager.ServerError = order.TargetString;
|
||||
orderManager.AuthenticationFailed = false;
|
||||
break;
|
||||
}
|
||||
{
|
||||
orderManager.ServerError = order.TargetString;
|
||||
orderManager.AuthenticationFailed = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case "AuthenticationError":
|
||||
{
|
||||
// The ConnectionFailedLogic will prompt the user for the password
|
||||
orderManager.ServerError = order.TargetString;
|
||||
orderManager.AuthenticationFailed = true;
|
||||
break;
|
||||
}
|
||||
{
|
||||
// The ConnectionFailedLogic will prompt the user for the password
|
||||
orderManager.ServerError = order.TargetString;
|
||||
orderManager.AuthenticationFailed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case "SyncInfo":
|
||||
{
|
||||
orderManager.LobbyInfo = Session.Deserialize(order.TargetString);
|
||||
Game.SyncLobbyInfo();
|
||||
break;
|
||||
}
|
||||
{
|
||||
orderManager.LobbyInfo = Session.Deserialize(order.TargetString, order.OrderString);
|
||||
Game.SyncLobbyInfo();
|
||||
break;
|
||||
}
|
||||
|
||||
case "SyncLobbyClients":
|
||||
{
|
||||
var clients = new List<Session.Client>();
|
||||
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString);
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var clients = new List<Session.Client>();
|
||||
var nodes = MiniYaml.FromString(order.TargetString);
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var strings = node.Key.Split('@');
|
||||
if (strings[0] == "Client")
|
||||
clients.Add(Session.Client.Deserialize(node.Value));
|
||||
}
|
||||
|
||||
orderManager.LobbyInfo.Clients = clients;
|
||||
Game.SyncLobbyInfo();
|
||||
break;
|
||||
var strings = node.Key.Split('@');
|
||||
if (strings[0] == "Client")
|
||||
clients.Add(Session.Client.Deserialize(node.Value));
|
||||
}
|
||||
|
||||
orderManager.LobbyInfo.Clients = clients;
|
||||
Game.SyncLobbyInfo();
|
||||
break;
|
||||
}
|
||||
|
||||
case "SyncLobbySlots":
|
||||
{
|
||||
var slots = new Dictionary<string, Session.Slot>();
|
||||
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString);
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var slots = new Dictionary<string, Session.Slot>();
|
||||
var nodes = MiniYaml.FromString(order.TargetString);
|
||||
foreach (var node in nodes)
|
||||
var strings = node.Key.Split('@');
|
||||
if (strings[0] == "Slot")
|
||||
{
|
||||
var strings = node.Key.Split('@');
|
||||
if (strings[0] == "Slot")
|
||||
{
|
||||
var slot = Session.Slot.Deserialize(node.Value);
|
||||
slots.Add(slot.PlayerReference, slot);
|
||||
}
|
||||
var slot = Session.Slot.Deserialize(node.Value);
|
||||
slots.Add(slot.PlayerReference, slot);
|
||||
}
|
||||
|
||||
orderManager.LobbyInfo.Slots = slots;
|
||||
Game.SyncLobbyInfo();
|
||||
break;
|
||||
}
|
||||
|
||||
orderManager.LobbyInfo.Slots = slots;
|
||||
Game.SyncLobbyInfo();
|
||||
break;
|
||||
}
|
||||
|
||||
case "SyncLobbyGlobalSettings":
|
||||
{
|
||||
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString);
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var nodes = MiniYaml.FromString(order.TargetString);
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var strings = node.Key.Split('@');
|
||||
if (strings[0] == "GlobalSettings")
|
||||
orderManager.LobbyInfo.GlobalSettings = Session.Global.Deserialize(node.Value);
|
||||
}
|
||||
|
||||
Game.SyncLobbyInfo();
|
||||
break;
|
||||
var strings = node.Key.Split('@');
|
||||
if (strings[0] == "GlobalSettings")
|
||||
orderManager.LobbyInfo.GlobalSettings = Session.Global.Deserialize(node.Value);
|
||||
}
|
||||
|
||||
Game.SyncLobbyInfo();
|
||||
break;
|
||||
}
|
||||
|
||||
case "SyncConnectionQuality":
|
||||
{
|
||||
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString);
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var nodes = MiniYaml.FromString(order.TargetString);
|
||||
foreach (var node in nodes)
|
||||
var strings = node.Key.Split('@');
|
||||
if (strings[0] == "ConnectionQuality")
|
||||
{
|
||||
var strings = node.Key.Split('@');
|
||||
if (strings[0] == "ConnectionQuality")
|
||||
{
|
||||
var client = orderManager.LobbyInfo.Clients.FirstOrDefault(c => c.Index == int.Parse(strings[1]));
|
||||
if (client != null)
|
||||
client.ConnectionQuality = FieldLoader.GetValue<Session.ConnectionQuality>("ConnectionQuality", node.Value.Value);
|
||||
}
|
||||
var client = orderManager.LobbyInfo.Clients.FirstOrDefault(c => c.Index == Exts.ParseInt32Invariant(strings[1]));
|
||||
if (client != null)
|
||||
client.ConnectionQuality = FieldLoader.GetValue<Session.ConnectionQuality>("ConnectionQuality", node.Value.Value);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "SyncMapPool":
|
||||
{
|
||||
orderManager.ServerMapPool = FieldLoader.GetValue<HashSet<string>>("SyncMapPool", order.TargetString);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
if (world == null)
|
||||
break;
|
||||
|
||||
if (order.GroupedActors == null)
|
||||
ResolveOrder(order, world, orderManager, clientId);
|
||||
else
|
||||
foreach (var subject in order.GroupedActors)
|
||||
ResolveOrder(Order.FromGroupedOrder(order, subject), world, orderManager, clientId);
|
||||
|
||||
{
|
||||
if (world == null)
|
||||
break;
|
||||
}
|
||||
|
||||
if (order.GroupedActors == null)
|
||||
ResolveOrder(order, world, orderManager, clientId);
|
||||
else
|
||||
foreach (var subject in order.GroupedActors)
|
||||
ResolveOrder(Order.FromGroupedOrder(order, subject), world, orderManager, clientId);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -133,9 +133,9 @@ namespace OpenRA
|
||||
|
||||
public ConstructorInfo GetCtor(Type type)
|
||||
{
|
||||
var flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
|
||||
var ctors = type.GetConstructors(flags).Where(x => x.HasAttribute<UseCtorAttribute>());
|
||||
if (ctors.Count() > 1)
|
||||
const BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
|
||||
var ctors = type.GetConstructors(Flags).Where(x => x.HasAttribute<UseCtorAttribute>()).ToList();
|
||||
if (ctors.Count > 1)
|
||||
throw new InvalidOperationException("ObjectCreator: UseCtor on multiple constructors; invalid.");
|
||||
return ctors.FirstOrDefault();
|
||||
}
|
||||
@@ -152,8 +152,8 @@ namespace OpenRA
|
||||
for (var i = 0; i < p.Length; i++)
|
||||
{
|
||||
var key = p[i].Name;
|
||||
if (!args.ContainsKey(key)) throw new InvalidOperationException($"ObjectCreator: key `{key}' not found");
|
||||
a[i] = args[key];
|
||||
if (!args.TryGetValue(key, out var arg)) throw new InvalidOperationException($"ObjectCreator: key `{key}' not found");
|
||||
a[i] = arg;
|
||||
}
|
||||
|
||||
return ctor.Invoke(a);
|
||||
|
||||
@@ -10,11 +10,10 @@
|
||||
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Linguini.Bundle" Version="0.5.0" />
|
||||
<PackageReference Include="Linguini.Bundle" Version="0.6.0" />
|
||||
<PackageReference Include="OpenRA-Eluant" Version="1.0.22" />
|
||||
<PackageReference Include="Mono.NAT" Version="3.0.4" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageReference Include="System.Threading.Channels" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -109,14 +109,14 @@ namespace OpenRA
|
||||
var p = Process.Start(psi);
|
||||
string line;
|
||||
while ((line = p.StandardOutput.ReadLine()) != null)
|
||||
if (line.StartsWith("Operating System: "))
|
||||
if (line.StartsWith("Operating System: ", StringComparison.Ordinal))
|
||||
return line[18..] + suffix;
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (File.Exists("/etc/os-release"))
|
||||
foreach (var line in File.ReadLines("/etc/os-release"))
|
||||
if (line.StartsWith("PRETTY_NAME="))
|
||||
if (line.StartsWith("PRETTY_NAME=", StringComparison.Ordinal))
|
||||
return line[13..^1] + suffix;
|
||||
}
|
||||
else if (CurrentPlatform == PlatformType.OSX)
|
||||
@@ -134,7 +134,7 @@ namespace OpenRA
|
||||
while ((line = p.StandardOutput.ReadLine()) != null)
|
||||
{
|
||||
line = line.Trim();
|
||||
if (line.StartsWith("System Version: "))
|
||||
if (line.StartsWith("System Version: ", StringComparison.Ordinal))
|
||||
return line[16..];
|
||||
}
|
||||
}
|
||||
@@ -274,7 +274,7 @@ namespace OpenRA
|
||||
throw new DirectoryNotFoundException(path);
|
||||
|
||||
if (!path.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) &&
|
||||
!path.EndsWith(Path.AltDirectorySeparatorChar.ToString(), StringComparison.Ordinal))
|
||||
!path.EndsWith(Path.AltDirectorySeparatorChar.ToString(), StringComparison.Ordinal))
|
||||
path += Path.DirectorySeparatorChar;
|
||||
|
||||
engineDirAccessed = true;
|
||||
|
||||
@@ -133,7 +133,7 @@ namespace OpenRA
|
||||
|
||||
static FactionInfo ResolveDisplayFaction(World world, string factionName)
|
||||
{
|
||||
var factions = world.Map.Rules.Actors[SystemActors.World].TraitInfos<FactionInfo>().ToArray();
|
||||
var factions = world.Map.Rules.Actors[SystemActors.World].TraitInfos<FactionInfo>();
|
||||
|
||||
return factions.FirstOrDefault(f => f.InternalName == factionName) ?? factions.First();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using OpenRA.FileFormats;
|
||||
using OpenRA.Graphics;
|
||||
@@ -94,10 +93,10 @@ namespace OpenRA
|
||||
});
|
||||
}
|
||||
|
||||
var labelNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Label");
|
||||
var icon24Node = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon24");
|
||||
var icon48Node = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon48");
|
||||
var icon72Node = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon72");
|
||||
var labelNode = yaml.NodeWithKeyOrDefault("Label");
|
||||
var icon24Node = yaml.NodeWithKeyOrDefault("Icon24");
|
||||
var icon48Node = yaml.NodeWithKeyOrDefault("Icon48");
|
||||
var icon72Node = yaml.NodeWithKeyOrDefault("Icon72");
|
||||
if (labelNode == null)
|
||||
return null;
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#endregion
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenRA
|
||||
{
|
||||
@@ -31,7 +30,7 @@ namespace OpenRA
|
||||
{
|
||||
var badges = new List<PlayerBadge>();
|
||||
|
||||
var badgesNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Badges");
|
||||
var badgesNode = yaml.NodeWithKeyOrDefault("Badges");
|
||||
if (badgesNode != null)
|
||||
{
|
||||
var playerDatabase = Game.ModData.Manifest.Get<PlayerDatabase>();
|
||||
|
||||
@@ -86,7 +86,7 @@ namespace OpenRA.Primitives
|
||||
|
||||
public static BitSet<T> FromStringsNoAlloc(string[] values)
|
||||
{
|
||||
return new BitSet<T>(BitSetAllocator<T>.GetBitsNoAlloc(values)) { };
|
||||
return new BitSet<T>(BitSetAllocator<T>.GetBitsNoAlloc(values));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace OpenRA.Primitives
|
||||
{
|
||||
public readonly struct Color : IEquatable<Color>, IScriptBindable
|
||||
{
|
||||
readonly long argb;
|
||||
readonly uint argb;
|
||||
|
||||
public static Color FromArgb(int red, int green, int blue)
|
||||
{
|
||||
@@ -26,7 +26,7 @@ namespace OpenRA.Primitives
|
||||
|
||||
public static Color FromArgb(int alpha, int red, int green, int blue)
|
||||
{
|
||||
return new Color(((byte)alpha << 24) + ((byte)red << 16) + ((byte)green << 8) + (byte)blue);
|
||||
return new Color((uint)(((byte)alpha << 24) + ((byte)red << 16) + ((byte)green << 8) + (byte)blue));
|
||||
}
|
||||
|
||||
public static Color FromAhsl(int alpha, float h, float s, float l)
|
||||
@@ -55,14 +55,14 @@ namespace OpenRA.Primitives
|
||||
return (A, h, s, v);
|
||||
}
|
||||
|
||||
Color(long argb)
|
||||
Color(uint argb)
|
||||
{
|
||||
this.argb = argb;
|
||||
}
|
||||
|
||||
public int ToArgb()
|
||||
public uint ToArgb()
|
||||
{
|
||||
return (int)argb;
|
||||
return argb;
|
||||
}
|
||||
|
||||
public static Color FromArgb(int alpha, Color baseColor)
|
||||
@@ -70,14 +70,9 @@ namespace OpenRA.Primitives
|
||||
return FromArgb(alpha, baseColor.R, baseColor.G, baseColor.B);
|
||||
}
|
||||
|
||||
public static Color FromArgb(int argb)
|
||||
{
|
||||
return FromArgb((byte)(argb >> 24), (byte)(argb >> 16), (byte)(argb >> 8), (byte)argb);
|
||||
}
|
||||
|
||||
public static Color FromArgb(uint argb)
|
||||
{
|
||||
return FromArgb((byte)(argb >> 24), (byte)(argb >> 16), (byte)(argb >> 8), (byte)argb);
|
||||
return new Color(argb);
|
||||
}
|
||||
|
||||
static float SrgbToLinear(float c)
|
||||
@@ -154,7 +149,7 @@ namespace OpenRA.Primitives
|
||||
|
||||
// Wrap negative values into [0-1)
|
||||
if (h < 0)
|
||||
h += 1;
|
||||
h++;
|
||||
|
||||
var s = delta / rgbMax;
|
||||
return (h, s, v);
|
||||
@@ -169,12 +164,12 @@ namespace OpenRA.Primitives
|
||||
|
||||
byte alpha = 255;
|
||||
if (!byte.TryParse(value.AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var red)
|
||||
|| !byte.TryParse(value.AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var green)
|
||||
|| !byte.TryParse(value.AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var blue))
|
||||
|| !byte.TryParse(value.AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var green)
|
||||
|| !byte.TryParse(value.AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var blue))
|
||||
return false;
|
||||
|
||||
if (value.Length == 8
|
||||
&& !byte.TryParse(value.AsSpan(6, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out alpha))
|
||||
&& !byte.TryParse(value.AsSpan(6, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out alpha))
|
||||
return false;
|
||||
|
||||
color = FromArgb(alpha, red, green, blue);
|
||||
@@ -224,9 +219,9 @@ namespace OpenRA.Primitives
|
||||
public override string ToString()
|
||||
{
|
||||
if (A == 255)
|
||||
return R.ToString("X2") + G.ToString("X2") + B.ToString("X2");
|
||||
return CryptoUtil.ToHex(stackalloc byte[3] { R, G, B });
|
||||
|
||||
return R.ToString("X2") + G.ToString("X2") + B.ToString("X2") + A.ToString("X2");
|
||||
return CryptoUtil.ToHex(stackalloc byte[4] { R, G, B, A });
|
||||
}
|
||||
|
||||
public static Color Transparent => FromArgb(0x00FFFFFF);
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace OpenRA.Primitives
|
||||
|
||||
public static LongBitSet<T> FromStringsNoAlloc(string[] values)
|
||||
{
|
||||
return new LongBitSet<T>(LongBitSetAllocator<T>.GetBitsNoAlloc(values)) { };
|
||||
return new LongBitSet<T>(LongBitSetAllocator<T>.GetBitsNoAlloc(values));
|
||||
}
|
||||
|
||||
public static void Reset()
|
||||
|
||||
@@ -13,13 +13,12 @@ using System;
|
||||
|
||||
namespace OpenRA.Primitives
|
||||
{
|
||||
public struct Rectangle : IEquatable<Rectangle>
|
||||
public readonly struct Rectangle : IEquatable<Rectangle>
|
||||
{
|
||||
// TODO: Make these readonly: this will require a lot of changes to the UI logic
|
||||
public int X;
|
||||
public int Y;
|
||||
public int Width;
|
||||
public int Height;
|
||||
public readonly int X;
|
||||
public readonly int Y;
|
||||
public readonly int Width;
|
||||
public readonly int Height;
|
||||
public static readonly Rectangle Empty;
|
||||
|
||||
public static Rectangle FromLTRB(int left, int top, int right, int bottom)
|
||||
@@ -58,35 +57,35 @@ namespace OpenRA.Primitives
|
||||
Height = size.Height;
|
||||
}
|
||||
|
||||
public int Left => X;
|
||||
public int Right => X + Width;
|
||||
public int Top => Y;
|
||||
public int Bottom => Y + Height;
|
||||
public bool IsEmpty => X == 0 && Y == 0 && Width == 0 && Height == 0;
|
||||
public int2 Location => new(X, Y);
|
||||
public Size Size => new(Width, Height);
|
||||
public readonly int Left => X;
|
||||
public readonly int Right => X + Width;
|
||||
public readonly int Top => Y;
|
||||
public readonly int Bottom => Y + Height;
|
||||
public readonly bool IsEmpty => X == 0 && Y == 0 && Width == 0 && Height == 0;
|
||||
public readonly int2 Location => new(X, Y);
|
||||
public readonly Size Size => new(Width, Height);
|
||||
|
||||
public int2 TopLeft => Location;
|
||||
public int2 TopRight => new(X + Width, Y);
|
||||
public int2 BottomLeft => new(X, Y + Height);
|
||||
public int2 BottomRight => new(X + Width, Y + Height);
|
||||
public readonly int2 TopLeft => Location;
|
||||
public readonly int2 TopRight => new(X + Width, Y);
|
||||
public readonly int2 BottomLeft => new(X, Y + Height);
|
||||
public readonly int2 BottomRight => new(X + Width, Y + Height);
|
||||
|
||||
public bool Contains(int x, int y)
|
||||
public readonly bool Contains(int x, int y)
|
||||
{
|
||||
return x >= Left && x < Right && y >= Top && y < Bottom;
|
||||
}
|
||||
|
||||
public bool Contains(int2 pt)
|
||||
public readonly bool Contains(int2 pt)
|
||||
{
|
||||
return Contains(pt.X, pt.Y);
|
||||
}
|
||||
|
||||
public bool Equals(Rectangle other)
|
||||
public readonly bool Equals(Rectangle other)
|
||||
{
|
||||
return this == other;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
public override readonly bool Equals(object obj)
|
||||
{
|
||||
if (obj is not Rectangle)
|
||||
return false;
|
||||
@@ -94,17 +93,17 @@ namespace OpenRA.Primitives
|
||||
return this == (Rectangle)obj;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
public override readonly int GetHashCode()
|
||||
{
|
||||
return Height + Width ^ X + Y;
|
||||
}
|
||||
|
||||
public bool IntersectsWith(Rectangle rect)
|
||||
public readonly bool IntersectsWith(Rectangle rect)
|
||||
{
|
||||
return Left < rect.Right && Right > rect.Left && Top < rect.Bottom && Bottom > rect.Top;
|
||||
}
|
||||
|
||||
bool IntersectsWithInclusive(Rectangle r)
|
||||
readonly bool IntersectsWithInclusive(Rectangle r)
|
||||
{
|
||||
return Left <= r.Right && Right >= r.Left && Top <= r.Bottom && Bottom >= r.Top;
|
||||
}
|
||||
@@ -117,14 +116,14 @@ namespace OpenRA.Primitives
|
||||
return FromLTRB(Math.Max(a.Left, b.Left), Math.Max(a.Top, b.Top), Math.Min(a.Right, b.Right), Math.Min(a.Bottom, b.Bottom));
|
||||
}
|
||||
|
||||
public bool Contains(Rectangle rect)
|
||||
public readonly bool Contains(Rectangle rect)
|
||||
{
|
||||
return rect == Intersect(this, rect);
|
||||
}
|
||||
|
||||
public static Rectangle operator *(int a, Rectangle b) { return new Rectangle(a * b.X, a * b.Y, a * b.Width, a * b.Height); }
|
||||
|
||||
public override string ToString()
|
||||
public override readonly string ToString()
|
||||
{
|
||||
return $"{X},{Y},{Width},{Height}";
|
||||
}
|
||||
|
||||
146
OpenRA.Game/Primitives/RingBuffer.cs
Normal file
146
OpenRA.Game/Primitives/RingBuffer.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
#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;
|
||||
|
||||
namespace OpenRA.Primitives
|
||||
{
|
||||
/// <summary>Fixed size rorating buffer backed by an array.</summary>
|
||||
public class RingBuffer<T> : ICollection<T>, IEnumerable<T>
|
||||
{
|
||||
readonly IComparer<T> comparer;
|
||||
readonly T[] values;
|
||||
int start;
|
||||
|
||||
public int Capacity => values.Length;
|
||||
public int Count { get; private set; }
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public RingBuffer(int capacity, IComparer<T> comparer)
|
||||
{
|
||||
this.comparer = comparer;
|
||||
values = new T[capacity];
|
||||
start = 0;
|
||||
Count = 0;
|
||||
}
|
||||
|
||||
public RingBuffer(int capacity)
|
||||
: this(capacity, Comparer<T>.Default) { }
|
||||
|
||||
public void Add(T value)
|
||||
{
|
||||
values[(start + Count) % values.Length] = value;
|
||||
if (Count < values.Length)
|
||||
Count++;
|
||||
else
|
||||
start = (start + 1) % values.Length;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Array.Clear(values, 0, values.Length);
|
||||
start = 0;
|
||||
Count = 0;
|
||||
}
|
||||
|
||||
public bool Contains(T value)
|
||||
{
|
||||
var capacity = values.Length;
|
||||
var end = start + Count;
|
||||
for (var i = start; i < end; ++i)
|
||||
if (comparer.Compare(values[i % capacity], value) == 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
if (array == null)
|
||||
throw new ArgumentNullException(nameof(array));
|
||||
|
||||
if (arrayIndex < 0)
|
||||
throw new ArgumentNullException(nameof(arrayIndex));
|
||||
|
||||
if (arrayIndex + Count >= array.Length)
|
||||
throw new ArgumentException("Invalid array capacity");
|
||||
|
||||
var destinationIndex = arrayIndex;
|
||||
var end = start + Count;
|
||||
var capacity = values.Length;
|
||||
for (var i = start; i < end; ++i)
|
||||
array[destinationIndex++] = values[i % capacity];
|
||||
}
|
||||
|
||||
public bool Remove(T value)
|
||||
{
|
||||
var capacity = values.Length;
|
||||
var end = start + Count;
|
||||
for (var i = start; i < end; ++i)
|
||||
{
|
||||
if (comparer.Compare(values[i % capacity], value) == 0)
|
||||
{
|
||||
end--;
|
||||
for (var j = i; j < end; ++j)
|
||||
values[j % capacity] = values[(j + 1) % capacity];
|
||||
|
||||
Count--;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public T this[int pos]
|
||||
{
|
||||
get => values[(start + pos) % values.Length];
|
||||
|
||||
set
|
||||
{
|
||||
if (pos >= Count)
|
||||
throw new ArgumentException($"Index out of bounds: {pos}");
|
||||
|
||||
values[(start + pos) % values.Length] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public T First()
|
||||
{
|
||||
if (Count == 0)
|
||||
throw new ArgumentException("Empty buffer");
|
||||
|
||||
return values[start];
|
||||
}
|
||||
|
||||
public T Last()
|
||||
{
|
||||
if (Count == 0)
|
||||
throw new ArgumentException("Empty buffer");
|
||||
|
||||
return values[(start + Count - 1) % values.Length];
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
var initState = start + Count;
|
||||
for (var i = 0; i < Count; i++)
|
||||
{
|
||||
if (start + Count != initState)
|
||||
throw new InvalidOperationException("Collection was modified; enumeration operation may not execute");
|
||||
yield return values[(start + i) % values.Length];
|
||||
}
|
||||
}
|
||||
|
||||
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); }
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,20 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace OpenRA.Primitives
|
||||
{
|
||||
public class TypeDictionary : IEnumerable<object>
|
||||
{
|
||||
static readonly Func<Type, List<object>> CreateList = type => new List<object>();
|
||||
readonly Dictionary<Type, List<object>> data = new();
|
||||
static readonly Func<Type, ITypeContainer> CreateTypeContainer = t =>
|
||||
(ITypeContainer)typeof(TypeContainer<>).MakeGenericType(t).GetConstructor(Type.EmptyTypes).Invoke(null);
|
||||
|
||||
readonly Dictionary<Type, ITypeContainer> data = new();
|
||||
|
||||
ITypeContainer InnerGet(Type t)
|
||||
{
|
||||
return data.GetOrAdd(t, CreateTypeContainer);
|
||||
}
|
||||
|
||||
public void Add(object val)
|
||||
{
|
||||
@@ -33,7 +39,7 @@ namespace OpenRA.Primitives
|
||||
|
||||
void InnerAdd(Type t, object val)
|
||||
{
|
||||
data.GetOrAdd(t, CreateList).Add(val);
|
||||
InnerGet(t).Add(val);
|
||||
}
|
||||
|
||||
public bool Contains<T>()
|
||||
@@ -48,35 +54,33 @@ namespace OpenRA.Primitives
|
||||
|
||||
public T Get<T>()
|
||||
{
|
||||
return (T)Get(typeof(T), true);
|
||||
return Get<T>(true);
|
||||
}
|
||||
|
||||
public T GetOrDefault<T>()
|
||||
{
|
||||
var result = Get(typeof(T), false);
|
||||
if (result == null)
|
||||
return default;
|
||||
return (T)result;
|
||||
return Get<T>(false);
|
||||
}
|
||||
|
||||
object Get(Type t, bool throwsIfMissing)
|
||||
T Get<T>(bool throwsIfMissing)
|
||||
{
|
||||
if (!data.TryGetValue(t, out var ret))
|
||||
if (!data.TryGetValue(typeof(T), out var container))
|
||||
{
|
||||
if (throwsIfMissing)
|
||||
throw new InvalidOperationException($"TypeDictionary does not contain instance of type `{t}`");
|
||||
return null;
|
||||
throw new InvalidOperationException($"TypeDictionary does not contain instance of type `{typeof(T)}`");
|
||||
return default;
|
||||
}
|
||||
|
||||
if (ret.Count > 1)
|
||||
throw new InvalidOperationException($"TypeDictionary contains multiple instances of type `{t}`");
|
||||
return ret[0];
|
||||
var list = ((TypeContainer<T>)container).Objects;
|
||||
if (list.Count > 1)
|
||||
throw new InvalidOperationException($"TypeDictionary contains multiple instances of type `{typeof(T)}`");
|
||||
return list[0];
|
||||
}
|
||||
|
||||
public IEnumerable<T> WithInterface<T>()
|
||||
public IReadOnlyCollection<T> WithInterface<T>()
|
||||
{
|
||||
if (data.TryGetValue(typeof(T), out var objs))
|
||||
return objs.Cast<T>();
|
||||
if (data.TryGetValue(typeof(T), out var container))
|
||||
return ((TypeContainer<T>)container).Objects;
|
||||
return Array.Empty<T>();
|
||||
}
|
||||
|
||||
@@ -92,18 +96,19 @@ namespace OpenRA.Primitives
|
||||
|
||||
void InnerRemove(Type t, object val)
|
||||
{
|
||||
if (!data.TryGetValue(t, out var objs))
|
||||
if (!data.TryGetValue(t, out var container))
|
||||
return;
|
||||
objs.Remove(val);
|
||||
if (objs.Count == 0)
|
||||
|
||||
container.Remove(val);
|
||||
if (container.Count == 0)
|
||||
data.Remove(t);
|
||||
}
|
||||
|
||||
public void TrimExcess()
|
||||
{
|
||||
data.TrimExcess();
|
||||
foreach (var objs in data.Values)
|
||||
objs.TrimExcess();
|
||||
foreach (var t in data.Keys)
|
||||
InnerGet(t).TrimExcess();
|
||||
}
|
||||
|
||||
public IEnumerator<object> GetEnumerator()
|
||||
@@ -115,6 +120,36 @@ namespace OpenRA.Primitives
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
interface ITypeContainer
|
||||
{
|
||||
int Count { get; }
|
||||
void Add(object value);
|
||||
void Remove(object value);
|
||||
void TrimExcess();
|
||||
}
|
||||
|
||||
sealed class TypeContainer<T> : ITypeContainer
|
||||
{
|
||||
public List<T> Objects { get; } = new List<T>();
|
||||
|
||||
public int Count => Objects.Count;
|
||||
|
||||
public void Add(object value)
|
||||
{
|
||||
Objects.Add((T)value);
|
||||
}
|
||||
|
||||
public void Remove(object value)
|
||||
{
|
||||
Objects.Remove((T)value);
|
||||
}
|
||||
|
||||
public void TrimExcess()
|
||||
{
|
||||
Objects.TrimExcess();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class TypeExts
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace OpenRA
|
||||
public SpriteRenderer WorldSpriteRenderer { get; }
|
||||
public RgbaSpriteRenderer WorldRgbaSpriteRenderer { get; }
|
||||
public RgbaColorRenderer WorldRgbaColorRenderer { get; }
|
||||
public ModelRenderer WorldModelRenderer { get; }
|
||||
public IRenderer[] WorldRenderers = Array.Empty<IRenderer>();
|
||||
public RgbaColorRenderer RgbaColorRenderer { get; }
|
||||
public SpriteRenderer SpriteRenderer { get; }
|
||||
public RgbaSpriteRenderer RgbaSpriteRenderer { get; }
|
||||
@@ -41,10 +41,13 @@ namespace OpenRA
|
||||
internal IGraphicsContext Context { get; }
|
||||
|
||||
internal int SheetSize { get; }
|
||||
internal int TempBufferSize { get; }
|
||||
internal int TempVertexBufferSize { get; }
|
||||
internal int TempIndexBufferSize { get; }
|
||||
|
||||
readonly IVertexBuffer<Vertex> tempBuffer;
|
||||
readonly IVertexBuffer<Vertex> tempVertexBuffer;
|
||||
readonly IIndexBuffer quadIndexBuffer;
|
||||
readonly Stack<Rectangle> scissorState = new();
|
||||
readonly ITexture worldBufferSnapshot;
|
||||
|
||||
IFrameBuffer screenBuffer;
|
||||
Sprite screenSprite;
|
||||
@@ -58,6 +61,15 @@ namespace OpenRA
|
||||
public Size WorldFrameBufferSize => worldSheet.Size;
|
||||
public int WorldDownscaleFactor { get; private set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Copies and returns the currently rendered world state as a temporary texture.
|
||||
/// </summary>
|
||||
public ITexture WorldBufferSnapshot()
|
||||
{
|
||||
worldBufferSnapshot.SetDataFromReadBuffer(new Rectangle(int2.Zero, worldSheet.Size));
|
||||
return worldBufferSnapshot;
|
||||
}
|
||||
|
||||
SheetBuilder fontSheetBuilder;
|
||||
readonly IPlatform platform;
|
||||
|
||||
@@ -75,24 +87,28 @@ namespace OpenRA
|
||||
this.platform = platform;
|
||||
var resolution = GetResolution(graphicSettings);
|
||||
|
||||
TempVertexBufferSize = graphicSettings.BatchSize - graphicSettings.BatchSize % 4;
|
||||
TempIndexBufferSize = TempVertexBufferSize / 4 * 6;
|
||||
|
||||
Window = platform.CreateWindow(new Size(resolution.Width, resolution.Height),
|
||||
graphicSettings.Mode, graphicSettings.UIScale, graphicSettings.BatchSize,
|
||||
graphicSettings.VideoDisplay, graphicSettings.GLProfile, !graphicSettings.DisableLegacyGL);
|
||||
graphicSettings.Mode, graphicSettings.UIScale, TempVertexBufferSize, TempIndexBufferSize,
|
||||
graphicSettings.VideoDisplay, graphicSettings.GLProfile);
|
||||
|
||||
Context = Window.Context;
|
||||
|
||||
TempBufferSize = graphicSettings.BatchSize;
|
||||
SheetSize = graphicSettings.SheetSize;
|
||||
|
||||
WorldSpriteRenderer = new SpriteRenderer(this, Context.CreateShader("combined"));
|
||||
var combinedBindings = new CombinedShaderBindings();
|
||||
WorldSpriteRenderer = new SpriteRenderer(this, Context.CreateShader(combinedBindings));
|
||||
WorldRgbaSpriteRenderer = new RgbaSpriteRenderer(WorldSpriteRenderer);
|
||||
WorldRgbaColorRenderer = new RgbaColorRenderer(WorldSpriteRenderer);
|
||||
WorldModelRenderer = new ModelRenderer(this, Context.CreateShader("model"));
|
||||
SpriteRenderer = new SpriteRenderer(this, Context.CreateShader("combined"));
|
||||
SpriteRenderer = new SpriteRenderer(this, Context.CreateShader(combinedBindings));
|
||||
RgbaSpriteRenderer = new RgbaSpriteRenderer(SpriteRenderer);
|
||||
RgbaColorRenderer = new RgbaColorRenderer(SpriteRenderer);
|
||||
|
||||
tempBuffer = Context.CreateVertexBuffer(TempBufferSize);
|
||||
tempVertexBuffer = Context.CreateVertexBuffer<Vertex>(TempVertexBufferSize);
|
||||
quadIndexBuffer = Context.CreateIndexBuffer(Util.CreateQuadIndices(TempIndexBufferSize / 6));
|
||||
worldBufferSnapshot = Context.CreateTexture();
|
||||
}
|
||||
|
||||
static Size GetResolution(GraphicSettings graphicsSettings)
|
||||
@@ -253,8 +269,6 @@ namespace OpenRA
|
||||
if (lastWorldViewport != worldViewport)
|
||||
{
|
||||
WorldSpriteRenderer.SetViewportParams(worldSheet.Size, WorldDownscaleFactor, depthMargin, worldViewport.Location);
|
||||
WorldModelRenderer.SetViewportParams();
|
||||
|
||||
lastWorldViewport = worldViewport;
|
||||
}
|
||||
|
||||
@@ -300,9 +314,11 @@ namespace OpenRA
|
||||
Flush();
|
||||
currentPaletteTexture = palette.Texture;
|
||||
|
||||
SpriteRenderer.SetPalette(currentPaletteTexture, palette.ColorShifts);
|
||||
WorldSpriteRenderer.SetPalette(currentPaletteTexture, palette.ColorShifts);
|
||||
WorldModelRenderer.SetPalette(currentPaletteTexture);
|
||||
SpriteRenderer.SetPalette(palette);
|
||||
WorldSpriteRenderer.SetPalette(palette);
|
||||
|
||||
foreach (var r in WorldRenderers)
|
||||
r.SetPalette(palette);
|
||||
}
|
||||
|
||||
public void EndFrame(IInputHandler inputHandler)
|
||||
@@ -326,27 +342,32 @@ namespace OpenRA
|
||||
renderType = RenderType.None;
|
||||
}
|
||||
|
||||
public void DrawBatch(Vertex[] vertices, int numVertices, PrimitiveType type)
|
||||
{
|
||||
tempBuffer.SetData(vertices, numVertices);
|
||||
DrawBatch(tempBuffer, 0, numVertices, type);
|
||||
}
|
||||
|
||||
public void DrawBatch(ref Vertex[] vertices, int numVertices, PrimitiveType type)
|
||||
{
|
||||
tempBuffer.SetData(ref vertices, numVertices);
|
||||
DrawBatch(tempBuffer, 0, numVertices, type);
|
||||
}
|
||||
|
||||
public void DrawBatch<T>(IVertexBuffer<T> vertices,
|
||||
public void DrawBatch<T>(IVertexBuffer<T> vertices, IShader shader,
|
||||
int firstVertex, int numVertices, PrimitiveType type)
|
||||
where T : struct
|
||||
{
|
||||
vertices.Bind();
|
||||
shader.Bind();
|
||||
Context.DrawPrimitives(type, firstVertex, numVertices);
|
||||
PerfHistory.Increment("batches", 1);
|
||||
}
|
||||
|
||||
public void DrawQuadBatch(ref Vertex[] vertices, IShader shader, int numVertices)
|
||||
{
|
||||
tempVertexBuffer.SetData(ref vertices, numVertices);
|
||||
DrawQuadBatch(tempVertexBuffer, quadIndexBuffer, shader, numVertices / 4 * 6, 0);
|
||||
}
|
||||
|
||||
public void DrawQuadBatch<T>(IVertexBuffer<T> vertices, IIndexBuffer indices, IShader shader, int numIndices, int start)
|
||||
where T : struct
|
||||
{
|
||||
vertices.Bind();
|
||||
indices.Bind();
|
||||
shader.Bind();
|
||||
Context.DrawElements(numIndices, start);
|
||||
PerfHistory.Increment("batches", 1);
|
||||
}
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
CurrentBatchRenderer = null;
|
||||
@@ -374,9 +395,19 @@ namespace OpenRA
|
||||
}
|
||||
}
|
||||
|
||||
public IVertexBuffer<Vertex> CreateVertexBuffer(int length)
|
||||
public IFrameBuffer CreateFrameBuffer(Size s)
|
||||
{
|
||||
return Context.CreateVertexBuffer(length);
|
||||
return Context.CreateFrameBuffer(s);
|
||||
}
|
||||
|
||||
public IShader CreateShader(IShaderBindings bindings)
|
||||
{
|
||||
return Context.CreateShader(bindings);
|
||||
}
|
||||
|
||||
public IVertexBuffer<T> CreateVertexBuffer<T>(int length) where T : struct
|
||||
{
|
||||
return Context.CreateVertexBuffer<T>(length);
|
||||
}
|
||||
|
||||
public void EnableScissor(Rectangle rect)
|
||||
@@ -503,8 +534,11 @@ namespace OpenRA
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
WorldModelRenderer.Dispose();
|
||||
tempBuffer.Dispose();
|
||||
worldBuffer?.Dispose();
|
||||
screenBuffer.Dispose();
|
||||
worldBufferSnapshot.Dispose();
|
||||
tempVertexBuffer.Dispose();
|
||||
quadIndexBuffer.Dispose();
|
||||
fontSheetBuilder?.Dispose();
|
||||
if (Fonts != null)
|
||||
foreach (var font in Fonts.Values)
|
||||
|
||||
@@ -74,14 +74,17 @@ namespace OpenRA.Scripting
|
||||
/// Provides global bindings in Lua code.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Instance methods and properties declared in derived classes will be made available in Lua. Use
|
||||
/// <see cref="ScriptGlobalAttribute"/> on your derived class to specify the name exposed in Lua. It is recommended
|
||||
/// to apply <see cref="DescAttribute"/> against each method or property to provide a description of what it does.
|
||||
///
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Any parameters to your method that are <see cref="LuaValue"/>s will be disposed automatically when your method
|
||||
/// completes. If you need to return any of these values, or need them to live longer than your method, you must
|
||||
/// use <see cref="LuaValue.CopyReference"/> to get your own copy of the value. Any copied values you return will
|
||||
/// be disposed automatically, but you assume responsibility for disposing any other copies.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class ScriptGlobal : ScriptObjectWrapper
|
||||
{
|
||||
@@ -99,7 +102,7 @@ namespace OpenRA.Scripting
|
||||
if (names.Length != 1)
|
||||
throw new InvalidOperationException($"[ScriptGlobal] attribute not found for global table '{type}'");
|
||||
|
||||
Name = names.First().Name;
|
||||
Name = names[0].Name;
|
||||
Bind(new[] { this });
|
||||
}
|
||||
|
||||
@@ -111,7 +114,7 @@ namespace OpenRA.Scripting
|
||||
{
|
||||
using (var luaObject = a.ToLuaValue(Context))
|
||||
using (var filterResult = filter.Call(luaObject))
|
||||
using (var result = filterResult.First())
|
||||
using (var result = filterResult[0])
|
||||
return result.ToBoolean();
|
||||
});
|
||||
}
|
||||
@@ -211,7 +214,7 @@ namespace OpenRA.Scripting
|
||||
var ctor = b.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c =>
|
||||
{
|
||||
var p = c.GetParameters();
|
||||
return p.Length == 1 && p.First().ParameterType == typeof(ScriptContext);
|
||||
return p.Length == 1 && p[0].ParameterType == typeof(ScriptContext);
|
||||
});
|
||||
|
||||
if (ctor == null)
|
||||
|
||||
@@ -125,8 +125,8 @@ namespace OpenRA.Scripting
|
||||
public static IEnumerable<MemberInfo> WrappableMembers(Type t)
|
||||
{
|
||||
// Only expose defined public non-static methods that were explicitly declared by the author
|
||||
var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
|
||||
foreach (var mi in t.GetMembers(flags))
|
||||
const BindingFlags Flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
|
||||
foreach (var mi in t.GetMembers(Flags))
|
||||
{
|
||||
// Properties are always wrappable
|
||||
if (mi is PropertyInfo)
|
||||
@@ -150,7 +150,7 @@ namespace OpenRA.Scripting
|
||||
// Remove the namespace and the trailing "Info"
|
||||
return types.SelectMany(i => i.GetGenericArguments())
|
||||
.Select(g => g.Name.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault())
|
||||
.Select(s => s.EndsWith("Info") ? s.Remove(s.Length - 4, 4) : s)
|
||||
.Select(s => s.EndsWith("Info", StringComparison.Ordinal) ? s.Remove(s.Length - 4, 4) : s)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,13 +40,10 @@ namespace OpenRA.Scripting
|
||||
t = nullable;
|
||||
|
||||
// Value wraps a CLR object
|
||||
if (value.TryGetClrObject(out var temp))
|
||||
if (value.TryGetClrObject(out var temp) && temp.GetType() == t)
|
||||
{
|
||||
if (temp.GetType() == t)
|
||||
{
|
||||
clrObject = temp;
|
||||
return true;
|
||||
}
|
||||
clrObject = temp;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value is LuaNil && !t.IsValueType)
|
||||
|
||||
@@ -62,10 +62,7 @@ namespace OpenRA.Traits
|
||||
|
||||
public static Actor WithHighestSelectionPriority(this IEnumerable<ActorBoundsPair> actors, int2 selectionPixel, Modifiers modifiers)
|
||||
{
|
||||
if (!actors.Any())
|
||||
return null;
|
||||
|
||||
return actors.MaxBy(a => CalculateActorSelectionPriority(a.Actor.Info, a.Bounds, selectionPixel, modifiers)).Actor;
|
||||
return actors.MaxByOrDefault(a => CalculateActorSelectionPriority(a.Actor.Info, a.Bounds, selectionPixel, modifiers)).Actor;
|
||||
}
|
||||
|
||||
public static FrozenActor WithHighestSelectionPriority(this IEnumerable<FrozenActor> actors, int2 selectionPixel, Modifiers modifiers)
|
||||
|
||||
@@ -60,11 +60,11 @@ namespace OpenRA.Server
|
||||
static byte[] CreatePingFrame()
|
||||
{
|
||||
var ms = new MemoryStream(21);
|
||||
ms.WriteArray(BitConverter.GetBytes(13));
|
||||
ms.WriteArray(BitConverter.GetBytes(0));
|
||||
ms.WriteArray(BitConverter.GetBytes(0));
|
||||
ms.Write(13);
|
||||
ms.Write(0);
|
||||
ms.Write(0);
|
||||
ms.WriteByte((byte)OrderType.Ping);
|
||||
ms.WriteArray(BitConverter.GetBytes(Game.RunTime));
|
||||
ms.Write(Game.RunTime);
|
||||
return ms.GetBuffer();
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ namespace OpenRA.Server
|
||||
frame = BitConverter.ToInt32(bytes, 4);
|
||||
state = ReceiveState.Data;
|
||||
|
||||
if (expectLength < 0 || (server.Type != ServerType.Local && expectLength > MaxOrderLength))
|
||||
if (expectLength < 0 || (server.IsMultiplayer && expectLength > MaxOrderLength))
|
||||
{
|
||||
Log.Write("server", $"Closing socket connection to {EndPoint} because of excessive order length: {expectLength}");
|
||||
return;
|
||||
@@ -153,9 +153,8 @@ namespace OpenRA.Server
|
||||
return;
|
||||
|
||||
// Regularly check player ping
|
||||
if (lastPingSent.ElapsedMilliseconds > 1000)
|
||||
if (TrySendData(CreatePingFrame()))
|
||||
lastPingSent.Restart();
|
||||
if (lastPingSent.ElapsedMilliseconds > 1000 && TrySendData(CreatePingFrame()))
|
||||
lastPingSent.Restart();
|
||||
|
||||
// Send all data immediately, we will block again on read
|
||||
while (sendQueue.TryTake(out var data, 0))
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace OpenRA.Server
|
||||
ticksPerInterval = Interval / timestep;
|
||||
|
||||
this.players = players.ToList();
|
||||
baselinePlayer = this.players.First();
|
||||
baselinePlayer = this.players[0];
|
||||
|
||||
foreach (var player in this.players)
|
||||
{
|
||||
@@ -128,7 +128,7 @@ namespace OpenRA.Server
|
||||
players.Remove(player);
|
||||
if (player == baselinePlayer && players.Count > 0)
|
||||
{
|
||||
var newBaseline = players.First();
|
||||
var newBaseline = players[0];
|
||||
Interlocked.Exchange(ref baselinePlayer, newBaseline);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,6 @@ namespace OpenRA.Server
|
||||
// The protocol for server and world orders
|
||||
// This applies after the handshake has completed, and is provided to support
|
||||
// alternative server implementations that wish to support multiple versions in parallel
|
||||
public const int Orders = 20;
|
||||
public const int Orders = 21;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,9 @@ namespace OpenRA.Server
|
||||
public enum ServerType
|
||||
{
|
||||
Local = 0,
|
||||
Multiplayer = 1,
|
||||
Dedicated = 2
|
||||
Skirmish = 1,
|
||||
Multiplayer = 2,
|
||||
Dedicated = 3
|
||||
}
|
||||
|
||||
public sealed class Server
|
||||
@@ -117,6 +118,7 @@ namespace OpenRA.Server
|
||||
|
||||
public readonly MersenneTwister Random = new();
|
||||
public readonly ServerType Type;
|
||||
public bool IsMultiplayer => Type == ServerType.Dedicated || Type == ServerType.Multiplayer;
|
||||
|
||||
public readonly List<Connection> Conns = new();
|
||||
|
||||
@@ -128,7 +130,8 @@ namespace OpenRA.Server
|
||||
// Managed by LobbyCommands
|
||||
public MapPreview Map;
|
||||
public readonly MapStatusCache MapStatusCache;
|
||||
public GameSave GameSave = null;
|
||||
public GameSave GameSave;
|
||||
public HashSet<string> MapPool;
|
||||
|
||||
// Default to the next frame for ServerType.Local - MP servers take the value from the selected GameSpeed.
|
||||
public int OrderLatency = 1;
|
||||
@@ -247,12 +250,9 @@ namespace OpenRA.Server
|
||||
{
|
||||
listener.Server.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception ex) when (ex is SocketException || ex is ArgumentException)
|
||||
{
|
||||
if (ex is SocketException || ex is ArgumentException)
|
||||
Log.Write("server", $"Failed to set socket option on {endpoint}: {ex.Message}");
|
||||
else
|
||||
throw;
|
||||
Log.Write("server", $"Failed to set socket option on {endpoint}: {ex.Message}");
|
||||
}
|
||||
|
||||
listener.Start();
|
||||
@@ -282,7 +282,8 @@ namespace OpenRA.Server
|
||||
}
|
||||
}
|
||||
}
|
||||
}) { Name = $"Connection listener ({listener.LocalEndpoint})", IsBackground = true }.Start();
|
||||
})
|
||||
{ Name = $"Connection listener ({listener.LocalEndpoint})", IsBackground = true }.Start();
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
@@ -305,10 +306,10 @@ namespace OpenRA.Server
|
||||
|
||||
randomSeed = (int)DateTime.Now.ToBinary();
|
||||
|
||||
if (type != ServerType.Local && settings.EnableGeoIP)
|
||||
if (IsMultiplayer && settings.EnableGeoIP)
|
||||
GeoIP.Initialize();
|
||||
|
||||
if (type != ServerType.Local)
|
||||
if (IsMultiplayer)
|
||||
Nat.TryForwardPort(Settings.ListenPort, Settings.ListenPort);
|
||||
|
||||
foreach (var trait in modData.Manifest.ServerTraits)
|
||||
@@ -316,7 +317,6 @@ namespace OpenRA.Server
|
||||
|
||||
serverTraits.TrimExcess();
|
||||
|
||||
Map = ModData.MapCache[settings.Map];
|
||||
MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks);
|
||||
|
||||
playerMessageTracker = new PlayerMessageTracker(this, DispatchOrdersToClient, SendLocalizedMessageTo);
|
||||
@@ -327,8 +327,6 @@ namespace OpenRA.Server
|
||||
GlobalSettings =
|
||||
{
|
||||
RandomSeed = randomSeed,
|
||||
Map = Map.Uid,
|
||||
MapStatus = Session.MapStatus.Unknown,
|
||||
ServerName = settings.Name,
|
||||
EnableSingleplayer = settings.EnableSingleplayer || Type != ServerType.Dedicated,
|
||||
EnableSyncReports = settings.EnableSyncReports,
|
||||
@@ -348,8 +346,7 @@ namespace OpenRA.Server
|
||||
|
||||
new Thread(_ =>
|
||||
{
|
||||
// Initial status is set off the main thread to avoid triggering a load screen when joining a skirmish game
|
||||
LobbyInfo.GlobalSettings.MapStatus = MapStatusCache[Map];
|
||||
// Note: at least one of these is required to set the initial LobbyInfo.Map and MapStatus
|
||||
foreach (var t in serverTraits.WithInterface<INotifyServerStart>())
|
||||
t.ServerStarted(this);
|
||||
|
||||
@@ -386,7 +383,7 @@ namespace OpenRA.Server
|
||||
if (State == ServerState.ShuttingDown)
|
||||
{
|
||||
EndGame();
|
||||
if (type != ServerType.Local)
|
||||
if (IsMultiplayer)
|
||||
Nat.TryRemovePortForward();
|
||||
break;
|
||||
}
|
||||
@@ -439,8 +436,8 @@ namespace OpenRA.Server
|
||||
{
|
||||
// Send handshake and client index.
|
||||
var ms = new MemoryStream(8);
|
||||
ms.WriteArray(BitConverter.GetBytes(ProtocolVersion.Handshake));
|
||||
ms.WriteArray(BitConverter.GetBytes(newConn.PlayerIndex));
|
||||
ms.Write(ProtocolVersion.Handshake);
|
||||
ms.Write(newConn.PlayerIndex);
|
||||
newConn.TrySendData(ms.ToArray());
|
||||
|
||||
// Dispatch a handshake order
|
||||
@@ -466,7 +463,7 @@ namespace OpenRA.Server
|
||||
Conns.Add(newConn);
|
||||
}
|
||||
|
||||
void ValidateClient(Connection newConn, string data)
|
||||
void ValidateClient(Connection newConn, string data, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -479,7 +476,7 @@ namespace OpenRA.Server
|
||||
return;
|
||||
}
|
||||
|
||||
var handshake = HandshakeResponse.Deserialize(data);
|
||||
var handshake = HandshakeResponse.Deserialize(data, name);
|
||||
|
||||
if (!string.IsNullOrEmpty(Settings.Password) && handshake.Password != Settings.Password)
|
||||
{
|
||||
@@ -494,7 +491,7 @@ namespace OpenRA.Server
|
||||
{
|
||||
Name = OpenRA.Settings.SanitizedPlayerName(handshake.Client.Name),
|
||||
IPAddress = ipAddress.ToString(),
|
||||
AnonymizedIPAddress = Type != ServerType.Local && Settings.ShareAnonymizedIPs ? Session.AnonymizeIP(ipAddress) : null,
|
||||
AnonymizedIPAddress = IsMultiplayer && Settings.ShareAnonymizedIPs ? Session.AnonymizeIP(ipAddress) : null,
|
||||
Location = GeoIP.LookupCountry(ipAddress),
|
||||
Index = newConn.PlayerIndex,
|
||||
PreferredColor = handshake.Client.PreferredColor,
|
||||
@@ -583,8 +580,7 @@ namespace OpenRA.Server
|
||||
|
||||
Log.Write("server", $"{client.Name} ({newConn.EndPoint}) has joined the game.");
|
||||
|
||||
if (Type != ServerType.Local)
|
||||
SendLocalizedMessage(Joined, Translation.Arguments("player", client.Name));
|
||||
SendLocalizedMessage(Joined, Translation.Arguments("player", client.Name));
|
||||
|
||||
if (Type == ServerType.Dedicated)
|
||||
{
|
||||
@@ -607,7 +603,7 @@ namespace OpenRA.Server
|
||||
}
|
||||
}
|
||||
|
||||
if (Type == ServerType.Local)
|
||||
if (!IsMultiplayer)
|
||||
{
|
||||
// Local servers can only be joined by the local client, so we can trust their identity without validation
|
||||
client.Fingerprint = handshake.Fingerprint;
|
||||
@@ -622,10 +618,11 @@ namespace OpenRA.Server
|
||||
try
|
||||
{
|
||||
var httpClient = HttpClientFactory.Create();
|
||||
var httpResponseMessage = await httpClient.GetAsync(playerDatabase.Profile + handshake.Fingerprint);
|
||||
var url = playerDatabase.Profile + handshake.Fingerprint;
|
||||
var httpResponseMessage = await httpClient.GetAsync(url);
|
||||
var result = await httpResponseMessage.Content.ReadAsStreamAsync();
|
||||
|
||||
var yaml = MiniYaml.FromStream(result).First();
|
||||
var yaml = MiniYaml.FromStream(result, url).First();
|
||||
if (yaml.Key == "Player")
|
||||
{
|
||||
profile = FieldLoader.Load<PlayerProfile>(yaml.Value);
|
||||
@@ -708,19 +705,19 @@ namespace OpenRA.Server
|
||||
static byte[] CreateFrame(int client, int frame, byte[] data)
|
||||
{
|
||||
var ms = new MemoryStream(data.Length + 12);
|
||||
ms.WriteArray(BitConverter.GetBytes(data.Length + 4));
|
||||
ms.WriteArray(BitConverter.GetBytes(client));
|
||||
ms.WriteArray(BitConverter.GetBytes(frame));
|
||||
ms.WriteArray(data);
|
||||
ms.Write(data.Length + 4);
|
||||
ms.Write(client);
|
||||
ms.Write(frame);
|
||||
ms.Write(data);
|
||||
return ms.GetBuffer();
|
||||
}
|
||||
|
||||
static byte[] CreateAckFrame(int frame, byte count)
|
||||
{
|
||||
var ms = new MemoryStream(14);
|
||||
ms.WriteArray(BitConverter.GetBytes(6));
|
||||
ms.WriteArray(BitConverter.GetBytes(0));
|
||||
ms.WriteArray(BitConverter.GetBytes(frame));
|
||||
ms.Write(6);
|
||||
ms.Write(0);
|
||||
ms.Write(frame);
|
||||
ms.WriteByte((byte)OrderType.Ack);
|
||||
ms.WriteByte(count);
|
||||
return ms.GetBuffer();
|
||||
@@ -729,9 +726,9 @@ namespace OpenRA.Server
|
||||
static byte[] CreateTickScaleFrame(float scale)
|
||||
{
|
||||
var ms = new MemoryStream(17);
|
||||
ms.WriteArray(BitConverter.GetBytes(9));
|
||||
ms.WriteArray(BitConverter.GetBytes(0));
|
||||
ms.WriteArray(BitConverter.GetBytes(0));
|
||||
ms.Write(9);
|
||||
ms.Write(0);
|
||||
ms.Write(0);
|
||||
ms.WriteByte((byte)OrderType.TickScale);
|
||||
ms.Write(scale);
|
||||
return ms.GetBuffer();
|
||||
@@ -880,13 +877,13 @@ namespace OpenRA.Server
|
||||
|
||||
public void DispatchServerOrdersToClients(byte[] data, int frame = 0)
|
||||
{
|
||||
var from = 0;
|
||||
var frameData = CreateFrame(from, frame, data);
|
||||
const int From = 0;
|
||||
var frameData = CreateFrame(From, frame, data);
|
||||
foreach (var c in Conns.ToList())
|
||||
if (c.Validated)
|
||||
DispatchFrameToClient(c, from, frameData);
|
||||
DispatchFrameToClient(c, From, frameData);
|
||||
|
||||
RecordOrder(frame, data, from);
|
||||
RecordOrder(frame, data, From);
|
||||
}
|
||||
|
||||
public void ReceiveOrders(Connection conn, int frame, byte[] data)
|
||||
@@ -972,7 +969,7 @@ namespace OpenRA.Server
|
||||
|
||||
void WriteLineWithTimeStamp(string line)
|
||||
{
|
||||
Console.WriteLine($"[{DateTime.Now.ToString(Settings.TimestampFormat)}] {line}");
|
||||
Console.WriteLine($"[{DateTime.Now.ToString(Settings.TimestampFormat, CultureInfo.CurrentCulture)}] {line}");
|
||||
}
|
||||
|
||||
void InterpretServerOrder(Connection conn, Order o)
|
||||
@@ -984,7 +981,7 @@ namespace OpenRA.Server
|
||||
if (!conn.Validated)
|
||||
{
|
||||
if (o.OrderString == "HandshakeResponse")
|
||||
ValidateClient(conn, o.TargetString);
|
||||
ValidateClient(conn, o.TargetString, o.OrderString);
|
||||
else
|
||||
{
|
||||
Log.Write("server", $"Rejected connection from {conn.EndPoint}; Order `{o.OrderString}` is not a `HandshakeResponse`.");
|
||||
@@ -997,70 +994,39 @@ namespace OpenRA.Server
|
||||
switch (o.OrderString)
|
||||
{
|
||||
case "Command":
|
||||
{
|
||||
if (!InterpretCommand(o.TargetString, conn))
|
||||
{
|
||||
var handledBy = serverTraits.WithInterface<IInterpretCommand>()
|
||||
.FirstOrDefault(t => t.InterpretCommand(this, conn, GetClient(conn), o.TargetString));
|
||||
|
||||
if (handledBy == null)
|
||||
{
|
||||
Log.Write("server", $"Unknown server command: {o.TargetString}");
|
||||
SendLocalizedMessageTo(conn, UnknownServerCommand, Translation.Arguments("command", o.TargetString));
|
||||
}
|
||||
|
||||
break;
|
||||
Log.Write("server", $"Unknown server command: {o.TargetString}");
|
||||
SendLocalizedMessageTo(conn, UnknownServerCommand, Translation.Arguments("command", o.TargetString));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Chat":
|
||||
{
|
||||
if (Type == ServerType.Local || !playerMessageTracker.IsPlayerAtFloodLimit(conn))
|
||||
DispatchOrdersToClients(conn, 0, o.Serialize());
|
||||
{
|
||||
if (!IsMultiplayer || !playerMessageTracker.IsPlayerAtFloodLimit(conn))
|
||||
DispatchOrdersToClients(conn, 0, o.Serialize());
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "GameSaveTraitData":
|
||||
{
|
||||
if (GameSave != null)
|
||||
{
|
||||
if (GameSave != null)
|
||||
{
|
||||
var data = MiniYaml.FromString(o.TargetString)[0];
|
||||
GameSave.AddTraitData(int.Parse(data.Key), data.Value);
|
||||
}
|
||||
|
||||
break;
|
||||
var data = MiniYaml.FromString(o.TargetString, o.OrderString)[0];
|
||||
GameSave.AddTraitData(OpenRA.Exts.ParseInt32Invariant(data.Key), data.Value);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "CreateGameSave":
|
||||
{
|
||||
if (GameSave != null)
|
||||
{
|
||||
if (GameSave != null)
|
||||
{
|
||||
// Sanitize potentially malicious input
|
||||
var filename = o.TargetString;
|
||||
var invalidIndex = -1;
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
while ((invalidIndex = filename.IndexOfAny(invalidChars)) != -1)
|
||||
filename = filename.Remove(invalidIndex, 1);
|
||||
|
||||
var baseSavePath = Path.Combine(
|
||||
Platform.SupportDir,
|
||||
"Saves",
|
||||
ModData.Manifest.Id,
|
||||
ModData.Manifest.Metadata.Version);
|
||||
|
||||
if (!Directory.Exists(baseSavePath))
|
||||
Directory.CreateDirectory(baseSavePath);
|
||||
|
||||
GameSave.Save(Path.Combine(baseSavePath, filename));
|
||||
DispatchServerOrdersToClients(Order.FromTargetString("GameSaved", filename, true));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "LoadGameSave":
|
||||
{
|
||||
if (Type == ServerType.Dedicated || State >= ServerState.GameStarted)
|
||||
break;
|
||||
|
||||
// Sanitize potentially malicious input
|
||||
var filename = o.TargetString;
|
||||
var invalidIndex = -1;
|
||||
@@ -1068,62 +1034,90 @@ namespace OpenRA.Server
|
||||
while ((invalidIndex = filename.IndexOfAny(invalidChars)) != -1)
|
||||
filename = filename.Remove(invalidIndex, 1);
|
||||
|
||||
var savePath = Path.Combine(
|
||||
var baseSavePath = Path.Combine(
|
||||
Platform.SupportDir,
|
||||
"Saves",
|
||||
ModData.Manifest.Id,
|
||||
ModData.Manifest.Metadata.Version,
|
||||
filename);
|
||||
ModData.Manifest.Metadata.Version);
|
||||
|
||||
GameSave = new GameSave(savePath);
|
||||
LobbyInfo.GlobalSettings = GameSave.GlobalSettings;
|
||||
LobbyInfo.Slots = GameSave.Slots;
|
||||
if (!Directory.Exists(baseSavePath))
|
||||
Directory.CreateDirectory(baseSavePath);
|
||||
|
||||
// Reassign clients to slots
|
||||
// - Bot ordering is preserved
|
||||
// - Humans are assigned on a first-come-first-serve basis
|
||||
// - Leftover humans become spectators
|
||||
|
||||
// Start by removing all bots and assigning all players as spectators
|
||||
foreach (var c in LobbyInfo.Clients)
|
||||
{
|
||||
if (c.Bot != null)
|
||||
LobbyInfo.Clients.Remove(c);
|
||||
else
|
||||
c.Slot = null;
|
||||
}
|
||||
|
||||
// Rebuild/remap the saved client state
|
||||
// TODO: Multiplayer saves should leave all humans as spectators so they can manually pick slots
|
||||
var adminClientIndex = LobbyInfo.Clients.First(c => c.IsAdmin).Index;
|
||||
foreach (var kv in GameSave.SlotClients)
|
||||
{
|
||||
if (kv.Value.Bot != null)
|
||||
{
|
||||
var bot = new Session.Client()
|
||||
{
|
||||
Index = ChooseFreePlayerIndex(),
|
||||
State = Session.ClientState.NotReady,
|
||||
BotControllerClientIndex = adminClientIndex
|
||||
};
|
||||
|
||||
kv.Value.ApplyTo(bot);
|
||||
LobbyInfo.Clients.Add(bot);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This will throw if the server doesn't have enough human clients to fill all player slots
|
||||
// See TODO above - this isn't a problem in practice because MP saves won't use this
|
||||
var client = LobbyInfo.Clients.First(c => c.Slot == null);
|
||||
kv.Value.ApplyTo(client);
|
||||
}
|
||||
}
|
||||
|
||||
SyncLobbyInfo();
|
||||
SyncLobbyClients();
|
||||
|
||||
break;
|
||||
GameSave.Save(Path.Combine(baseSavePath, filename));
|
||||
DispatchServerOrdersToClients(Order.FromTargetString("GameSaved", filename, true));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "LoadGameSave":
|
||||
{
|
||||
if (Type == ServerType.Dedicated || State >= ServerState.GameStarted)
|
||||
break;
|
||||
|
||||
// Sanitize potentially malicious input
|
||||
var filename = o.TargetString;
|
||||
var invalidIndex = -1;
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
while ((invalidIndex = filename.IndexOfAny(invalidChars)) != -1)
|
||||
filename = filename.Remove(invalidIndex, 1);
|
||||
|
||||
var savePath = Path.Combine(
|
||||
Platform.SupportDir,
|
||||
"Saves",
|
||||
ModData.Manifest.Id,
|
||||
ModData.Manifest.Metadata.Version,
|
||||
filename);
|
||||
|
||||
GameSave = new GameSave(savePath);
|
||||
LobbyInfo.GlobalSettings = GameSave.GlobalSettings;
|
||||
LobbyInfo.Slots = GameSave.Slots;
|
||||
|
||||
// Reassign clients to slots
|
||||
// - Bot ordering is preserved
|
||||
// - Humans are assigned on a first-come-first-serve basis
|
||||
// - Leftover humans become spectators
|
||||
|
||||
// Start by removing all bots and assigning all players as spectators
|
||||
foreach (var c in LobbyInfo.Clients)
|
||||
{
|
||||
if (c.Bot != null)
|
||||
LobbyInfo.Clients.Remove(c);
|
||||
else
|
||||
c.Slot = null;
|
||||
}
|
||||
|
||||
// Rebuild/remap the saved client state
|
||||
// TODO: Multiplayer saves should leave all humans as spectators so they can manually pick slots
|
||||
var adminClientIndex = LobbyInfo.Clients.First(c => c.IsAdmin).Index;
|
||||
foreach (var kv in GameSave.SlotClients)
|
||||
{
|
||||
if (kv.Value.Bot != null)
|
||||
{
|
||||
var bot = new Session.Client()
|
||||
{
|
||||
Index = ChooseFreePlayerIndex(),
|
||||
State = Session.ClientState.NotReady,
|
||||
BotControllerClientIndex = adminClientIndex
|
||||
};
|
||||
|
||||
kv.Value.ApplyTo(bot);
|
||||
LobbyInfo.Clients.Add(bot);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This will throw if the server doesn't have enough human clients to fill all player slots
|
||||
// See TODO above - this isn't a problem in practice because MP saves won't use this
|
||||
var client = LobbyInfo.Clients.First(c => c.Slot == null);
|
||||
kv.Value.ApplyTo(client);
|
||||
}
|
||||
}
|
||||
|
||||
SyncLobbyInfo();
|
||||
SyncLobbyClients();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1258,7 +1252,7 @@ namespace OpenRA.Server
|
||||
lock (LobbyInfo)
|
||||
{
|
||||
// TODO: Only need to sync the specific client that has changed to avoid conflicts!
|
||||
var clientData = LobbyInfo.Clients.Select(client => client.Serialize()).ToList();
|
||||
var clientData = LobbyInfo.Clients.ConvertAll(client => client.Serialize());
|
||||
|
||||
DispatchServerOrdersToClients(Order.FromTargetString("SyncLobbyClients", clientData.WriteToString(), true));
|
||||
|
||||
@@ -1358,7 +1352,7 @@ namespace OpenRA.Server
|
||||
|
||||
State = ServerState.GameStarted;
|
||||
|
||||
if (Type != ServerType.Local)
|
||||
if (IsMultiplayer)
|
||||
OrderLatency = gameSpeed.OrderLatency;
|
||||
|
||||
if (GameSave == null && LobbyInfo.GlobalSettings.GameSavesEnabled)
|
||||
@@ -1372,8 +1366,8 @@ namespace OpenRA.Server
|
||||
{
|
||||
startGameData = new List<MiniYamlNode>()
|
||||
{
|
||||
new MiniYamlNode("SaveLastOrdersFrame", GameSave.LastOrdersFrame.ToString()),
|
||||
new MiniYamlNode("SaveSyncFrame", GameSave.LastSyncFrame.ToString())
|
||||
new("SaveLastOrdersFrame", GameSave.LastOrdersFrame.ToStringInvariant()),
|
||||
new("SaveSyncFrame", GameSave.LastSyncFrame.ToStringInvariant())
|
||||
}.WriteToString();
|
||||
}
|
||||
}
|
||||
@@ -1418,6 +1412,15 @@ namespace OpenRA.Server
|
||||
}
|
||||
}
|
||||
|
||||
public bool InterpretCommand(string command, Connection conn)
|
||||
{
|
||||
foreach (var t in serverTraits.WithInterface<IInterpretCommand>())
|
||||
if (t.InterpretCommand(this, conn, GetClient(conn), command))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public ConnectionTarget GetEndpointForLocalConnection()
|
||||
{
|
||||
var endpoints = new List<DnsEndPoint>();
|
||||
@@ -1435,6 +1438,27 @@ namespace OpenRA.Server
|
||||
return new ConnectionTarget(endpoints);
|
||||
}
|
||||
|
||||
public bool MapIsUnknown(string uid)
|
||||
{
|
||||
if (string.IsNullOrEmpty(uid))
|
||||
return true;
|
||||
|
||||
var status = ModData.MapCache[uid].Status;
|
||||
return status != MapStatus.Available && status != MapStatus.DownloadAvailable;
|
||||
}
|
||||
|
||||
public bool MapIsKnown(string uid)
|
||||
{
|
||||
if (string.IsNullOrEmpty(uid))
|
||||
return false;
|
||||
|
||||
if (MapPool != null && !MapPool.Contains(uid))
|
||||
return false;
|
||||
|
||||
var status = ModData.MapCache[uid].Status;
|
||||
return status == MapStatus.Available || status == MapStatus.DownloadAvailable;
|
||||
}
|
||||
|
||||
interface IServerEvent { void Invoke(Server server); }
|
||||
|
||||
sealed class ConnectionConnectEvent : IServerEvent
|
||||
@@ -1490,9 +1514,9 @@ namespace OpenRA.Server
|
||||
readonly int[] pingHistory;
|
||||
|
||||
// TODO: future net code changes
|
||||
#pragma warning disable IDE0052
|
||||
#pragma warning disable IDE0052
|
||||
readonly byte queueLength;
|
||||
#pragma warning restore IDE0052
|
||||
#pragma warning restore IDE0052
|
||||
|
||||
public ConnectionPingEvent(Connection connection, int[] pingHistory, byte queueLength)
|
||||
{
|
||||
|
||||
@@ -102,6 +102,9 @@ namespace OpenRA
|
||||
[Desc("For dedicated servers only, treat maps that fail the lint checks as invalid.")]
|
||||
public bool EnableLintChecks = true;
|
||||
|
||||
[Desc("For dedicated servers only, a comma separated list of map uids that are allowed to be used.")]
|
||||
public string[] MapPool = Array.Empty<string>();
|
||||
|
||||
[Desc("Delay in milliseconds before newly joined players can send chat messages.")]
|
||||
public int FloodLimitJoinCooldown = 5000;
|
||||
|
||||
@@ -210,9 +213,6 @@ namespace OpenRA
|
||||
[Desc("Disable operating-system provided cursor rendering.")]
|
||||
public bool DisableHardwareCursors = false;
|
||||
|
||||
[Desc("Disable legacy OpenGL 2.1 support.")]
|
||||
public bool DisableLegacyGL = true;
|
||||
|
||||
[Desc("Display index to use in a multi-monitor fullscreen setup.")]
|
||||
public int VideoDisplay = 0;
|
||||
|
||||
@@ -370,13 +370,14 @@ namespace OpenRA
|
||||
|
||||
public void Save()
|
||||
{
|
||||
var yamlCacheBuilder = yamlCache.ConvertAll(n => new MiniYamlNodeBuilder(n));
|
||||
foreach (var kv in Sections)
|
||||
{
|
||||
var sectionYaml = yamlCache.FirstOrDefault(x => x.Key == kv.Key);
|
||||
var sectionYaml = yamlCacheBuilder.FirstOrDefault(x => x.Key == kv.Key);
|
||||
if (sectionYaml == null)
|
||||
{
|
||||
sectionYaml = new MiniYamlNode(kv.Key, new MiniYaml(""));
|
||||
yamlCache.Add(sectionYaml);
|
||||
sectionYaml = new MiniYamlNodeBuilder(kv.Key, new MiniYamlBuilder(""));
|
||||
yamlCacheBuilder.Add(sectionYaml);
|
||||
}
|
||||
|
||||
var defaultValues = Activator.CreateInstance(kv.Value.GetType());
|
||||
@@ -393,27 +394,29 @@ namespace OpenRA
|
||||
else
|
||||
{
|
||||
// Update or add the custom value
|
||||
var fieldYaml = sectionYaml.Value.Nodes.FirstOrDefault(n => n.Key == fli.YamlName);
|
||||
var fieldYaml = sectionYaml.Value.NodeWithKeyOrDefault(fli.YamlName);
|
||||
if (fieldYaml != null)
|
||||
fieldYaml.Value.Value = serialized;
|
||||
else
|
||||
sectionYaml.Value.Nodes.Add(new MiniYamlNode(fli.YamlName, new MiniYaml(serialized)));
|
||||
sectionYaml.Value.Nodes.Add(new MiniYamlNodeBuilder(fli.YamlName, new MiniYamlBuilder(serialized)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var keysYaml = yamlCache.FirstOrDefault(x => x.Key == "Keys");
|
||||
var keysYaml = yamlCacheBuilder.FirstOrDefault(x => x.Key == "Keys");
|
||||
if (keysYaml == null)
|
||||
{
|
||||
keysYaml = new MiniYamlNode("Keys", new MiniYaml(""));
|
||||
yamlCache.Add(keysYaml);
|
||||
keysYaml = new MiniYamlNodeBuilder("Keys", new MiniYamlBuilder(""));
|
||||
yamlCacheBuilder.Add(keysYaml);
|
||||
}
|
||||
|
||||
keysYaml.Value.Nodes.Clear();
|
||||
foreach (var kv in Keys)
|
||||
keysYaml.Value.Nodes.Add(new MiniYamlNode(kv.Key, FieldSaver.FormatValue(kv.Value)));
|
||||
keysYaml.Value.Nodes.Add(new MiniYamlNodeBuilder(kv.Key, FieldSaver.FormatValue(kv.Value)));
|
||||
|
||||
yamlCache.WriteToFile(settingsFile);
|
||||
yamlCacheBuilder.WriteToFile(settingsFile);
|
||||
yamlCache.Clear();
|
||||
yamlCache.AddRange(yamlCacheBuilder.Select(n => n.Build()));
|
||||
}
|
||||
|
||||
static string SanitizedName(string dirty)
|
||||
@@ -424,7 +427,7 @@ namespace OpenRA
|
||||
var clean = dirty;
|
||||
|
||||
// reserved characters for MiniYAML and JSON
|
||||
var disallowedChars = new char[] { '#', '@', ':', '\n', '\t', '[', ']', '{', '}', '"', '`' };
|
||||
var disallowedChars = new char[] { '#', '@', ':', '\n', '\t', '[', ']', '{', '}', '<', '>', '"', '`' };
|
||||
foreach (var disallowedChar in disallowedChars)
|
||||
clean = clean.Replace(disallowedChar.ToString(), string.Empty);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user