Compare commits

..

28 Commits

Author SHA1 Message Date
hacker
d40f5292b6 Updated version
Some checks failed
Continuous Integration / Linux (.NET 6.0) (push) Has been cancelled
Continuous Integration / Linux (mono) (push) Has been cancelled
Continuous Integration / Windows (.NET 6.0) (push) Has been cancelled
2024-07-10 00:55:06 +01:00
hacker
3a75add1ba Added Dockerfile 2024-07-10 00:55:04 +01:00
hacker
b3bcdeb4ec Log chat messages on the server 2024-07-10 00:33:09 +01:00
abcdefg30
dde39344a0 Add NotBefore<SpawnStartingUnitsInfo> to LuaScriptInfo
(cherry picked from commit 9f96d0c772)
2023-10-06 15:04:53 +03:00
Pavel Penev
386f691c2e Added a new helper method - a temporary fix
(cherry picked from commit d83e579dfe)
2023-09-27 11:09:34 +03:00
Gustas
24623eaa65 Close the ingame menu upon voting
(cherry picked from commit d5c940ba4c)
2023-09-27 10:47:20 +03:00
Gustas
dc89341634 Add vote kick
(cherry picked from commit 144e716cdf)
2023-09-27 10:47:17 +03:00
JovialFeline
8961b4986f Disable flak truck in Soviet-13, others 2023-09-22 12:26:50 +03:00
Gustas
cbf4207d22 Add backup ExplicitSequenceFilenames to update rules
(cherry picked from commit 29eaab59be)
2023-09-18 11:07:24 +03:00
penev92
c27bf85631 Bumped Eluant NuGet version
The new version fixes the windows 32-bit build not working.
2023-09-16 20:08:05 +02:00
dnqbob
809cb16075 Fix Target.Invalid comparion bug in AutoTarget 2023-09-11 18:57:14 +03:00
Matthias Mailänder
f4c186b7a6 This is not just about difficulty. 2023-08-28 23:34:58 +03:00
Matthias Mailänder
c3cf94b67a The description is optional so don't crash when it is null. 2023-08-28 23:34:58 +03:00
JovialFeline
8b3e7bec2a Add text fix, polish to Controlled Burn 2023-08-28 19:32:56 +02:00
abcdefg30
64ec6eef0a Fix Folder.GetStream using FileNotFoundExceptions to detect if a file exists 2023-08-20 23:01:34 +03:00
dnqbob
4dec1fe430 Autocarryall put down unit if destination is cancelled when picking up 2023-08-19 11:56:35 +03:00
Matthias Mailänder
db3145ed5e Evaluate read only dictionaries. 2023-08-06 17:13:12 +03:00
Gustas
e49135bb09 Fix gen1 map importer crashing on invalid tiles 2023-08-06 13:56:17 +02:00
Gustas
9d79e52989 Fix out of bounds cells not being randomised 2023-08-06 13:56:10 +02:00
Smittytron
0ac9d96ab8 Add Soviet13b 2023-08-06 14:41:15 +03:00
Gustas
5ce559c853 Fix low power notification never triggering 2023-08-05 19:05:52 +02:00
Gustas
a4821b51a2 Grant condition to units closest to the crate 2023-08-05 13:35:55 +02:00
Gustas
cfc026a1ac Fix aircraft jittering 2023-08-05 13:29:41 +02:00
Gustas
58ab3eb153 Fix misaligned TD combat observer tab 2023-08-05 13:23:10 +02:00
Gustas
47b6542b1d Exit game save with escape 2023-08-03 15:56:59 +02:00
Gustas
3c7addcb80 Trigger a button sound when saving a game with enter 2023-08-03 15:56:48 +02:00
Gustas
37f1b9efbf Fix lua sanity check crashing on dedicated servers 2023-08-03 15:34:43 +02:00
abcdefg30
82acdbc32a Fix RA assets installation from the Steam C&C:R version 2023-08-01 22:29:52 +03:00
2257 changed files with 27974 additions and 57970 deletions

File diff suppressed because it is too large Load Diff

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
patreon: orahosting

View File

@@ -14,14 +14,11 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Remove System .NET
run: sudo apt-get remove -y dotnet*
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Install .NET 6.0 - name: Install .NET 6.0
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: '6.0.x' dotnet-version: '6.0.x'
@@ -42,7 +39,7 @@ jobs:
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Check Code - name: Check Code
run: | run: |
@@ -60,10 +57,10 @@ jobs:
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Install .NET 6.0 - name: Install .NET 6.0
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: '6.0.x' dotnet-version: '6.0.x'

View File

@@ -1,9 +1,6 @@
name: Deploy Documentation name: Deploy Documentation
on: on:
push:
branches: [ bleed ]
tags: [ 'release-*', 'playtest-*' ]
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag: tag:
@@ -15,49 +12,18 @@ permissions:
contents: read # to fetch code (actions/checkout) contents: read # to fetch code (actions/checkout)
jobs: jobs:
prepare: wiki:
name: Prepare version strings name: Update Wiki
if: github.repository == 'openra/openra' if: github.repository == 'openra/openra'
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: 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 - name: Clone Repository
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
ref: ${{ needs.prepare.outputs.git_tag }} ref: ${{ github.event.inputs.tag }}
- name: Install .NET 6 - name: Install .NET 6
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: '6.0.x' dotnet-version: '6.0.x'
@@ -66,53 +32,49 @@ jobs:
make all make all
- name: Clone Wiki - name: Clone Wiki
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
repository: openra/openra.wiki repository: openra/openra.wiki
token: ${{ secrets.DOCS_TOKEN }} token: ${{ secrets.DOCS_TOKEN }}
path: wiki path: wiki
- name: Update Wiki (Playtest) - name: Update Wiki (Playtest)
if: startsWith(needs.prepare.outputs.git_tag, 'playtest-') if: startsWith(github.event.inputs.tag, 'playtest-')
env:
GIT_TAG: ${{ github.event.inputs.tag }}
run: | run: |
./utility.sh all --settings-docs "${{ needs.prepare.outputs.git_tag }}" > "wiki/Settings (playtest).md" ./utility.sh all --settings-docs "${GIT_TAG}" > "wiki/Settings (playtest).md"
- name: Update Wiki (Release) - name: Update Wiki (Release)
if: startsWith(needs.prepare.outputs.git_tag, 'release-') if: startsWith(github.event.inputs.tag, 'release-')
env:
GIT_TAG: ${{ github.event.inputs.tag }}
run: | run: |
./utility.sh all --settings-docs "${{ needs.prepare.outputs.git_tag }}" > "wiki/Settings.md" ./utility.sh all --settings-docs "${GIT_TAG}" > "wiki/Settings.md"
- name: Push Wiki - name: Push Wiki
env:
GIT_TAG: ${{ github.event.inputs.tag }}
run: | run: |
cd wiki cd wiki
git config --local user.email "actions@github.com" git config --local user.email "actions@github.com"
git config --local user.name "GitHub Actions" git config --local user.name "GitHub Actions"
git status git add --all
git diff-index --quiet HEAD || \ git commit -m "Update auto-generated documentation for ${GIT_TAG}"
(
git add --all && \
git commit -m "Update auto-generated documentation for ${{ needs.prepare.outputs.git_tag }}" && \
git push origin master git push origin master
)
docs: docs:
name: Update docs.openra.net name: Update docs.openra.net
needs: prepare
if: github.repository == 'openra/openra' if: github.repository == 'openra/openra'
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Debug output
run: |
echo ${{ needs.prepare.outputs.git_tag }}
echo ${{ needs.prepare.outputs.version_type }}
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
ref: ${{ needs.prepare.outputs.git_tag }} ref: ${{ github.event.inputs.tag }}
- name: Install .NET 6 - name: Install .NET 6
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: '6.0.x' dotnet-version: '6.0.x'
@@ -120,31 +82,62 @@ jobs:
run: | run: |
make all make all
# version_type is release/playtest/bleed - the name of the target branch. - name: Clone docs.openra.net (Playtest)
- name: Clone docs.openra.net if: startsWith(github.event.inputs.tag, 'playtest-')
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
repository: openra/docs repository: openra/docs
token: ${{ secrets.DOCS_TOKEN }} token: ${{ secrets.DOCS_TOKEN }}
path: docs path: docs
ref: ${{ needs.prepare.outputs.version_type }} ref: playtest
- name: Generate docs files - 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 }}
run: | run: |
./utility.sh all --docs "${{ needs.prepare.outputs.git_tag }}" | python3 ./packaging/format-docs.py > "docs/api/traits.md" ./utility.sh all --docs "${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 --weapon-docs "${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 --sprite-sequence-docs "${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" ./utility.sh all --lua-docs "${GIT_TAG}" > "docs/api/lua.md"
- name: Update docs.openra.net - 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 }}
run: | run: |
cd docs cd docs
git config --local user.email "actions@github.com" git config --local user.email "actions@github.com"
git config --local user.name "GitHub Actions" git config --local user.name "GitHub Actions"
git status git add api/*.md
git diff-index --quiet HEAD || \ git commit -m "Update auto-generated documentation for ${GIT_TAG}"
(
git add api/*.md && \ - name: Push docs.openra.net (Release)
git commit -m "Update auto-generated documentation for ${{ needs.prepare.outputs.git_tag }}" && \ if: startsWith(github.event.inputs.tag, 'release-')
git push origin ${{ needs.prepare.outputs.version_type }} 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

View File

@@ -15,56 +15,73 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
if: github.repository == 'openra/openra' if: github.repository == 'openra/openra'
steps: steps:
- name: Download Butler - name: Download Packages
env:
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
run: |
wget -cq -O butler-linux-amd64.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
unzip butler-linux-amd64.zip
rm butler-linux-amd64.zip
chmod +x butler
./butler -V
./butler login
- name: Publish Windows Installer
env:
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
run: | run: |
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}-x64.exe" wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}-x64.exe"
./butler push "OpenRA-${{ github.event.inputs.tag }}-x64.exe" "openra/openra:win" --userversion ${{ github.event.inputs.tag }} wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}-x64-winportable.zip" -O "OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip"
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}.dmg"
- name: Publish Windows Itch Bundle wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Dune-2000-x86_64.AppImage"
env: wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Red-Alert-x86_64.AppImage"
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }} wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Tiberian-Dawn-x86_64.AppImage"
run: |
wget -q "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.event.inputs.tag }}/packaging/.itch.toml" wget -q "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.event.inputs.tag }}/packaging/.itch.toml"
zip -u "OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip" .itch.toml zip -u "OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip" .itch.toml
./butler push "OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip" "openra/openra:itch" --userversion ${{ github.event.inputs.tag }}
- name: Publish Windows Installer
uses: josephbmanley/butler-publish-itchio-action@master
env:
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
CHANNEL: win
ITCH_GAME: openra
ITCH_USER: openra
VERSION: ${{ github.event.inputs.tag }}
PACKAGE: OpenRA-${{ github.event.inputs.tag }}-x64.exe
- name: Publish Windows Itch Bundle
uses: josephbmanley/butler-publish-itchio-action@master
env:
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
CHANNEL: itch
ITCH_GAME: openra
ITCH_USER: openra
VERSION: ${{ github.event.inputs.tag }}
PACKAGE: OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip
- name: Publish macOS Package - name: Publish macOS Package
uses: josephbmanley/butler-publish-itchio-action@master
env: env:
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }} BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
run: | CHANNEL: macos
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}.dmg" ITCH_GAME: openra
./butler push "OpenRA-${{ github.event.inputs.tag }}.dmg" "openra/openra:macos" --userversion ${{ github.event.inputs.tag }} ITCH_USER: openra
VERSION: ${{ github.event.inputs.tag }}
PACKAGE: OpenRA-${{ github.event.inputs.tag }}.dmg
- name: Publish RA AppImage - name: Publish RA AppImage
uses: josephbmanley/butler-publish-itchio-action@master
env: env:
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }} BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
run: | CHANNEL: linux-ra
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Red-Alert-x86_64.AppImage" ITCH_GAME: openra
./butler push "OpenRA-Red-Alert-x86_64.AppImage" "openra/openra:linux-ra" --userversion ${{ github.event.inputs.tag }} ITCH_USER: openra
VERSION: ${{ github.event.inputs.tag }}
PACKAGE: OpenRA-Red-Alert-x86_64.AppImage
- name: Publish TD AppImage - name: Publish TD AppImage
uses: josephbmanley/butler-publish-itchio-action@master
env: env:
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }} BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
run: | CHANNEL: linux-cnc
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Tiberian-Dawn-x86_64.AppImage" ITCH_GAME: openra
./butler push "OpenRA-Tiberian-Dawn-x86_64.AppImage" "openra/openra:linux-cnc" --userversion ${{ github.event.inputs.tag }} ITCH_USER: openra
VERSION: ${{ github.event.inputs.tag }}
PACKAGE: OpenRA-Tiberian-Dawn-x86_64.AppImage
- name: Publish D2k AppImage - name: Publish D2k AppImage
uses: josephbmanley/butler-publish-itchio-action@master
env: env:
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }} BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
run: | CHANNEL: linux-d2k
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Dune-2000-x86_64.AppImage" ITCH_GAME: openra
./butler push "OpenRA-Dune-2000-x86_64.AppImage" "openra/openra:linux-d2k" --userversion ${{ github.event.inputs.tag }} ITCH_USER: openra
VERSION: ${{ github.event.inputs.tag }}
PACKAGE: OpenRA-Dune-2000-x86_64.AppImage

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Prepare Environment - name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV} run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -40,10 +40,10 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Install .NET 6.0 - name: Install .NET 6.0
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: '6.0.x' dotnet-version: '6.0.x'
@@ -53,25 +53,27 @@ jobs:
- name: Package AppImages - name: Package AppImages
run: | run: |
mkdir -p build/linux mkdir -p build/linux
sudo apt-get install -y desktop-file-utils sudo apt install libfuse2
./packaging/linux/buildpackage.sh "${GIT_TAG}" "${PWD}/build/linux" ./packaging/linux/buildpackage.sh "${GIT_TAG}" "${PWD}/build/linux"
- name: Upload Packages - name: Upload Packages
env: uses: svenstaro/upload-release-action@v2
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with:
shell: bash repo_token: ${{ secrets.GITHUB_TOKEN }}
run: | tag: ${{ github.ref }}
gh release upload ${{ github.ref_name }} build/linux/* overwrite: true
file_glob: true
file: build/linux/*
macos: macos:
name: macOS Disk Image name: macOS Disk Image
runs-on: macos-13 runs-on: macos-11
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Install .NET 6.0 - name: Install .NET 6.0
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: '6.0.x' dotnet-version: '6.0.x'
@@ -90,21 +92,23 @@ jobs:
./packaging/macos/buildpackage.sh "${GIT_TAG}" "${PWD}/build/macos" ./packaging/macos/buildpackage.sh "${GIT_TAG}" "${PWD}/build/macos"
- name: Upload Package - name: Upload Package
env: uses: svenstaro/upload-release-action@v2
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with:
shell: bash repo_token: ${{ secrets.GITHUB_TOKEN }}
run: | tag: ${{ github.ref }}
gh release upload ${{ github.ref_name }} build/macos/* overwrite: true
file_glob: true
file: build/macos/*
windows: windows:
name: Windows Installers name: Windows Installers
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Install .NET 6.0 - name: Install .NET 6.0
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: '6.0.x' dotnet-version: '6.0.x'
@@ -120,8 +124,10 @@ jobs:
./packaging/windows/buildpackage.sh "${GIT_TAG}" "${PWD}/build/windows" ./packaging/windows/buildpackage.sh "${GIT_TAG}" "${PWD}/build/windows"
- name: Upload Packages - name: Upload Packages
env: uses: svenstaro/upload-release-action@v2
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with:
shell: bash repo_token: ${{ secrets.GITHUB_TOKEN }}
run: | tag: ${{ github.ref }}
gh release upload ${{ github.ref_name }} build/windows/* overwrite: true
file_glob: true
file: build/windows/*

60
AUTHORS
View File

@@ -1,4 +1,5 @@
OpenRA wouldn't be where it is today without the hard work of many contributors. OpenRA wouldn't be where it is today without the
hard work of many contributors.
The OpenRA developers are: The OpenRA developers are:
* Gustas Kažukauskas (PunkPun) * Gustas Kažukauskas (PunkPun)
@@ -97,7 +98,6 @@ Also thanks to:
* Kanar * Kanar
* Kenny Hoxworth (hoxworth) * Kenny Hoxworth (hoxworth)
* Kevin Azzam (ChaoticMind) * Kevin Azzam (ChaoticMind)
* Kevin Streser
* Krishnakanth Mallik * Krishnakanth Mallik
* Kyle Smith (Smitty) * Kyle Smith (Smitty)
* Kyrre Soerensen (zypres) * Kyrre Soerensen (zypres)
@@ -150,7 +150,6 @@ Also thanks to:
* Teemu Nieminen (Temeez) * Teemu Nieminen (Temeez)
* Thomas Christlieb (ThomasChr) * Thomas Christlieb (ThomasChr)
* Tim Mylemans (gecko) * Tim Mylemans (gecko)
* Tinix
* Tirili * Tirili
* Tomas Einarsson (Mesacer) * Tomas Einarsson (Mesacer)
* Tom van Leth (tovl) * Tom van Leth (tovl)
@@ -162,42 +161,61 @@ Also thanks to:
* Wojciech Walaszek (Voidwalker) * Wojciech Walaszek (Voidwalker)
* Wuschel * Wuschel
Using GNU FreeFont distributed under the GNU GPL terms. Using GNU FreeFont distributed under the GNU GPL
terms.
Using Simple DirectMedia Layer distributed under the terms of the zlib license. Using Simple DirectMedia Layer distributed under
the terms of the zlib license.
Using FreeType distributed under the terms of the FreeType License. Using FreeType distributed under the terms of the
FreeType License.
Using OpenAL Soft distributed under the GNU LGPL. Using OpenAL Soft distributed under the GNU LGPL.
Using SDL2-CS and OpenAL-CS created by Ethan Lee and released under the zlib license. Using SDL2-CS and OpenAL-CS created by Ethan
Lee and released under the zlib license.
Using Eluant created by Chris Howie and released under the MIT license. Using Eluant created by Chris Howie and released
under the MIT license.
Using FuzzyLogicLibrary (fuzzynet) by Dmitry Kaluzhny and released under the GNU GPL terms. Using FuzzyLogicLibrary (fuzzynet) by Dmitry
Kaluzhny and released under the GNU GPL terms.
Using Mono.Nat by Alan McGovern, Ben Motmans, Nicholas Terry distributed under the MIT license. Using Mono.Nat by Alan McGovern, Ben Motmans,
Nicholas Terry distributed under the MIT license.
Using MP3Sharp by Robert Bruke and Zane Wagner licensed under the GNU LGPL Version 3. Using MP3Sharp by Robert Bruke and Zane Wagner
licensed under the GNU LGPL Version 3.
Using TagLib# by Stephen Shaw licensed under the GNU LGPL Version 2.1. Using TagLib# by Stephen Shaw licensed under the
GNU LGPL Version 2.1.
Using NVorbis by Andrew Ward distributed under the MIT license. Using NVorbis by Andrew Ward distributed under
the MIT license.
Using ICSharpCode.SharpZipLib initially by Mike Krueger and distributed under the GNU GPL terms. Using ICSharpCode.SharpZipLib initially by Mike
Krueger and distributed under the GNU GPL terms.
Using rix0rrr.BeaconLib developed by Rico Huijbers distributed under MIT License. Using rix0rrr.BeaconLib developed by Rico Huijbers
distributed under MIT License.
Using DiscordRichPresence developed by Lachee distributed under MIT License. Using DiscordRichPresence developed by Lachee
distributed under MIT License.
Using Json.NET developed by James Newton-King distributed under MIT License. Using Json.NET developed by James Newton-King
distributed under MIT License.
Using ANGLE distributed under the BS3 3-Clause license. Using ANGLE distributed under the BS3 3-Clause license.
Using Pfim developed by Nick Babcock distributed under the MIT license. Using Pfim developed by Nick Babcock
distributed under the MIT license.
Using Linguini by the Space Station 14 team licensed under Apache and MIT terms. Using Linguini by the Space Station 14 team
licensed under Apache and MIT terms.
This site or product includes IP2Location LITE data available from https://www.ip2location.com. This site or product includes IP2Location LITE data
available from http://www.ip2location.com.
Finally, special thanks goes to the original teams at Westwood Studios and EA for creating the classic games which OpenRA aims to reimagine. Finally, special thanks goes to the original teams
at Westwood Studios and EA for creating the classic
games which OpenRA aims to reimagine.

View File

@@ -70,7 +70,7 @@ members of the project's leadership.
## Attribution ## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [https://contributor-covenant.org/version/1/4][version] available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: https://contributor-covenant.org [homepage]: http://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/4/ [version]: http://contributor-covenant.org/version/1/4/

View File

@@ -16,7 +16,7 @@ Help us keep OpenRA open and inclusive. Please read and follow our [Code of Cond
* [Coding standard](https://github.com/OpenRA/OpenRA/wiki/Coding-Standard) * [Coding standard](https://github.com/OpenRA/OpenRA/wiki/Coding-Standard)
* [Branches and Releases](https://github.com/OpenRA/OpenRA/wiki/Branches-and-Releases) * [Branches and Releases](https://github.com/OpenRA/OpenRA/wiki/Branches-and-Releases)
* [Licensing](https://www.gnu.org/licenses/quick-guide-gplv3.html) * [Licensing](http://www.gnu.org/licenses/quick-guide-gplv3.html)
Please `git rebase` to the latest revision of the bleed branch. Please `git rebase` to the latest revision of the bleed branch.

View File

@@ -1,7 +1,7 @@
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed. of this license document, but changing it is not allowed.
@@ -645,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found.
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail. Also add information on how to contact you by electronic and paper mail.
@@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school, You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>. <http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read Public License instead of this License. But first, please read
<https://www.gnu.org/philosophy/why-not-lgpl.html>. <http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@@ -51,11 +51,8 @@
</ItemGroup> </ItemGroup>
</Target> </Target>
<!-- StyleCop/Roslynator --> <!-- StyleCop -->
<ItemGroup> <ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
<!-- Roslynator analyzers fail to run under Mono (AD0001) -->
<PackageReference Include="Roslynator.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -7,7 +7,7 @@ Windows
======= =======
Compiling OpenRA requires the following dependencies: Compiling OpenRA requires the following dependencies:
* [Windows PowerShell >= 4.0](https://microsoft.com/powershell) (included by default in recent Windows 10 versions) * [Windows PowerShell >= 4.0](http://microsoft.com/powershell) (included by default in recent Windows 10 versions)
* [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0) (or via Visual Studio) * [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0) (or via Visual Studio)
To compile OpenRA, open the `OpenRA.sln` solution in the main folder, build it from the command-line with `dotnet` or use the Makefile analogue command `make all` scripted in PowerShell syntax. To compile OpenRA, open the `OpenRA.sln` solution in the main folder, build it from the command-line with `dotnet` or use the Makefile analogue command `make all` scripted in PowerShell syntax.
@@ -25,7 +25,7 @@ To compile OpenRA, run `make` from the command line (or `make RUNTIME=mono` if u
The default behaviour on the x86_64 architecture is to download several pre-compiled native libraries using the Nuget packaging manager. If you prefer to use system libraries, compile instead using `make TARGETPLATFORM=unix-generic`. The default behaviour on the x86_64 architecture is to download several pre-compiled native libraries using the Nuget packaging manager. If you prefer to use system libraries, compile instead using `make TARGETPLATFORM=unix-generic`.
If you choose to use system libraries, or your system is not x86_64, you will need to install [SDL 2](https://www.libsdl.org/download-2.0.php), [FreeType](https://gnuwin32.sourceforge.net/packages/freetype.htm), [OpenAL](https://openal-soft.org/), and [liblua 5.1](https://luabinaries.sourceforge.net/download.html) before compiling OpenRA. If you choose to use system libraries, or your system is not x86_64, you will need to install [SDL 2](https://www.libsdl.org/download-2.0.php), [FreeType](http://gnuwin32.sourceforge.net/packages/freetype.htm), [OpenAL](https://openal-soft.org/), and [liblua 5.1](http://luabinaries.sourceforge.net/download.html) before compiling OpenRA.
These can be installed using your package manager on various distros: These can be installed using your package manager on various distros:

View File

@@ -153,12 +153,12 @@ tests:
############# LOCAL INSTALLATION AND DOWNSTREAM PACKAGING ############## ############# LOCAL INSTALLATION AND DOWNSTREAM PACKAGING ##############
# #
version: VERSION mods/*/mod.yaml version: VERSION mods/ra/mod.yaml mods/cnc/mod.yaml mods/d2k/mod.yaml mods/ts/mod.yaml mods/modcontent/mod.yaml mods/all/mod.yaml
ifeq ($(VERSION),) ifeq ($(VERSION),)
$(error Unable to determine new version (requires git or override of variable VERSION)) $(error Unable to determine new version (requires git or override of variable VERSION))
endif endif
@sh -c '. ./packaging/functions.sh; set_engine_version "$(VERSION)" .' @sh -c '. ./packaging/functions.sh; set_engine_version "$(VERSION)" .'
@sh -c '. ./packaging/functions.sh; set_mod_version "$(VERSION)" mods/*/mod.yaml' @sh -c '. ./packaging/functions.sh; set_mod_version "$(VERSION)" mods/ra/mod.yaml mods/cnc/mod.yaml mods/d2k/mod.yaml mods/ts/mod.yaml mods/modcontent/mod.yaml mods/all/mod.yaml'
install: install:
@sh -c '. ./packaging/functions.sh; install_assemblies $(CWD) $(DESTDIR)$(gameinstalldir) $(TARGETPLATFORM) $(RUNTIME) True True True' @sh -c '. ./packaging/functions.sh; install_assemblies $(CWD) $(DESTDIR)$(gameinstalldir) $(TARGETPLATFORM) $(RUNTIME) True True True'

View File

@@ -146,22 +146,18 @@ namespace OpenRA.Activities
} }
/// <summary> /// <summary>
/// <para>
/// Called every tick to run activity logic. Returns false if the activity should /// 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 /// remain active, or true if it is complete. Cancelled activities must ensure they
/// return the actor to a consistent state before returning true. /// return the actor to a consistent state before returning true.
/// </para> ///
/// <para>
/// Child activities can be queued using QueueChild, and these will be ticked /// 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 /// 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 /// in parallel with child activities should set ChildHasPriority to false and
/// manually call TickChildren. /// manually call TickChildren.
/// </para> ///
/// <para>
/// Queuing one or more child activities and returning true is valid, and causes /// Queuing one or more child activities and returning true is valid, and causes
/// the activity to be completed immediately (without ticking again) once the /// the activity to be completed immediately (without ticking again) once the
/// children have completed. /// children have completed.
/// </para>
/// </summary> /// </summary>
public virtual bool Tick(Actor self) public virtual bool Tick(Actor self)
{ {
@@ -226,11 +222,10 @@ namespace OpenRA.Activities
} }
/// <summary> /// <summary>
/// <para>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> ///
/// Call this method from any place that's called during a tick, such as the Tick() method itself or /// 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. /// the Before(First|Last)Run() methods. The origin activity will be marked in the output.
/// </para>
/// </summary> /// </summary>
/// <param name="self">The actor performing this activity.</param> /// <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> /// <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>

View File

@@ -71,12 +71,7 @@ namespace OpenRA
public IEffectiveOwner EffectiveOwner { get; } public IEffectiveOwner EffectiveOwner { get; }
public IOccupySpace OccupiesSpace { get; } public IOccupySpace OccupiesSpace { get; }
public ITargetable[] Targetables { get; } public ITargetable[] Targetables { get; }
public IEnumerable<ITargetablePositions> EnabledTargetablePositions { get; } public IEnumerable<ITargetablePositions> EnabledTargetablePositions { get; private set; }
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 IsIdle => CurrentActivity == null;
public bool IsDead => Disposed || (health != null && health.IsDead); public bool IsDead => Disposed || (health != null && health.IsDead);
@@ -160,7 +155,6 @@ namespace OpenRA
var targetablesList = new List<ITargetable>(); var targetablesList = new List<ITargetable>();
var targetablePositionsList = new List<ITargetablePositions>(); var targetablePositionsList = new List<ITargetablePositions>();
var syncHashesList = new List<SyncHash>(); var syncHashesList = new List<SyncHash>();
var crushablesList = new List<ICrushable>();
foreach (var traitInfo in Info.TraitsInConstructOrder()) foreach (var traitInfo in Info.TraitsInConstructOrder())
{ {
@@ -187,7 +181,6 @@ namespace OpenRA
{ if (trait is ITargetable t) targetablesList.Add(t); } { if (trait is ITargetable t) targetablesList.Add(t); }
{ if (trait is ITargetablePositions t) targetablePositionsList.Add(t); } { if (trait is ITargetablePositions t) targetablePositionsList.Add(t); }
{ if (trait is ISync t) syncHashesList.Add(new SyncHash(t)); } { if (trait is ISync t) syncHashesList.Add(new SyncHash(t)); }
{ if (trait is ICrushable t) crushablesList.Add(t); }
} }
resolveOrders = resolveOrdersList.ToArray(); resolveOrders = resolveOrdersList.ToArray();
@@ -202,7 +195,6 @@ namespace OpenRA
EnabledTargetablePositions = targetablePositions.Where(Exts.IsTraitEnabled); EnabledTargetablePositions = targetablePositions.Where(Exts.IsTraitEnabled);
enabledTargetableWorldPositions = EnabledTargetablePositions.SelectMany(tp => tp.TargetablePositions(this)); enabledTargetableWorldPositions = EnabledTargetablePositions.SelectMany(tp => tp.TargetablePositions(this));
SyncHashes = syncHashesList.ToArray(); SyncHashes = syncHashesList.ToArray();
crushables = crushablesList.ToArray();
} }
} }

View File

@@ -16,8 +16,7 @@ using OpenRA.Scripting;
namespace OpenRA namespace OpenRA
{ {
public readonly struct CPos : IEquatable<CPos>, IScriptBindable, public readonly struct CPos : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding, IEquatable<CPos>
ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding, ILuaToStringBinding
{ {
// Coordinates are packed in a 32 bit signed int // Coordinates are packed in a 32 bit signed int
// X and Y are 12 bits (signed): -2048...2047 // X and Y are 12 bits (signed): -2048...2047
@@ -97,8 +96,7 @@ namespace OpenRA
public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right) public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right)
{ {
if (!left.TryGetClrValue(out CPos a) || !right.TryGetClrValue(out CVec b)) if (!left.TryGetClrValue(out CPos a) || !right.TryGetClrValue(out CVec b))
throw new LuaException("Attempted to call CPos.Add(CPos, CVec) with invalid arguments " + throw new LuaException($"Attempted to call CPos.Add(CPos, CVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
return new LuaCustomClrObject(a + b); return new LuaCustomClrObject(a + b);
} }
@@ -107,8 +105,7 @@ namespace OpenRA
{ {
var rightType = right.WrappedClrType(); var rightType = right.WrappedClrType();
if (!left.TryGetClrValue(out CPos a)) if (!left.TryGetClrValue(out CPos a))
throw new LuaException("Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments " + throw new LuaException($"Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments ({left.WrappedClrType().Name}, {rightType.Name})");
$"({left.WrappedClrType().Name}, {rightType.Name})");
if (rightType == typeof(CPos)) if (rightType == typeof(CPos))
{ {
@@ -121,8 +118,7 @@ namespace OpenRA
return new LuaCustomClrObject(a - b); return new LuaCustomClrObject(a - b);
} }
throw new LuaException("Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments " + throw new LuaException($"Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments ({left.WrappedClrType().Name}, {rightType.Name})");
$"({left.WrappedClrType().Name}, {rightType.Name})");
} }
public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right) public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right)
@@ -149,8 +145,6 @@ namespace OpenRA
set => throw new LuaException("CPos is read-only. Use CPos.New to create a new value"); set => throw new LuaException("CPos is read-only. Use CPos.New to create a new value");
} }
public LuaValue ToString(LuaRuntime runtime) => ToString();
#endregion #endregion
} }
} }

View File

@@ -17,9 +17,7 @@ using OpenRA.Scripting;
namespace OpenRA namespace OpenRA
{ {
public readonly struct CVec : IEquatable<CVec>, IScriptBindable, public readonly struct CVec : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaUnaryMinusBinding, ILuaEqualityBinding, ILuaTableBinding, IEquatable<CVec>
ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaUnaryMinusBinding,
ILuaMultiplicationBinding, ILuaDivisionBinding, ILuaTableBinding, ILuaToStringBinding
{ {
public readonly int X, Y; public readonly int X, Y;
@@ -63,14 +61,14 @@ namespace OpenRA
public static readonly CVec[] Directions = public static readonly CVec[] Directions =
{ {
new(-1, -1), new CVec(-1, -1),
new(-1, 0), new CVec(-1, 0),
new(-1, 1), new CVec(-1, 1),
new(0, -1), new CVec(0, -1),
new(0, 1), new CVec(0, 1),
new(1, -1), new CVec(1, -1),
new(1, 0), new CVec(1, 0),
new(1, 1), new CVec(1, 1),
}; };
#region Scripting interface #region Scripting interface
@@ -78,8 +76,7 @@ namespace OpenRA
public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right) public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right)
{ {
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b)) if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b))
throw new LuaException("Attempted to call CVec.Add(CVec, CVec) with invalid arguments " + throw new LuaException($"Attempted to call CVec.Add(CVec, CVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
return new LuaCustomClrObject(a + b); return new LuaCustomClrObject(a + b);
} }
@@ -87,12 +84,16 @@ namespace OpenRA
public LuaValue Subtract(LuaRuntime runtime, LuaValue left, LuaValue right) public LuaValue Subtract(LuaRuntime runtime, LuaValue left, LuaValue right)
{ {
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b)) if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b))
throw new LuaException("Attempted to call CVec.Subtract(CVec, CVec) with invalid arguments " + throw new LuaException($"Attempted to call CVec.Subtract(CVec, CVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
return new LuaCustomClrObject(a - b); return new LuaCustomClrObject(a - b);
} }
public LuaValue Minus(LuaRuntime runtime)
{
return new LuaCustomClrObject(-this);
}
public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right) public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right)
{ {
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b)) if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b))
@@ -101,29 +102,6 @@ namespace OpenRA
return a == b; return a == b;
} }
public LuaValue Minus(LuaRuntime runtime)
{
return new LuaCustomClrObject(-this);
}
public LuaValue Multiply(LuaRuntime runtime, LuaValue left, LuaValue right)
{
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out int b))
throw new LuaException("Attempted to call CVec.Multiply(CVec, integer) with invalid arguments " +
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
return new LuaCustomClrObject(a * b);
}
public LuaValue Divide(LuaRuntime runtime, LuaValue left, LuaValue right)
{
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out int b))
throw new LuaException("Attempted to call CVec.Multiply(CVec, integer) with invalid arguments " +
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
return new LuaCustomClrObject(a / b);
}
public LuaValue this[LuaRuntime runtime, LuaValue key] public LuaValue this[LuaRuntime runtime, LuaValue key]
{ {
get get
@@ -132,7 +110,6 @@ namespace OpenRA
{ {
case "X": return X; case "X": return X;
case "Y": return Y; case "Y": return Y;
case "Length": return Length;
default: throw new LuaException($"CVec does not define a member '{key}'"); default: throw new LuaException($"CVec does not define a member '{key}'");
} }
} }
@@ -140,8 +117,6 @@ namespace OpenRA
set => throw new LuaException("CVec is read-only. Use CVec.New to create a new value"); set => throw new LuaException("CVec is read-only. Use CVec.New to create a new value");
} }
public LuaValue ToString(LuaRuntime runtime) => ToString();
#endregion #endregion
} }
} }

View File

@@ -22,9 +22,6 @@ namespace OpenRA
// Fixed byte pattern for the OID header // Fixed byte pattern for the OID header
static readonly byte[] OIDHeader = { 0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0 }; static readonly byte[] OIDHeader = { 0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0 };
static readonly char[] HexUpperAlphabet = "0123456789ABCDEF".ToArray();
static readonly char[] HexLowerAlphabet = "0123456789abcdef".ToArray();
public static string PublicKeyFingerprint(RSAParameters parameters) public static string PublicKeyFingerprint(RSAParameters parameters)
{ {
// Public key fingerprint is defined as the SHA1 of the modulus + exponent bytes // Public key fingerprint is defined as the SHA1 of the modulus + exponent bytes
@@ -56,33 +53,33 @@ namespace OpenRA
using (var s = new MemoryStream(data)) using (var s = new MemoryStream(data))
{ {
// SEQUENCE // SEQUENCE
s.ReadUInt8(); s.ReadByte();
ReadTLVLength(s); ReadTLVLength(s);
// SEQUENCE -> fixed header junk // SEQUENCE -> fixed header junk
s.ReadUInt8(); s.ReadByte();
var headerLength = ReadTLVLength(s); var headerLength = ReadTLVLength(s);
s.Position += headerLength; s.Position += headerLength;
// SEQUENCE -> BIT_STRING // SEQUENCE -> BIT_STRING
s.ReadUInt8(); s.ReadByte();
ReadTLVLength(s); ReadTLVLength(s);
s.ReadUInt8(); s.ReadByte();
// SEQUENCE -> BIT_STRING -> SEQUENCE // SEQUENCE -> BIT_STRING -> SEQUENCE
s.ReadUInt8(); s.ReadByte();
ReadTLVLength(s); ReadTLVLength(s);
// SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (modulus) // SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (modulus)
s.ReadUInt8(); s.ReadByte();
var modulusLength = ReadTLVLength(s); var modulusLength = ReadTLVLength(s);
s.ReadUInt8(); s.ReadByte();
var modulus = s.ReadBytes(modulusLength - 1); var modulus = s.ReadBytes(modulusLength - 1);
// SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (exponent) // SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (exponent)
s.ReadUInt8(); s.ReadByte();
var exponentLength = ReadTLVLength(s); var exponentLength = ReadTLVLength(s);
s.ReadUInt8(); s.ReadByte();
var exponent = s.ReadBytes(exponentLength - 1); var exponent = s.ReadBytes(exponentLength - 1);
return new RSAParameters return new RSAParameters
@@ -161,13 +158,13 @@ namespace OpenRA
static int ReadTLVLength(Stream s) static int ReadTLVLength(Stream s)
{ {
var length = s.ReadUInt8(); var length = s.ReadByte();
if (length < 0x80) if (length < 0x80)
return length; return length;
Span<byte> data = stackalloc byte[4]; var data = new byte[4];
s.ReadBytes(data[..Math.Min(length & 0x7F, 4)]); s.ReadBytes(data, 0, Math.Min(length & 0x7F, 4));
return BitConverter.ToInt32(data); return BitConverter.ToInt32(data.ToArray(), 0);
} }
static int TripletFullLength(int dataLength) static int TripletFullLength(int dataLength)
@@ -252,44 +249,19 @@ namespace OpenRA
public static string SHA1Hash(Stream data) public static string SHA1Hash(Stream data)
{ {
using var csp = SHA1.Create(); using (var csp = SHA1.Create())
return ToHex(csp.ComputeHash(data), true); return new string(csp.ComputeHash(data).SelectMany(a => a.ToString("x2")).ToArray());
} }
public static string SHA1Hash(byte[] data) public static string SHA1Hash(byte[] data)
{ {
using var csp = SHA1.Create(); using (var csp = SHA1.Create())
return ToHex(csp.ComputeHash(data), true); return new string(csp.ComputeHash(data).SelectMany(a => a.ToString("x2")).ToArray());
} }
public static string SHA1Hash(string data) public static string SHA1Hash(string data)
{ {
return SHA1Hash(Encoding.UTF8.GetBytes(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);
}
} }
} }

View File

@@ -27,6 +27,7 @@ namespace OpenRA
{ {
public readonly string Id; public readonly string Id;
public readonly string Version; public readonly string Version;
public readonly string Title;
public readonly string LaunchPath; public readonly string LaunchPath;
public readonly string[] LaunchArgs; public readonly string[] LaunchArgs;
public Sprite Icon { get; internal set; } public Sprite Icon { get; internal set; }
@@ -65,7 +66,6 @@ namespace OpenRA
// Several types of support directory types are available, depending on // Several types of support directory types are available, depending on
// how the player has installed and launched the game. // how the player has installed and launched the game.
// Read registration metadata from all of them // 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)) foreach (var source in GetSupportDirs(ModRegistration.User | ModRegistration.System))
{ {
var metadataPath = Path.Combine(source, "ModMetadata"); var metadataPath = Path.Combine(source, "ModMetadata");
@@ -76,7 +76,7 @@ namespace OpenRA
{ {
try try
{ {
var yaml = MiniYaml.FromFile(path, stringPool: stringPool).First().Value; var yaml = MiniYaml.FromStream(File.OpenRead(path), path).First().Value;
LoadMod(yaml, path); LoadMod(yaml, path);
} }
catch (Exception e) catch (Exception e)
@@ -94,17 +94,17 @@ namespace OpenRA
if (sheetBuilder != null) if (sheetBuilder != null)
{ {
var iconNode = yaml.NodeWithKeyOrDefault("Icon"); var iconNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon");
if (iconNode != null && !string.IsNullOrEmpty(iconNode.Value.Value)) if (iconNode != null && !string.IsNullOrEmpty(iconNode.Value.Value))
using (var stream = new MemoryStream(Convert.FromBase64String(iconNode.Value.Value))) using (var stream = new MemoryStream(Convert.FromBase64String(iconNode.Value.Value)))
mod.Icon = sheetBuilder.Add(new Png(stream)); mod.Icon = sheetBuilder.Add(new Png(stream));
var icon2xNode = yaml.NodeWithKeyOrDefault("Icon2x"); var icon2xNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon2x");
if (icon2xNode != null && !string.IsNullOrEmpty(icon2xNode.Value.Value)) if (icon2xNode != null && !string.IsNullOrEmpty(icon2xNode.Value.Value))
using (var stream = new MemoryStream(Convert.FromBase64String(icon2xNode.Value.Value))) using (var stream = new MemoryStream(Convert.FromBase64String(icon2xNode.Value.Value)))
mod.Icon2x = sheetBuilder.Add(new Png(stream), 1f / 2); mod.Icon2x = sheetBuilder.Add(new Png(stream), 1f / 2);
var icon3xNode = yaml.NodeWithKeyOrDefault("Icon3x"); var icon3xNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon3x");
if (icon3xNode != null && !string.IsNullOrEmpty(icon3xNode.Value.Value)) if (icon3xNode != null && !string.IsNullOrEmpty(icon3xNode.Value.Value))
using (var stream = new MemoryStream(Convert.FromBase64String(icon3xNode.Value.Value))) using (var stream = new MemoryStream(Convert.FromBase64String(icon3xNode.Value.Value)))
mod.Icon3x = sheetBuilder.Add(new Png(stream), 1f / 3); mod.Icon3x = sheetBuilder.Add(new Png(stream), 1f / 3);
@@ -122,29 +122,26 @@ namespace OpenRA
return; return;
var key = ExternalMod.MakeKey(mod); var key = ExternalMod.MakeKey(mod);
var yaml = new MiniYamlNode("Registration", new MiniYaml("", new[] var yaml = new MiniYamlNode("Registration", new MiniYaml("", new List<MiniYamlNode>()
{ {
new MiniYamlNode("Id", mod.Id), new MiniYamlNode("Id", mod.Id),
new MiniYamlNode("Version", mod.Metadata.Version), new MiniYamlNode("Version", mod.Metadata.Version),
new MiniYamlNode("Title", mod.Metadata.Title),
new MiniYamlNode("LaunchPath", launchPath), new MiniYamlNode("LaunchPath", launchPath),
new MiniYamlNode("LaunchArgs", new[] { "Game.Mod=" + mod.Id }.Concat(launchArgs).JoinWith(", ")) new MiniYamlNode("LaunchArgs", new[] { "Game.Mod=" + mod.Id }.Concat(launchArgs).JoinWith(", "))
})); }));
var iconNodes = new List<MiniYamlNode>();
using (var stream = mod.Package.GetStream("icon.png")) using (var stream = mod.Package.GetStream("icon.png"))
if (stream != null) if (stream != null)
iconNodes.Add(new MiniYamlNode("Icon", Convert.ToBase64String(stream.ReadAllBytes()))); yaml.Value.Nodes.Add(new MiniYamlNode("Icon", Convert.ToBase64String(stream.ReadAllBytes())));
using (var stream = mod.Package.GetStream("icon-2x.png")) using (var stream = mod.Package.GetStream("icon-2x.png"))
if (stream != null) if (stream != null)
iconNodes.Add(new MiniYamlNode("Icon2x", Convert.ToBase64String(stream.ReadAllBytes()))); yaml.Value.Nodes.Add(new MiniYamlNode("Icon2x", Convert.ToBase64String(stream.ReadAllBytes())));
using (var stream = mod.Package.GetStream("icon-3x.png")) using (var stream = mod.Package.GetStream("icon-3x.png"))
if (stream != null) if (stream != null)
iconNodes.Add(new MiniYamlNode("Icon3x", Convert.ToBase64String(stream.ReadAllBytes()))); yaml.Value.Nodes.Add(new MiniYamlNode("Icon3x", Convert.ToBase64String(stream.ReadAllBytes())));
yaml = yaml.WithValue(yaml.Value.WithNodesAppended(iconNodes));
var sources = new HashSet<string>(); var sources = new HashSet<string>();
if (registration.HasFlag(ModRegistration.System)) if (registration.HasFlag(ModRegistration.System))
@@ -204,7 +201,7 @@ namespace OpenRA
string modKey = null; string modKey = null;
try try
{ {
var yaml = MiniYaml.FromFile(path).First().Value; var yaml = MiniYaml.FromStream(File.OpenRead(path), path).First().Value;
var m = FieldLoader.Load<ExternalMod>(yaml); var m = FieldLoader.Load<ExternalMod>(yaml);
modKey = ExternalMod.MakeKey(m); modKey = ExternalMod.MakeKey(m);

View File

@@ -14,7 +14,6 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Support; using OpenRA.Support;
using OpenRA.Traits; using OpenRA.Traits;
@@ -23,24 +22,15 @@ namespace OpenRA
{ {
public static class Exts public static class Exts
{ {
/// <summary>Returns <see cref="Color"/> of the <paramref name="actor"/>, taking <see cref="Actor.EffectiveOwner"/> into account.</summary> public static bool IsUppercase(this string str)
public static Color OwnerColor(this Actor actor)
{ {
var effectiveOwner = actor.EffectiveOwner; return string.Compare(str.ToUpperInvariant(), str, false) == 0;
if (effectiveOwner != null && effectiveOwner.Disguised && actor.World.RenderPlayer != null)
return effectiveOwner.Owner.Color;
return actor.Owner.Color;
} }
public static string FormatInvariant(this string format, params object[] args) public static T WithDefault<T>(T def, Func<T> f)
{ {
return string.Format(CultureInfo.InvariantCulture, format, args); try { return f(); }
} catch { return def; }
public static string FormatCurrent(this string format, params object[] args)
{
return string.Format(CultureInfo.CurrentCulture, format, args);
} }
public static Lazy<T> Lazy<T>(Func<T> p) { return new Lazy<T>(p); } public static Lazy<T> Lazy<T>(Func<T> p) { return new Lazy<T>(p); }
@@ -141,35 +131,11 @@ namespace OpenRA
return ret; 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) public static int IndexOf<T>(this T[] array, T value)
{ {
return Array.IndexOf(array, 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) public static T Random<T>(this IEnumerable<T> ts, MersenneTwister r)
{ {
return Random(ts, r, true); return Random(ts, r, true);
@@ -182,7 +148,7 @@ namespace OpenRA
static T Random<T>(IEnumerable<T> ts, MersenneTwister r, bool throws) static T Random<T>(IEnumerable<T> ts, MersenneTwister r, bool throws)
{ {
var xs = ts as IReadOnlyCollection<T>; var xs = ts as ICollection<T>;
xs ??= ts.ToList(); xs ??= ts.ToList();
if (xs.Count == 0) if (xs.Count == 0)
{ {
@@ -337,9 +303,9 @@ namespace OpenRA
// Adjust for other rounding modes // Adjust for other rounding modes
if (round == ISqrtRoundMode.Nearest && remainder > root) if (round == ISqrtRoundMode.Nearest && remainder > root)
root++; root += 1;
else if (round == ISqrtRoundMode.Ceiling && root * root < number) else if (round == ISqrtRoundMode.Ceiling && root * root < number)
root++; root += 1;
return root; return root;
} }
@@ -378,9 +344,9 @@ namespace OpenRA
// Adjust for other rounding modes // Adjust for other rounding modes
if (round == ISqrtRoundMode.Nearest && remainder > root) if (round == ISqrtRoundMode.Nearest && remainder > root)
root++; root += 1;
else if (round == ISqrtRoundMode.Ceiling && root * root < number) else if (round == ISqrtRoundMode.Ceiling && root * root < number)
root++; root += 1;
return root; return root;
} }
@@ -390,11 +356,6 @@ namespace OpenRA
return number * 46341 / 32768; return number * 46341 / 32768;
} }
public static int MultiplyBySqrtTwoOverTwo(int number)
{
return (int)(number * 23170L / 32768L);
}
public static int IntegerDivisionRoundingAwayFromZero(int dividend, int divisor) public static int IntegerDivisionRoundingAwayFromZero(int dividend, int divisor)
{ {
var quotient = Math.DivRem(dividend, divisor, out var remainder); var quotient = Math.DivRem(dividend, divisor, out var remainder);
@@ -433,26 +394,15 @@ namespace OpenRA
public static Dictionary<TKey, TElement> ToDictionaryWithConflictLog<TSource, TKey, TElement>( public static Dictionary<TKey, TElement> ToDictionaryWithConflictLog<TSource, TKey, TElement>(
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector,
string debugName, Func<TKey, string> logKey = null, Func<TElement, string> logValue = null) string debugName, Func<TKey, string> logKey = null, Func<TElement, string> logValue = null)
{
var output = new Dictionary<TKey, TElement>();
IntoDictionaryWithConflictLog(source, keySelector, elementSelector, debugName, output, logKey, logValue);
return output;
}
public static void IntoDictionaryWithConflictLog<TSource, TKey, TElement>(
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector,
string debugName, Dictionary<TKey, TElement> output,
Func<TKey, string> logKey = null, Func<TElement, string> logValue = null)
{ {
// Fall back on ToString() if null functions are provided: // Fall back on ToString() if null functions are provided:
logKey ??= s => s.ToString(); logKey ??= s => s.ToString();
logValue ??= s => s.ToString(); logValue ??= s => s.ToString();
// Try to build a dictionary and log all duplicates found (if any): // Try to build a dictionary and log all duplicates found (if any):
Dictionary<TKey, List<string>> dupKeys = null; var dupKeys = new Dictionary<TKey, List<string>>();
var capacity = source is ICollection<TSource> collection ? collection.Count : 0; var capacity = source is ICollection<TSource> collection ? collection.Count : 0;
output.Clear(); var d = new Dictionary<TKey, TElement>(capacity);
output.EnsureCapacity(capacity);
foreach (var item in source) foreach (var item in source)
{ {
var key = keySelector(item); var key = keySelector(item);
@@ -463,15 +413,14 @@ namespace OpenRA
continue; continue;
// Check for a key conflict: // Check for a key conflict:
if (!output.TryAdd(key, element)) if (!d.TryAdd(key, element))
{ {
dupKeys ??= new Dictionary<TKey, List<string>>();
if (!dupKeys.TryGetValue(key, out var dupKeyMessages)) if (!dupKeys.TryGetValue(key, out var dupKeyMessages))
{ {
// Log the initial conflicting value already inserted: // Log the initial conflicting value already inserted:
dupKeyMessages = new List<string> dupKeyMessages = new List<string>
{ {
logValue(output[key]) logValue(d[key])
}; };
dupKeys.Add(key, dupKeyMessages); dupKeys.Add(key, dupKeyMessages);
} }
@@ -482,14 +431,15 @@ namespace OpenRA
} }
// If any duplicates were found, throw a descriptive error // If any duplicates were found, throw a descriptive error
if (dupKeys != null) if (dupKeys.Count > 0)
{ {
var badKeysFormatted = new StringBuilder( var badKeysFormatted = string.Join(", ", dupKeys.Select(p => $"{logKey(p.Key)}: [{string.Join(",", p.Value)}]"));
$"{debugName}, duplicate values found for the following keys: "); var msg = $"{debugName}, duplicate values found for the following keys: {badKeysFormatted}";
foreach (var p in dupKeys) throw new ArgumentException(msg);
badKeysFormatted.Append($"{logKey(p.Key)}: [{string.Join(",", p.Value)}]");
throw new ArgumentException(badKeysFormatted.ToString());
} }
// Return the dictionary we built:
return d;
} }
public static Color ColorLerp(float t, Color c1, Color c2) public static Color ColorLerp(float t, Color c1, Color c2)
@@ -543,22 +493,17 @@ namespace OpenRA
return result; return result;
} }
public static byte ParseByteInvariant(string s) public static int ParseIntegerInvariant(string s)
{
return byte.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static short ParseInt16Invariant(string s)
{
return short.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static int ParseInt32Invariant(string s)
{ {
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo); return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
} }
public static bool TryParseInt32Invariant(string s, out int i) public static byte ParseByte(string s)
{
return byte.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static bool TryParseIntegerInvariant(string s, out int i)
{ {
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i); return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
} }
@@ -568,26 +513,6 @@ namespace OpenRA
return long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i); 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) public static bool IsTraitEnabled<T>(this T trait)
{ {
return trait is not IDisabledTrait disabledTrait || !disabledTrait.IsTraitDisabled; return trait is not IDisabledTrait disabledTrait || !disabledTrait.IsTraitDisabled;
@@ -637,6 +562,11 @@ namespace OpenRA
{ {
return new LineSplitEnumerator(str.AsSpan(), separator); 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 public ref struct LineSplitEnumerator
@@ -651,7 +581,7 @@ namespace OpenRA
Current = default; Current = default;
} }
public readonly LineSplitEnumerator GetEnumerator() => this; public LineSplitEnumerator GetEnumerator() => this;
public bool MoveNext() public bool MoveNext()
{ {

View File

@@ -87,7 +87,6 @@ namespace OpenRA
{ typeof(WAngle), ParseWAngle }, { typeof(WAngle), ParseWAngle },
{ typeof(WRot), ParseWRot }, { typeof(WRot), ParseWRot },
{ typeof(CPos), ParseCPos }, { typeof(CPos), ParseCPos },
{ typeof(CPos[]), ParseCPosArray },
{ typeof(CVec), ParseCVec }, { typeof(CVec), ParseCVec },
{ typeof(CVec[]), ParseCVecArray }, { typeof(CVec[]), ParseCVecArray },
{ typeof(BooleanExpression), ParseBooleanExpression }, { typeof(BooleanExpression), ParseBooleanExpression },
@@ -119,7 +118,7 @@ namespace OpenRA
static object ParseInt(string fieldName, Type fieldType, string value, MemberInfo field) static object ParseInt(string fieldName, Type fieldType, string value, MemberInfo field)
{ {
if (Exts.TryParseInt32Invariant(value, out var res)) if (Exts.TryParseIntegerInvariant(value, out var res))
{ {
if (res >= 0 && res < BoxedInts.Length) if (res >= 0 && res < BoxedInts.Length)
return BoxedInts[res]; return BoxedInts[res];
@@ -196,12 +195,12 @@ namespace OpenRA
if (value != null) if (value != null)
{ {
var parts = value.Split(SplitComma); var parts = value.Split(SplitComma);
if (parts.Length == 3 if (parts.Length == 3)
&& WDist.TryParse(parts[0], out var rx) {
&& WDist.TryParse(parts[1], out var ry) if (WDist.TryParse(parts[0], out var rx) && WDist.TryParse(parts[1], out var ry) && WDist.TryParse(parts[2], out var rz))
&& WDist.TryParse(parts[2], out var rz))
return new WVec(rx, ry, rz); return new WVec(rx, ry, rz);
} }
}
return InvalidValueAction(value, fieldType, fieldName); return InvalidValueAction(value, fieldType, fieldName);
} }
@@ -236,19 +235,21 @@ namespace OpenRA
if (value != null) if (value != null)
{ {
var parts = value.Split(SplitComma); var parts = value.Split(SplitComma);
if (parts.Length == 3 if (parts.Length == 3)
&& WDist.TryParse(parts[0], out var rx) {
if (WDist.TryParse(parts[0], out var rx)
&& WDist.TryParse(parts[1], out var ry) && WDist.TryParse(parts[1], out var ry)
&& WDist.TryParse(parts[2], out var rz)) && WDist.TryParse(parts[2], out var rz))
return new WPos(rx, ry, rz); return new WPos(rx, ry, rz);
} }
}
return InvalidValueAction(value, fieldType, fieldName); return InvalidValueAction(value, fieldType, fieldName);
} }
static object ParseWAngle(string fieldName, Type fieldType, string value, MemberInfo field) static object ParseWAngle(string fieldName, Type fieldType, string value, MemberInfo field)
{ {
if (Exts.TryParseInt32Invariant(value, out var res)) if (Exts.TryParseIntegerInvariant(value, out var res))
return new WAngle(res); return new WAngle(res);
return InvalidValueAction(value, fieldType, fieldName); return InvalidValueAction(value, fieldType, fieldName);
} }
@@ -258,12 +259,14 @@ namespace OpenRA
if (value != null) if (value != null)
{ {
var parts = value.Split(SplitComma); var parts = value.Split(SplitComma);
if (parts.Length == 3 if (parts.Length == 3)
&& Exts.TryParseInt32Invariant(parts[0], out var rr) {
&& Exts.TryParseInt32Invariant(parts[1], out var rp) if (Exts.TryParseIntegerInvariant(parts[0], out var rr)
&& Exts.TryParseInt32Invariant(parts[2], out var ry)) && 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)); return new WRot(new WAngle(rr), new WAngle(rp), new WAngle(ry));
} }
}
return InvalidValueAction(value, fieldType, fieldName); return InvalidValueAction(value, fieldType, fieldName);
} }
@@ -275,33 +278,10 @@ namespace OpenRA
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries); var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 3) if (parts.Length == 3)
return new CPos( return new CPos(
Exts.ParseInt32Invariant(parts[0]), Exts.ParseIntegerInvariant(parts[0]),
Exts.ParseInt32Invariant(parts[1]), Exts.ParseIntegerInvariant(parts[1]),
Exts.ParseByteInvariant(parts[2])); Exts.ParseByte(parts[2]));
return new CPos(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1])); return new CPos(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(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); return InvalidValueAction(value, fieldType, fieldName);
@@ -312,7 +292,7 @@ namespace OpenRA
if (value != null) if (value != null)
{ {
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries); var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new CVec(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1])); return new CVec(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
} }
return InvalidValueAction(value, fieldType, fieldName); return InvalidValueAction(value, fieldType, fieldName);
@@ -405,7 +385,7 @@ namespace OpenRA
var ints = new int2[parts.Length / 2]; var ints = new int2[parts.Length / 2];
for (var i = 0; i < ints.Length; i++) for (var i = 0; i < ints.Length; i++)
ints[i] = new int2(Exts.ParseInt32Invariant(parts[2 * i]), Exts.ParseInt32Invariant(parts[2 * i + 1])); ints[i] = new int2(Exts.ParseIntegerInvariant(parts[2 * i]), Exts.ParseIntegerInvariant(parts[2 * i + 1]));
return ints; return ints;
} }
@@ -418,7 +398,7 @@ namespace OpenRA
if (value != null) if (value != null)
{ {
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries); var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new Size(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1])); return new Size(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
} }
return InvalidValueAction(value, fieldType, fieldName); return InvalidValueAction(value, fieldType, fieldName);
@@ -432,7 +412,7 @@ namespace OpenRA
if (parts.Length != 2) if (parts.Length != 2)
return InvalidValueAction(value, fieldType, fieldName); return InvalidValueAction(value, fieldType, fieldName);
return new int2(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1])); return new int2(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
} }
return InvalidValueAction(value, fieldType, fieldName); return InvalidValueAction(value, fieldType, fieldName);
@@ -480,10 +460,10 @@ namespace OpenRA
{ {
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries); var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new Rectangle( return new Rectangle(
Exts.ParseInt32Invariant(parts[0]), Exts.ParseIntegerInvariant(parts[0]),
Exts.ParseInt32Invariant(parts[1]), Exts.ParseIntegerInvariant(parts[1]),
Exts.ParseInt32Invariant(parts[2]), Exts.ParseIntegerInvariant(parts[2]),
Exts.ParseInt32Invariant(parts[3])); Exts.ParseIntegerInvariant(parts[3]));
} }
return InvalidValueAction(value, fieldType, fieldName); return InvalidValueAction(value, fieldType, fieldName);
@@ -520,7 +500,7 @@ namespace OpenRA
if (yaml == null) if (yaml == null)
return Activator.CreateInstance(fieldType); return Activator.CreateInstance(fieldType);
var dict = Activator.CreateInstance(fieldType, yaml.Nodes.Length); var dict = Activator.CreateInstance(fieldType, yaml.Nodes.Count);
var arguments = fieldType.GetGenericArguments(); var arguments = fieldType.GetGenericArguments();
var addMethod = fieldType.GetMethod(nameof(Dictionary<object, object>.Add), arguments); var addMethod = fieldType.GetMethod(nameof(Dictionary<object, object>.Add), arguments);
var addArgs = new object[2]; var addArgs = new object[2];
@@ -551,7 +531,7 @@ namespace OpenRA
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
return null; return null;
var innerType = fieldType.GetGenericArguments()[0]; var innerType = fieldType.GetGenericArguments().First();
var innerValue = GetValue("Nullable<T>", innerType, value, field); var innerValue = GetValue("Nullable<T>", innerType, value, field);
return fieldType.GetConstructor(new[] { innerType }).Invoke(new[] { innerValue }); return fieldType.GetConstructor(new[] { innerType }).Invoke(new[] { innerValue });
} }

View File

@@ -15,7 +15,6 @@ using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text;
using OpenRA.Primitives; using OpenRA.Primitives;
namespace OpenRA namespace OpenRA
@@ -59,7 +58,7 @@ namespace OpenRA
return new MiniYaml( return new MiniYaml(
null, null,
fields.Select(info => new MiniYamlNode(info.YamlName, FormatValue(o, info.Field)))); fields.Select(info => new MiniYamlNode(info.YamlName, FormatValue(o, info.Field))).ToList());
} }
public static MiniYamlNode SaveField(object o, string field) public static MiniYamlNode SaveField(object o, string field)
@@ -85,7 +84,7 @@ namespace OpenRA
// This is only for documentation generation // This is only for documentation generation
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>)) if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{ {
var result = new StringBuilder(); var result = "";
var dict = (System.Collections.IDictionary)v; var dict = (System.Collections.IDictionary)v;
foreach (var kvp in dict) foreach (var kvp in dict)
{ {
@@ -95,10 +94,10 @@ namespace OpenRA
var formattedKey = FormatValue(key); var formattedKey = FormatValue(key);
var formattedValue = FormatValue(value); var formattedValue = FormatValue(value);
result.Append($"{formattedKey}: {formattedValue}{Environment.NewLine}"); result += $"{formattedKey}: {formattedValue}{Environment.NewLine}";
} }
return result.ToString(); return result;
} }
if (v is DateTime d) if (v is DateTime d)

View File

@@ -16,7 +16,6 @@ using System.Linq;
using System.Net; using System.Net;
using System.Text; using System.Text;
using ICSharpCode.SharpZipLib.Checksum; using ICSharpCode.SharpZipLib.Checksum;
using ICSharpCode.SharpZipLib.Zip.Compression;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams; using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Primitives; using OpenRA.Primitives;
@@ -46,13 +45,12 @@ namespace OpenRA.FileFormats
var data = new List<byte>(); var data = new List<byte>();
Type = SpriteFrameType.Rgba32; Type = SpriteFrameType.Rgba32;
byte bitDepth = 8;
while (true) while (true)
{ {
var length = IPAddress.NetworkToHostOrder(s.ReadInt32()); var length = IPAddress.NetworkToHostOrder(s.ReadInt32());
var type = s.ReadASCII(4); var type = Encoding.UTF8.GetString(s.ReadBytes(4));
var content = s.ReadBytes(length); var content = s.ReadBytes(length);
s.ReadInt32(); // crc /*var crc = */s.ReadInt32();
if (!headerParsed && type != "IHDR") if (!headerParsed && type != "IHDR")
throw new InvalidDataException("Invalid PNG file - header does not appear first."); throw new InvalidDataException("Invalid PNG file - header does not appear first.");
@@ -68,7 +66,7 @@ namespace OpenRA.FileFormats
Width = IPAddress.NetworkToHostOrder(ms.ReadInt32()); Width = IPAddress.NetworkToHostOrder(ms.ReadInt32());
Height = IPAddress.NetworkToHostOrder(ms.ReadInt32()); Height = IPAddress.NetworkToHostOrder(ms.ReadInt32());
bitDepth = ms.ReadUInt8(); var bitDepth = ms.ReadUInt8();
var colorType = (PngColorType)ms.ReadUInt8(); var colorType = (PngColorType)ms.ReadUInt8();
if (IsPaletted(bitDepth, colorType)) if (IsPaletted(bitDepth, colorType))
Type = SpriteFrameType.Indexed8; Type = SpriteFrameType.Indexed8;
@@ -78,7 +76,7 @@ namespace OpenRA.FileFormats
Data = new byte[Width * Height * PixelStride]; Data = new byte[Width * Height * PixelStride];
var compression = ms.ReadUInt8(); var compression = ms.ReadUInt8();
ms.ReadUInt8(); // filter /*var filter = */ms.ReadUInt8();
var interlace = ms.ReadUInt8(); var interlace = ms.ReadUInt8();
if (compression != 0) if (compression != 0)
@@ -94,8 +92,8 @@ namespace OpenRA.FileFormats
case "PLTE": case "PLTE":
{ {
Palette = new Color[length / 3]; Palette = new Color[256];
for (var i = 0; i < Palette.Length; i++) for (var i = 0; i < length / 3; i++)
{ {
var r = ms.ReadUInt8(); var g = ms.ReadUInt8(); var b = ms.ReadUInt8(); var r = ms.ReadUInt8(); var g = ms.ReadUInt8(); var b = ms.ReadUInt8();
Palette[i] = Color.FromArgb(r, g, b); Palette[i] = Color.FromArgb(r, g, b);
@@ -138,34 +136,14 @@ namespace OpenRA.FileFormats
{ {
var pxStride = PixelStride; var pxStride = PixelStride;
var rowStride = Width * pxStride; var rowStride = Width * pxStride;
var pixelsPerByte = 8 / bitDepth;
var sourceRowStride = Exts.IntegerDivisionRoundingAwayFromZero(rowStride, pixelsPerByte);
Span<byte> prevLine = new byte[rowStride]; Span<byte> prevLine = new byte[rowStride];
for (var y = 0; y < Height; y++) for (var y = 0; y < Height; y++)
{ {
var filter = (PngFilter)ds.ReadUInt8(); var filter = (PngFilter)ds.ReadUInt8();
ds.ReadBytes(Data, y * rowStride, sourceRowStride); ds.ReadBytes(Data, y * rowStride, rowStride);
var line = Data.AsSpan(y * rowStride, rowStride); 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) switch (filter)
{ {
case PngFilter.None: case PngFilter.None:
@@ -291,7 +269,7 @@ namespace OpenRA.FileFormats
static bool IsPaletted(byte bitDepth, PngColorType colorType) 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; return true;
if (bitDepth == 8 && colorType == (PngColorType.Color | PngColorType.Alpha)) if (bitDepth == 8 && colorType == (PngColorType.Color | PngColorType.Alpha))
@@ -309,10 +287,10 @@ namespace OpenRA.FileFormats
var typeBytes = Encoding.ASCII.GetBytes(type); var typeBytes = Encoding.ASCII.GetBytes(type);
output.Write(IPAddress.HostToNetworkOrder((int)input.Length)); output.Write(IPAddress.HostToNetworkOrder((int)input.Length));
output.Write(typeBytes); output.WriteArray(typeBytes);
var data = input.ReadAllBytes(); var data = input.ReadAllBytes();
output.Write(data); output.WriteArray(data);
var crc32 = new Crc32(); var crc32 = new Crc32();
crc32.Update(typeBytes); crc32.Update(typeBytes);
@@ -324,7 +302,7 @@ namespace OpenRA.FileFormats
{ {
using (var output = new MemoryStream()) using (var output = new MemoryStream())
{ {
output.Write(Signature); output.WriteArray(Signature);
using (var header = new MemoryStream()) using (var header = new MemoryStream())
{ {
header.Write(IPAddress.HostToNetworkOrder(Width)); header.Write(IPAddress.HostToNetworkOrder(Width));
@@ -372,14 +350,13 @@ namespace OpenRA.FileFormats
using (var data = new MemoryStream()) using (var data = new MemoryStream())
{ {
using (var compressed = new DeflaterOutputStream(data, new Deflater(Deflater.BEST_COMPRESSION))) using (var compressed = new DeflaterOutputStream(data))
{ {
var rowStride = Width * PixelStride; var rowStride = Width * PixelStride;
for (var y = 0; y < Height; y++) for (var y = 0; y < Height; y++)
{ {
// Assuming no filtering for simplicity // Write uncompressed scanlines for simplicity
const byte FilterType = 0; compressed.WriteByte(0);
compressed.WriteByte(FilterType);
compressed.Write(Data, y * rowStride, rowStride); compressed.Write(Data, y * rowStride, rowStride);
} }
@@ -394,7 +371,7 @@ namespace OpenRA.FileFormats
{ {
using (var text = new MemoryStream()) using (var text = new MemoryStream())
{ {
text.Write(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value)); text.WriteArray(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
WritePngChunk(output, "tEXt", text); WritePngChunk(output, "tEXt", text);
} }
} }

View File

@@ -47,8 +47,8 @@ namespace OpenRA.FileFormats
throw new NotSupportedException($"Metadata version {version} is not supported"); throw new NotSupportedException($"Metadata version {version} is not supported");
// Read game info (max 100K limit as a safeguard against corrupted files) // Read game info (max 100K limit as a safeguard against corrupted files)
var data = fs.ReadLengthPrefixedString(Encoding.UTF8, 1024 * 100); var data = fs.ReadString(Encoding.UTF8, 1024 * 100);
GameInfo = GameInformation.Deserialize(data, path); GameInfo = GameInformation.Deserialize(data);
} }
public void Write(BinaryWriter writer) public void Write(BinaryWriter writer)
@@ -62,7 +62,7 @@ namespace OpenRA.FileFormats
{ {
// Write lobby info data // Write lobby info data
writer.Flush(); writer.Flush();
dataLength += writer.BaseStream.WriteLengthPrefixedString(Encoding.UTF8, GameInfo.Serialize()); dataLength += writer.BaseStream.WriteString(Encoding.UTF8, GameInfo.Serialize());
} }
// Write total length & end marker // Write total length & end marker

View File

@@ -23,7 +23,7 @@ namespace OpenRA.FileSystem
bool TryGetPackageContaining(string path, out IReadOnlyPackage package, out string filename); bool TryGetPackageContaining(string path, out IReadOnlyPackage package, out string filename);
bool TryOpen(string filename, out Stream s); bool TryOpen(string filename, out Stream s);
bool Exists(string filename); bool Exists(string filename);
bool IsExternalFile(string filename); bool IsExternalModFile(string filename);
} }
public class FileSystem : IReadOnlyFileSystem public class FileSystem : IReadOnlyFileSystem
@@ -83,14 +83,14 @@ namespace OpenRA.FileSystem
public void Mount(string name, string explicitName = null) public void Mount(string name, string explicitName = null)
{ {
var optional = name.StartsWith('~'); var optional = name.StartsWith("~", StringComparison.Ordinal);
if (optional) if (optional)
name = name[1..]; name = name[1..];
try try
{ {
IReadOnlyPackage package; IReadOnlyPackage package;
if (name.StartsWith('$')) if (name.StartsWith("$", StringComparison.Ordinal))
{ {
name = name[1..]; name = name[1..];
@@ -109,8 +109,10 @@ namespace OpenRA.FileSystem
Mount(package, explicitName); Mount(package, explicitName);
} }
catch when (optional) catch
{ {
if (!optional)
throw;
} }
} }
@@ -159,7 +161,9 @@ namespace OpenRA.FileSystem
explicitMounts.Remove(key); explicitMounts.Remove(key);
// Mod packages aren't owned by us, so we shouldn't dispose them // Mod packages aren't owned by us, so we shouldn't dispose them
if (!modPackages.Remove(package)) if (modPackages.Contains(package))
modPackages.Remove(package);
else
package.Dispose(); package.Dispose();
} }
else else
@@ -181,13 +185,11 @@ namespace OpenRA.FileSystem
fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>()); fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
} }
public void TrimExcess() public void LoadFromManifest(Manifest manifest)
{ {
mountedPackages.TrimExcess(); UnmountAll();
explicitMounts.TrimExcess(); foreach (var kv in manifest.Packages)
modPackages.TrimExcess(); Mount(kv.Key, kv.Value);
foreach (var packages in fileIndex.Values)
packages.TrimExcess();
} }
Stream GetFromCache(string filename) Stream GetFromCache(string filename)
@@ -224,12 +226,15 @@ namespace OpenRA.FileSystem
public bool TryOpen(string filename, out Stream s) public bool TryOpen(string filename, out Stream s)
{ {
var explicitSplit = filename.IndexOf('|'); var explicitSplit = filename.IndexOf('|');
if (explicitSplit > 0 && explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage)) if (explicitSplit > 0)
{
if (explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
{ {
s = explicitPackage.GetStream(filename[(explicitSplit + 1)..]); s = explicitPackage.GetStream(filename[(explicitSplit + 1)..]);
if (s != null) if (s != null)
return true; return true;
} }
}
s = GetFromCache(filename); s = GetFromCache(filename);
if (s != null) if (s != null)
@@ -257,20 +262,64 @@ namespace OpenRA.FileSystem
public bool Exists(string filename) public bool Exists(string filename)
{ {
var explicitSplit = filename.IndexOf('|'); 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))
explicitPackage.Contains(filename[(explicitSplit + 1)..])) if (explicitPackage.Contains(filename[(explicitSplit + 1)..]))
return true; return true;
return fileIndex.ContainsKey(filename); return fileIndex.ContainsKey(filename);
} }
/// <summary> /// <summary>
/// Returns true if the given filename references any file outside the mod mount. /// Returns true if the given filename references an external mod via an explicit mount.
/// </summary> /// </summary>
public bool IsExternalFile(string filename) public bool IsExternalModFile(string filename)
{ {
return !filename.StartsWith($"{modID}|", StringComparison.Ordinal); var explicitSplit = filename.IndexOf('|');
if (explicitSplit < 0)
return false;
if (!explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
return false;
if (installedMods[modID].Package == explicitPackage)
return false;
return modPackages.Contains(explicitPackage);
}
/// <summary>
/// Resolves a filesystem for an assembly, accounting for explicit and mod mounts.
/// Assemblies must exist in the native OS file system (not inside an OpenRA-defined package).
/// </summary>
public static string ResolveAssemblyPath(string path, Manifest manifest, InstalledMods installedMods)
{
var explicitSplit = path.IndexOf('|');
if (explicitSplit > 0 && !path.StartsWith("^"))
{
var parent = path[..explicitSplit];
var filename = path[(explicitSplit + 1)..];
var parentPath = manifest.Packages.FirstOrDefault(kv => kv.Value == parent).Key;
if (parentPath == null)
return null;
if (parentPath.StartsWith("$", StringComparison.Ordinal))
{
if (!installedMods.TryGetValue(parentPath[1..], out var mod))
return null;
if (mod.Package is not Folder)
return null;
path = Path.Combine(mod.Package.Name, filename);
}
else
path = Path.Combine(parentPath, filename);
}
var resolvedPath = Platform.ResolvePath(path);
return File.Exists(resolvedPath) ? resolvedPath : null;
} }
public static string ResolveCaseInsensitivePath(string path) public static string ResolveCaseInsensitivePath(string path)
@@ -286,8 +335,7 @@ namespace OpenRA.FileSystem
if (name == ".") if (name == ".")
continue; continue;
resolved = Directory.GetFileSystemEntries(resolved) resolved = Directory.GetFileSystemEntries(resolved).FirstOrDefault(e => e.Equals(Path.Combine(resolved, name), StringComparison.InvariantCultureIgnoreCase));
.FirstOrDefault(e => e.Equals(Path.Combine(resolved, name), StringComparison.InvariantCultureIgnoreCase));
if (resolved == null) if (resolved == null)
return null; return null;

View File

@@ -84,7 +84,7 @@ namespace OpenRA.FileSystem
// in FileSystem.OpenPackage. Their internal name therefore contains the // in FileSystem.OpenPackage. Their internal name therefore contains the
// full parent path too. We need to be careful to not add a second path // full parent path too. We need to be careful to not add a second path
// prefix to these hacked packages. // prefix to these hacked packages.
var filePath = filename.StartsWith(Name, StringComparison.Ordinal) ? filename : Path.Combine(Name, filename); var filePath = filename.StartsWith(Name) ? filename : Path.Combine(Name, filename);
Directory.CreateDirectory(Path.GetDirectoryName(filePath)); Directory.CreateDirectory(Path.GetDirectoryName(filePath));
using (var s = File.Create(filePath)) using (var s = File.Create(filePath))
@@ -98,7 +98,7 @@ namespace OpenRA.FileSystem
// in FileSystem.OpenPackage. Their internal name therefore contains the // in FileSystem.OpenPackage. Their internal name therefore contains the
// full parent path too. We need to be careful to not add a second path // full parent path too. We need to be careful to not add a second path
// prefix to these hacked packages. // prefix to these hacked packages.
var filePath = filename.StartsWith(Name, StringComparison.Ordinal) ? filename : Path.Combine(Name, filename); var filePath = filename.StartsWith(Name) ? filename : Path.Combine(Name, filename);
if (Directory.Exists(filePath)) if (Directory.Exists(filePath))
Directory.Delete(filePath, true); Directory.Delete(filePath, true);
else if (File.Exists(filePath)) else if (File.Exists(filePath))

View File

@@ -21,7 +21,7 @@ namespace OpenRA.FileSystem
{ {
const uint ZipSignature = 0x04034b50; const uint ZipSignature = 0x04034b50;
public class ReadOnlyZipFile : IReadOnlyPackage class ReadOnlyZipFile : IReadOnlyPackage
{ {
public string Name { get; protected set; } public string Name { get; protected set; }
protected ZipFile pkg; protected ZipFile pkg;
@@ -68,7 +68,6 @@ namespace OpenRA.FileSystem
public void Dispose() public void Dispose()
{ {
pkg?.Close(); pkg?.Close();
GC.SuppressFinalize(this);
} }
public IReadOnlyPackage OpenPackage(string filename, FileSystem context) public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
@@ -94,7 +93,7 @@ namespace OpenRA.FileSystem
} }
} }
public sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage
{ {
readonly MemoryStream pkgStream = new(); readonly MemoryStream pkgStream = new();
@@ -114,15 +113,14 @@ namespace OpenRA.FileSystem
pkgStream.Position = 0; pkgStream.Position = 0;
pkg = new ZipFile(pkgStream); pkg = new ZipFile(pkgStream);
Name = filename; Name = filename;
// Remove subfields that can break ZIP updating.
foreach (ZipEntry entry in pkg)
entry.ExtraData = null;
} }
void Commit() void Commit()
{ {
File.WriteAllBytes(Name, pkgStream.ToArray()); var pos = pkgStream.Position;
pkgStream.Position = 0;
File.WriteAllBytes(Name, pkgStream.ReadBytes((int)pkgStream.Length));
pkgStream.Position = pos;
} }
public void Update(string filename, byte[] contents) public void Update(string filename, byte[] contents)
@@ -149,7 +147,7 @@ namespace OpenRA.FileSystem
public ZipFolder(ReadOnlyZipFile parent, string path) public ZipFolder(ReadOnlyZipFile parent, string path)
{ {
if (path.EndsWith('/')) if (path.EndsWith("/", StringComparison.Ordinal))
path = path[..^1]; path = path[..^1];
Name = path; Name = path;

View File

@@ -1,167 +0,0 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Linguini.Bundle;
using Linguini.Bundle.Builder;
using Linguini.Shared.Types.Bundle;
using Linguini.Syntax.Parser;
using Linguini.Syntax.Parser.Error;
using OpenRA.FileSystem;
using OpenRA.Traits;
namespace OpenRA
{
[AttributeUsage(AttributeTargets.Field)]
public sealed class FluentReferenceAttribute : Attribute
{
public readonly bool Optional;
public readonly string[] RequiredVariableNames;
public readonly LintDictionaryReference DictionaryReference;
public FluentReferenceAttribute() { }
public FluentReferenceAttribute(params string[] requiredVariableNames)
{
RequiredVariableNames = requiredVariableNames;
}
public FluentReferenceAttribute(LintDictionaryReference dictionaryReference = LintDictionaryReference.None)
{
DictionaryReference = dictionaryReference;
}
public FluentReferenceAttribute(bool optional)
{
Optional = optional;
}
}
public class FluentBundle
{
readonly Linguini.Bundle.FluentBundle bundle;
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem)
: this(culture, paths, fileSystem, error => Log.Write("debug", error.Message)) { }
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, string text)
: this(culture, paths, fileSystem, text, error => Log.Write("debug", error.Message)) { }
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, Action<ParseError> onError)
: this(culture, paths, fileSystem, null, onError) { }
public FluentBundle(string culture, string text, Action<ParseError> onError)
: this(culture, null, null, text, onError) { }
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, string text, Action<ParseError> onError)
{
bundle = LinguiniBuilder.Builder()
.CultureInfo(new CultureInfo(culture))
.SkipResources()
.SetUseIsolating(false)
.UseConcurrent()
.UncheckedBuild();
if (paths != null)
{
foreach (var path in paths)
{
var stream = fileSystem.Open(path);
using (var reader = new StreamReader(stream))
{
var parser = new LinguiniParser(reader);
var resource = parser.Parse();
foreach (var error in resource.Errors)
onError(error);
bundle.AddResourceOverriding(resource);
}
}
}
if (!string.IsNullOrEmpty(text))
{
var parser = new LinguiniParser(text);
var resource = parser.Parse();
foreach (var error in resource.Errors)
onError(error);
bundle.AddResourceOverriding(resource);
}
}
public string GetMessage(string key, object[] args = null)
{
if (!TryGetMessage(key, out var message, args))
message = key;
return message;
}
public bool TryGetMessage(string key, out string value, object[] args = null)
{
if (key == null)
throw new ArgumentNullException(nameof(key));
try
{
if (!HasMessage(key))
{
value = null;
return false;
}
Dictionary<string, IFluentType> fluentArgs = null;
if (args != null)
{
if (args.Length % 2 != 0)
throw new ArgumentException("Expected a comma separated list of name, value arguments " +
"but the number of arguments is not a multiple of two", nameof(args));
fluentArgs = new Dictionary<string, IFluentType>();
for (var i = 0; i < args.Length; i += 2)
{
var argKey = args[i] as string;
if (string.IsNullOrEmpty(argKey))
throw new ArgumentException($"Expected the argument at index {i} to be a non-empty string", nameof(args));
var argValue = args[i + 1];
if (argValue == null)
throw new ArgumentNullException(nameof(args), $"Expected the argument at index {i + 1} to be a non-null value");
fluentArgs.Add(argKey, argValue.ToFluentType());
}
}
var result = bundle.TryGetAttrMessage(key, fluentArgs, out var errors, out value);
foreach (var error in errors)
Log.Write("debug", $"FluentBundle of {key}: {error}");
return result;
}
catch (Exception)
{
Log.Write("debug", $"FluentBundle of {key}: threw exception");
value = null;
return false;
}
}
public bool HasMessage(string key)
{
return bundle.HasAttrMessage(key);
}
}
}

View File

@@ -1,93 +0,0 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Text;
using OpenRA.FileSystem;
namespace OpenRA
{
public static class FluentProvider
{
// Ensure thread-safety.
static readonly object SyncObject = new();
static FluentBundle modFluentBundle;
static FluentBundle mapFluentBundle;
public static void Initialize(ModData modData, IReadOnlyFileSystem fileSystem)
{
lock (SyncObject)
{
modFluentBundle = new FluentBundle(modData.Manifest.FluentCulture, modData.Manifest.FluentMessages, fileSystem);
if (fileSystem is Map map && map.FluentMessageDefinitions != null)
{
var files = Array.Empty<string>();
if (map.FluentMessageDefinitions.Value != null)
files = FieldLoader.GetValue<string[]>("value", map.FluentMessageDefinitions.Value);
string text = null;
if (map.FluentMessageDefinitions.Nodes.Length > 0)
{
var builder = new StringBuilder();
foreach (var node in map.FluentMessageDefinitions.Nodes)
if (node.Key == "base64")
builder.Append(Encoding.UTF8.GetString(Convert.FromBase64String(node.Value.Value)));
text = builder.ToString();
}
mapFluentBundle = new FluentBundle(modData.Manifest.FluentCulture, files, fileSystem, text);
}
}
}
public static string GetMessage(string key, params object[] args)
{
lock (SyncObject)
{
// By prioritizing mod-level fluent bundles we prevent maps from overwriting string keys. We do not want to
// allow maps to change the UI nor any other strings not exposed to the map.
if (modFluentBundle.TryGetMessage(key, out var message, args))
return message;
if (mapFluentBundle != null)
return mapFluentBundle.GetMessage(key, args);
return key;
}
}
public static bool TryGetMessage(string key, out string message, params object[] args)
{
lock (SyncObject)
{
// By prioritizing mod-level bundle we prevent maps from overwriting string keys. We do not want to
// allow maps to change the UI nor any other strings not exposed to the map.
if (modFluentBundle.TryGetMessage(key, out message, args))
return true;
if (mapFluentBundle != null && mapFluentBundle.TryGetMessage(key, out message, args))
return true;
return false;
}
}
/// <summary>Should only be used by <see cref="MapPreview"/>.</summary>
internal static bool TryGetModMessage(string key, out string message, params object[] args)
{
lock (SyncObject)
{
return modFluentBundle.TryGetMessage(key, out message, args);
}
}
}
}

View File

@@ -29,7 +29,7 @@ namespace OpenRA
{ {
public static class Game public static class Game
{ {
[FluentReference("filename")] [TranslationReference("filename")]
const string SavedScreenshot = "notification-saved-screenshot"; const string SavedScreenshot = "notification-saved-screenshot";
public const int TimestepJankThreshold = 250; // Don't catch up for delays larger than 250ms public const int TimestepJankThreshold = 250; // Don't catch up for delays larger than 250ms
@@ -180,7 +180,6 @@ namespace OpenRA
} }
public static event Action BeforeGameStart = () => { }; public static event Action BeforeGameStart = () => { };
public static event Action AfterGameStart = () => { };
internal static void StartGame(string mapUID, WorldType type) internal static void StartGame(string mapUID, WorldType type)
{ {
// Dispose of the old world before creating a new one. // Dispose of the old world before creating a new one.
@@ -224,12 +223,6 @@ namespace OpenRA
// Much better to clean up now then to drop frames during gameplay for GC pauses. // Much better to clean up now then to drop frames during gameplay for GC pauses.
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(); 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() public static void RestartGame()
@@ -368,7 +361,22 @@ namespace OpenRA
Settings.Game.Platform = p; Settings.Game.Platform = p;
try try
{ {
var platform = CreatePlatform(p); var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + p + ".dll");
#if NET5_0_OR_GREATER
var loader = new AssemblyLoader(rendererPath);
var platformType = loader.LoadDefaultAssembly().GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
#else
// NOTE: This is currently the only use of System.Reflection in this file, so would give an unused using error if we import it above
var assembly = System.Reflection.Assembly.LoadFile(rendererPath);
var platformType = assembly.GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
#endif
if (platformType == null)
throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation.");
var platform = (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null);
Renderer = new Renderer(platform, Settings.Graphics); Renderer = new Renderer(platform, Settings.Graphics);
Sound = new Sound(platform, Settings.Sound); Sound = new Sound(platform, Settings.Sound);
@@ -395,7 +403,7 @@ namespace OpenRA
Mods = new InstalledMods(modSearchPaths, explicitModPaths); Mods = new InstalledMods(modSearchPaths, explicitModPaths);
Console.WriteLine("Internal mods:"); Console.WriteLine("Internal mods:");
foreach (var mod in Mods) foreach (var mod in Mods)
Console.WriteLine($"\t{mod.Key} ({mod.Value.Metadata.Version})"); Console.WriteLine($"\t{mod.Key}: {mod.Value.Metadata.Title} ({mod.Value.Metadata.Version})");
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null); modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
@@ -408,7 +416,7 @@ namespace OpenRA
// Sanitize input from platform-specific launchers // Sanitize input from platform-specific launchers
// Process.Start requires paths to not be quoted, even if they contain spaces // Process.Start requires paths to not be quoted, even if they contain spaces
if (launchPath != null && launchPath[0] == '"' && launchPath.Last() == '"') if (launchPath != null && launchPath.First() == '"' && launchPath.Last() == '"')
launchPath = launchPath[1..^1]; launchPath = launchPath[1..^1];
// Metadata registration requires an explicit launch path // Metadata registration requires an explicit launch path
@@ -420,31 +428,11 @@ namespace OpenRA
Console.WriteLine("External mods:"); Console.WriteLine("External mods:");
foreach (var mod in ExternalMods) foreach (var mod in ExternalMods)
Console.WriteLine($"\t{mod.Key} ({mod.Value.Version})"); Console.WriteLine($"\t{mod.Key}: {mod.Value.Title} ({mod.Value.Version})");
InitializeMod(modID, args); InitializeMod(modID, args);
} }
public static IPlatform CreatePlatform(string platformName)
{
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + platformName + ".dll");
#if NET5_0_OR_GREATER
var loader = new AssemblyLoader(rendererPath);
var platformType = loader.LoadDefaultAssembly().GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
#else
// NOTE: This is currently the only use of System.Reflection in this file, so would give an unused using error if we import it above
var assembly = System.Reflection.Assembly.LoadFile(rendererPath);
var platformType = assembly.GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
#endif
if (platformType == null)
throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation.");
return (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null);
}
public static void InitializeMod(string mod, Arguments args) public static void InitializeMod(string mod, Arguments args)
{ {
// Clear static state if we have switched mods // Clear static state if we have switched mods
@@ -496,11 +484,11 @@ namespace OpenRA
Renderer.InitializeDepthBuffer(grid); Renderer.InitializeDepthBuffer(grid);
Cursor?.Dispose(); Cursor?.Dispose();
Cursor = new CursorManager(ModData.CursorProvider, ModData.Manifest.CursorSheetSize); Cursor = new CursorManager(ModData.CursorProvider);
var metadata = ModData.Manifest.Metadata; var metadata = ModData.Manifest.Metadata;
if (!string.IsNullOrEmpty(metadata.WindowTitleTranslated)) if (!string.IsNullOrEmpty(metadata.WindowTitle))
Renderer.Window.SetWindowTitle(metadata.WindowTitleTranslated); Renderer.Window.SetWindowTitle(metadata.WindowTitle);
PerfHistory.Items["render"].HasNormalTick = false; PerfHistory.Items["render"].HasNormalTick = false;
PerfHistory.Items["batches"].HasNormalTick = false; PerfHistory.Items["batches"].HasNormalTick = false;
@@ -536,11 +524,10 @@ namespace OpenRA
.Where(m => m.Status == MapStatus.Available && m.Visibility.HasFlag(MapVisibility.Shellmap)) .Where(m => m.Status == MapStatus.Available && m.Visibility.HasFlag(MapVisibility.Shellmap))
.Select(m => m.Uid); .Select(m => m.Uid);
var shellmap = shellmaps.RandomOrDefault(CosmeticRandom); if (!shellmaps.Any())
if (shellmap == null)
throw new InvalidDataException("No valid shellmaps available"); throw new InvalidDataException("No valid shellmaps available");
return shellmap; return shellmaps.Random(CosmeticRandom);
} }
public static void SwitchToExternalMod(ExternalMod mod, string[] launchArguments = null, Action onFailed = null) public static void SwitchToExternalMod(ExternalMod mod, string[] launchArguments = null, Action onFailed = null)
@@ -595,7 +582,7 @@ namespace OpenRA
Log.Write("debug", "Taking screenshot " + path); Log.Write("debug", "Taking screenshot " + path);
Renderer.SaveScreenshot(path); Renderer.SaveScreenshot(path);
TextNotificationsManager.Debug(FluentProvider.GetMessage(SavedScreenshot, "filename", filename)); TextNotificationsManager.Debug(TranslationProvider.GetString(SavedScreenshot, Translation.Arguments("filename", filename)));
} }
} }
@@ -614,10 +601,7 @@ namespace OpenRA
if (orderManager.LastTickTime.ShouldAdvance(tick)) if (orderManager.LastTickTime.ShouldAdvance(tick))
{ {
if (orderManager.GameStarted && orderManager.LocalFrameNumber == 0) using (new PerfSample("tick_time"))
PerfHistory.Reset(); // Remove history that occurred whilst the new game was loading.
using (var sample = new PerfSample("tick_time"))
{ {
orderManager.LastTickTime.AdvanceTickTime(tick); orderManager.LastTickTime.AdvanceTickTime(tick);
@@ -626,11 +610,7 @@ namespace OpenRA
Sync.RunUnsynced(world, orderManager.TickImmediate); Sync.RunUnsynced(world, orderManager.TickImmediate);
if (world == null) if (world == null)
{
if (orderManager.GameStarted)
PerfHistory.Reset(); // Remove old history when a new game starts.
return; return;
}
if (orderManager.TryTick()) if (orderManager.TryTick())
{ {
@@ -684,7 +664,7 @@ namespace OpenRA
// Prepare renderables (i.e. render voxels) before calling BeginFrame // Prepare renderables (i.e. render voxels) before calling BeginFrame
using (new PerfSample("render_prepare")) using (new PerfSample("render_prepare"))
{ {
worldRenderer?.BeginFrame(); Renderer.WorldModelRenderer.BeginFrame();
// World rendering is disabled while the loading screen is displayed // World rendering is disabled while the loading screen is displayed
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave) if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
@@ -694,7 +674,7 @@ namespace OpenRA
} }
Ui.PrepareRenderables(); Ui.PrepareRenderables();
worldRenderer?.EndFrame(); Renderer.WorldModelRenderer.EndFrame();
} }
// worldRenderer is null during the initial install/download screen // worldRenderer is null during the initial install/download screen
@@ -798,7 +778,7 @@ namespace OpenRA
var logicWorld = worldRenderer?.World; var logicWorld = worldRenderer?.World;
// ReplayTimestep = 0 means the replay is paused: we need to keep logicInterval as UI.Timestep to avoid breakage // 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; logicInterval = logicWorld == OrderManager.World ? OrderManager.SuggestedTimestep : logicWorld.Timestep;
// Ideal time between screen updates // Ideal time between screen updates
@@ -933,15 +913,15 @@ namespace OpenRA
{ {
var endpoints = new List<IPEndPoint> var endpoints = new List<IPEndPoint>
{ {
new(IPAddress.IPv6Any, settings.ListenPort), new IPEndPoint(IPAddress.IPv6Any, settings.ListenPort),
new(IPAddress.Any, settings.ListenPort) new IPEndPoint(IPAddress.Any, settings.ListenPort)
}; };
server = new Server.Server(endpoints, settings, ModData, ServerType.Multiplayer); server = new Server.Server(endpoints, settings, ModData, ServerType.Multiplayer);
return server.GetEndpointForLocalConnection(); return server.GetEndpointForLocalConnection();
} }
public static ConnectionTarget CreateLocalServer(string map, bool isSkirmish = false) public static ConnectionTarget CreateLocalServer(string map)
{ {
var settings = new ServerSettings() var settings = new ServerSettings()
{ {
@@ -955,9 +935,9 @@ namespace OpenRA
// This would break the Restart button, which relies on the PlayerIndex always being the same for local servers // This would break the Restart button, which relies on the PlayerIndex always being the same for local servers
var endpoints = new List<IPEndPoint> var endpoints = new List<IPEndPoint>
{ {
new(IPAddress.Loopback, 0) new IPEndPoint(IPAddress.Loopback, 0)
}; };
server = new Server.Server(endpoints, settings, ModData, isSkirmish ? ServerType.Skirmish : ServerType.Local); server = new Server.Server(endpoints, settings, ModData, ServerType.Local);
return server.GetEndpointForLocalConnection(); return server.GetEndpointForLocalConnection();
} }
@@ -985,7 +965,7 @@ namespace OpenRA
Order.Command($"state {Session.ClientState.Ready}") Order.Command($"state {Session.ClientState.Ready}")
}; };
var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.PackageName) == launchMap); var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.Package.Name) == launchMap);
if (map == null) if (map == null)
throw new ArgumentException($"Could not find map '{launchMap}'."); throw new ArgumentException($"Could not find map '{launchMap}'.");

View File

@@ -19,9 +19,6 @@ namespace OpenRA
{ {
public class GameInformation public class GameInformation
{ {
[FluentReference("name", "number")]
const string EnumeratedBotName = "enumerated-bot-name";
public string Mod; public string Mod;
public string Version; public string Version;
@@ -52,13 +49,13 @@ namespace OpenRA
playersByRuntime = new Dictionary<OpenRA.Player, Player>(); playersByRuntime = new Dictionary<OpenRA.Player, Player>();
} }
public static GameInformation Deserialize(string data, string path) public static GameInformation Deserialize(string data)
{ {
try try
{ {
var info = new GameInformation(); var info = new GameInformation();
var nodes = MiniYaml.FromString(data, path); var nodes = MiniYaml.FromString(data);
foreach (var node in nodes) foreach (var node in nodes)
{ {
var keyParts = node.Key.Split('@'); var keyParts = node.Key.Split('@');
@@ -88,7 +85,7 @@ namespace OpenRA
{ {
var nodes = new List<MiniYamlNode> var nodes = new List<MiniYamlNode>
{ {
new("Root", FieldSaver.Save(this)) new MiniYamlNode("Root", FieldSaver.Save(this))
}; };
for (var i = 0; i < Players.Count; i++) for (var i = 0; i < Players.Count; i++)
@@ -121,12 +118,11 @@ namespace OpenRA
Name = runtimePlayer.PlayerName, Name = runtimePlayer.PlayerName,
IsHuman = !runtimePlayer.IsBot, IsHuman = !runtimePlayer.IsBot,
IsBot = runtimePlayer.IsBot, IsBot = runtimePlayer.IsBot,
BotType = runtimePlayer.BotType,
FactionName = runtimePlayer.Faction.Name, FactionName = runtimePlayer.Faction.Name,
FactionId = runtimePlayer.Faction.InternalName, FactionId = runtimePlayer.Faction.InternalName,
DisplayFactionName = runtimePlayer.DisplayFaction.Name, DisplayFactionName = runtimePlayer.DisplayFaction.Name,
DisplayFactionId = runtimePlayer.DisplayFaction.InternalName, DisplayFactionId = runtimePlayer.DisplayFaction.InternalName,
Color = OpenRA.Player.GetColor(runtimePlayer), Color = runtimePlayer.Color,
Team = client.Team, Team = client.Team,
Handicap = client.Handicap, Handicap = client.Handicap,
SpawnPoint = runtimePlayer.SpawnPoint, SpawnPoint = runtimePlayer.SpawnPoint,
@@ -147,19 +143,6 @@ namespace OpenRA
return player; return player;
} }
public string ResolvedPlayerName(Player player)
{
if (player.IsBot)
{
var number = Players.Where(p => p.BotType == player.BotType).ToList().IndexOf(player) + 1;
return FluentProvider.GetMessage(EnumeratedBotName,
"name", FluentProvider.GetMessage(player.Name),
"number", number);
}
return player.Name;
}
public class Player public class Player
{ {
#region Start-up information #region Start-up information
@@ -170,7 +153,6 @@ namespace OpenRA
public string Name; public string Name;
public bool IsHuman; public bool IsHuman;
public bool IsBot; public bool IsBot;
public string BotType;
/// <summary>The faction's display name.</summary> /// <summary>The faction's display name.</summary>
public string FactionName; public string FactionName;

View File

@@ -22,7 +22,7 @@ namespace OpenRA
/// </summary> /// </summary>
public class ActorInfo public class ActorInfo
{ {
public const char AbstractActorPrefix = '^'; public const string AbstractActorPrefix = "^";
public const char TraitInstanceSeparator = '@'; public const char TraitInstanceSeparator = '@';
/// <summary> /// <summary>
@@ -33,7 +33,7 @@ namespace OpenRA
/// </summary> /// </summary>
public readonly string Name; public readonly string Name;
readonly TypeDictionary traits = new(); readonly TypeDictionary traits = new();
TraitInfo[] constructOrderCache = null; List<TraitInfo> constructOrderCache = null;
public ActorInfo(ObjectCreator creator, string name, MiniYaml node) public ActorInfo(ObjectCreator creator, string name, MiniYaml node)
{ {
@@ -130,7 +130,6 @@ namespace OpenRA
// Continue resolving traits as long as possible. // 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. // 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(); var readyToResolve = more.ToList();
while (readyToResolve.Count != 0) while (readyToResolve.Count != 0)
{ {
@@ -139,7 +138,6 @@ namespace OpenRA
readyToResolve.Clear(); readyToResolve.Clear();
readyToResolve.AddRange(more); readyToResolve.AddRange(more);
} }
#pragma warning restore CA1851
if (unresolved.Count != 0) if (unresolved.Count != 0)
{ {
@@ -162,7 +160,7 @@ namespace OpenRA
throw new YamlException(exceptionString); throw new YamlException(exceptionString);
} }
constructOrderCache = resolved.Select(r => r.Trait).ToArray(); constructOrderCache = resolved.Select(r => r.Trait).ToList();
return constructOrderCache; return constructOrderCache;
} }
@@ -187,7 +185,7 @@ namespace OpenRA
public bool HasTraitInfo<T>() where T : ITraitInfoInterface { return traits.Contains<T>(); } public bool HasTraitInfo<T>() where T : ITraitInfoInterface { return traits.Contains<T>(); }
public T TraitInfo<T>() where T : ITraitInfoInterface { return traits.Get<T>(); } public T TraitInfo<T>() where T : ITraitInfoInterface { return traits.Get<T>(); }
public T TraitInfoOrDefault<T>() where T : ITraitInfoInterface { return traits.GetOrDefault<T>(); } public T TraitInfoOrDefault<T>() where T : ITraitInfoInterface { return traits.GetOrDefault<T>(); }
public IReadOnlyCollection<T> TraitInfos<T>() where T : ITraitInfoInterface { return traits.WithInterface<T>(); } public IEnumerable<T> TraitInfos<T>() where T : ITraitInfoInterface { return traits.WithInterface<T>(); }
public BitSet<TargetableType> GetAllTargetTypes() public BitSet<TargetableType> GetAllTargetTypes()
{ {

View File

@@ -124,7 +124,7 @@ namespace OpenRA
{ {
var actors = MergeOrDefault("Manifest,Rules", fs, m.Rules, null, null, var actors = MergeOrDefault("Manifest,Rules", fs, m.Rules, null, null,
k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value), k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value),
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix)); filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal));
var weapons = MergeOrDefault("Manifest,Weapons", fs, m.Weapons, null, null, var weapons = MergeOrDefault("Manifest,Weapons", fs, m.Weapons, null, null,
k => new WeaponInfo(k.Value)); k => new WeaponInfo(k.Value));
@@ -182,7 +182,7 @@ namespace OpenRA
{ {
var actors = MergeOrDefault("Rules", fileSystem, m.Rules, mapRules, dr.Actors, var actors = MergeOrDefault("Rules", fileSystem, m.Rules, mapRules, dr.Actors,
k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value), k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value),
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix)); filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal));
var weapons = MergeOrDefault("Weapons", fileSystem, m.Weapons, mapWeapons, dr.Weapons, var weapons = MergeOrDefault("Weapons", fileSystem, m.Weapons, mapWeapons, dr.Weapons,
k => new WeaponInfo(k.Value)); k => new WeaponInfo(k.Value));
@@ -226,10 +226,10 @@ namespace OpenRA
static bool AnyCustomYaml(MiniYaml yaml) static bool AnyCustomYaml(MiniYaml yaml)
{ {
return yaml != null && (yaml.Value != null || yaml.Nodes.Length > 0); return yaml != null && (yaml.Value != null || yaml.Nodes.Count > 0);
} }
static bool AnyFlaggedTraits(ModData modData, IEnumerable<MiniYamlNode> actors) static bool AnyFlaggedTraits(ModData modData, List<MiniYamlNode> actors)
{ {
foreach (var actorNode in actors) foreach (var actorNode in actors)
{ {
@@ -260,9 +260,8 @@ namespace OpenRA
return true; return true;
// Any trait overrides that aren't explicitly whitelisted are flagged // Any trait overrides that aren't explicitly whitelisted are flagged
if (mapRules == null) if (mapRules != null)
return false; {
if (AnyFlaggedTraits(modData, mapRules.Nodes)) if (AnyFlaggedTraits(modData, mapRules.Nodes))
return true; return true;
@@ -273,6 +272,7 @@ namespace OpenRA
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f))) if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
return true; return true;
} }
}
return false; return false;
} }

View File

@@ -40,16 +40,16 @@ namespace OpenRA.GameRules
static Dictionary<string, SoundPool> ParseSoundPool(MiniYaml y, string key) static Dictionary<string, SoundPool> ParseSoundPool(MiniYaml y, string key)
{ {
var ret = new Dictionary<string, SoundPool>(); var ret = new Dictionary<string, SoundPool>();
var classifiction = y.NodeWithKey(key); var classifiction = y.Nodes.First(x => x.Key == key);
foreach (var t in classifiction.Value.Nodes) foreach (var t in classifiction.Value.Nodes)
{ {
var volumeModifier = 1f; var volumeModifier = 1f;
var volumeModifierNode = t.Value.NodeWithKeyOrDefault(nameof(SoundPool.VolumeModifier)); var volumeModifierNode = t.Value.Nodes.FirstOrDefault(x => x.Key == nameof(SoundPool.VolumeModifier));
if (volumeModifierNode != null) if (volumeModifierNode != null)
volumeModifier = FieldLoader.GetValue<float>(volumeModifierNode.Key, volumeModifierNode.Value.Value); volumeModifier = FieldLoader.GetValue<float>(volumeModifierNode.Key, volumeModifierNode.Value.Value);
var interruptType = SoundPool.DefaultInterruptType; var interruptType = SoundPool.DefaultInterruptType;
var interruptTypeNode = t.Value.NodeWithKeyOrDefault(nameof(SoundPool.InterruptType)); var interruptTypeNode = t.Value.Nodes.FirstOrDefault(x => x.Key == nameof(SoundPool.InterruptType));
if (interruptTypeNode != null) if (interruptTypeNode != null)
interruptType = FieldLoader.GetValue<SoundPool.InterruptType>(interruptTypeNode.Key, interruptTypeNode.Value.Value); interruptType = FieldLoader.GetValue<SoundPool.InterruptType>(interruptTypeNode.Key, interruptTypeNode.Value.Value);

View File

@@ -139,14 +139,13 @@ namespace OpenRA.GameRules
{ {
// Resolve any weapon-level yaml inheritance or removals // Resolve any weapon-level yaml inheritance or removals
// HACK: The "Defaults" sequence syntax prevents us from doing this generally during yaml parsing // HACK: The "Defaults" sequence syntax prevents us from doing this generally during yaml parsing
content = content.WithNodes(MiniYaml.Merge(new IReadOnlyCollection<MiniYamlNode>[] { content.Nodes })); content.Nodes = MiniYaml.Merge(new[] { content.Nodes });
FieldLoader.Load(this, content); FieldLoader.Load(this, content);
} }
static object LoadProjectile(MiniYaml yaml) static object LoadProjectile(MiniYaml yaml)
{ {
var proj = yaml.NodeWithKeyOrDefault("Projectile")?.Value; if (!yaml.ToDictionary().TryGetValue("Projectile", out var proj))
if (proj == null)
return null; return null;
var ret = Game.CreateObject<IProjectileInfo>(proj.Value + "Info"); var ret = Game.CreateObject<IProjectileInfo>(proj.Value + "Info");
@@ -160,7 +159,7 @@ namespace OpenRA.GameRules
static object LoadWarheads(MiniYaml yaml) static object LoadWarheads(MiniYaml yaml)
{ {
var retList = new List<IWarhead>(); var retList = new List<IWarhead>();
foreach (var node in yaml.Nodes.Where(n => n.Key.StartsWith("Warhead", StringComparison.Ordinal))) foreach (var node in yaml.Nodes.Where(n => n.Key.StartsWith("Warhead")))
{ {
var ret = Game.CreateObject<IWarhead>(node.Value.Value + "Warhead"); var ret = Game.CreateObject<IWarhead>(node.Value.Value + "Warhead");
if (ret == null) if (ret == null)

View File

@@ -10,12 +10,13 @@
#endregion #endregion
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace OpenRA namespace OpenRA
{ {
public class GameSpeed public class GameSpeed
{ {
[FluentReference] [TranslationReference]
[FieldLoader.Require] [FieldLoader.Require]
public readonly string Name; public readonly string Name;
@@ -37,7 +38,7 @@ namespace OpenRA
static object LoadSpeeds(MiniYaml y) static object LoadSpeeds(MiniYaml y)
{ {
var ret = new Dictionary<string, GameSpeed>(); var ret = new Dictionary<string, GameSpeed>();
var speedsNode = y.NodeWithKeyOrDefault("Speeds"); var speedsNode = y.Nodes.FirstOrDefault(n => n.Key == "Speeds");
if (speedsNode == null) if (speedsNode == null)
throw new YamlException("Error parsing GameSpeeds: Missing Speeds node!"); throw new YamlException("Error parsing GameSpeeds: Missing Speeds node!");

View File

@@ -22,7 +22,6 @@ namespace OpenRA.Graphics
public string Name { get; private set; } public string Name { get; private set; }
public bool IsDecoration { get; set; } public bool IsDecoration { get; set; }
readonly Map map;
readonly SequenceSet sequences; readonly SequenceSet sequences;
readonly Func<WAngle> facingFunc; readonly Func<WAngle> facingFunc;
readonly Func<bool> paused; readonly Func<bool> paused;
@@ -44,7 +43,6 @@ namespace OpenRA.Graphics
public Animation(World world, string name, Func<WAngle> facingFunc, Func<bool> paused) public Animation(World world, string name, Func<WAngle> facingFunc, Func<bool> paused)
{ {
map = world.Map;
sequences = world.Map.Sequences; sequences = world.Map.Sequences;
Name = name.ToLowerInvariant(); Name = name.ToLowerInvariant();
this.facingFunc = facingFunc; this.facingFunc = facingFunc;
@@ -60,18 +58,13 @@ namespace OpenRA.Graphics
var tintModifiers = CurrentSequence.IgnoreWorldTint ? TintModifiers.IgnoreWorldTint : TintModifiers.None; var tintModifiers = CurrentSequence.IgnoreWorldTint ? TintModifiers.IgnoreWorldTint : TintModifiers.None;
var alpha = CurrentSequence.GetAlpha(CurrentFrame); var alpha = CurrentSequence.GetAlpha(CurrentFrame);
var (image, rotation) = CurrentSequence.GetSpriteWithRotation(CurrentFrame, facingFunc()); var (image, rotation) = CurrentSequence.GetSpriteWithRotation(CurrentFrame, facingFunc());
var imageRenderable = new SpriteRenderable( var imageRenderable = new SpriteRenderable(image, pos, offset, CurrentSequence.ZOffset + zOffset, palette, CurrentSequence.Scale, alpha, float3.Ones, tintModifiers, IsDecoration,
image, pos, offset, CurrentSequence.ZOffset + zOffset, palette, rotation);
CurrentSequence.Scale, alpha, float3.Ones, tintModifiers, IsDecoration, rotation);
var shadow = CurrentSequence.GetShadow(CurrentFrame, facingFunc()); var shadow = CurrentSequence.GetShadow(CurrentFrame, facingFunc());
if (shadow != null) if (shadow != null)
{ {
var height = map.DistanceAboveTerrain(pos).Length; var shadowRenderable = new SpriteRenderable(shadow, pos, offset, CurrentSequence.ShadowZOffset + zOffset, palette, CurrentSequence.Scale, 1f, float3.Ones, tintModifiers,
var shadowRenderable = new SpriteRenderable(
shadow, pos, offset - new WVec(0, 0, height), CurrentSequence.ShadowZOffset + zOffset + height, palette,
CurrentSequence.Scale, 1f, float3.Ones, tintModifiers,
true, rotation); true, rotation);
return new IRenderable[] { shadowRenderable, imageRenderable }; return new IRenderable[] { shadowRenderable, imageRenderable };
} }

View File

@@ -77,12 +77,11 @@ namespace OpenRA.Graphics
cachedPanelSprites = new Dictionary<string, Sprite[]>(); cachedPanelSprites = new Dictionary<string, Sprite[]>();
cachedCollectionSheets = new Dictionary<Collection, (Sheet, int)>(); cachedCollectionSheets = new Dictionary<Collection, (Sheet, int)>();
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var chrome = MiniYaml.Merge(modData.Manifest.Chrome var chrome = MiniYaml.Merge(modData.Manifest.Chrome
.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool))); .Select(s => MiniYaml.FromStream(fileSystem.Open(s), s)));
foreach (var c in chrome) foreach (var c in chrome)
if (!c.Key.StartsWith('^')) if (!c.Key.StartsWith("^", StringComparison.Ordinal))
LoadCollection(c.Key, c.Value); LoadCollection(c.Key, c.Value);
} }
@@ -228,18 +227,14 @@ namespace OpenRA.Graphics
(PanelSides.Bottom | PanelSides.Right, new Rectangle(pr[0] + pr[2] + pr[4], pr[1] + pr[3] + pr[5], pr[6], pr[7])) (PanelSides.Bottom | PanelSides.Right, new Rectangle(pr[0] + pr[2] + pr[4], pr[1] + pr[3] + pr[5], pr[6], pr[7]))
}; };
sprites = sides sprites = sides.Select(x => ps.HasSide(x.PanelSides) ? new Sprite(sheetDensity.Sheet, sheetDensity.Density * x.Bounds, TextureChannel.RGBA, 1f / sheetDensity.Density) : null)
.Select(x =>
ps.HasSide(x.PanelSides)
? new Sprite(sheetDensity.Sheet, sheetDensity.Density * x.Bounds, TextureChannel.RGBA, 1f / sheetDensity.Density)
: null)
.ToArray(); .ToArray();
} }
else else
{ {
// PERF: We don't need to search for images if there are no definitions. // PERF: We don't need to search for images if there are no definitions.
// PERF: It's more efficient to send an empty array rather than an array of 9 nulls. // PERF: It's more efficient to send an empty array rather than an array of 9 nulls.
if (collection.Regions.Count == 0) if (!collection.Regions.Any())
return Array.Empty<Sprite>(); return Array.Empty<Sprite>();
// Support manual definitions for unusual dialog layouts // Support manual definitions for unusual dialog layouts

View File

@@ -11,12 +11,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using OpenRA.Primitives; using OpenRA.Primitives;
namespace OpenRA.Graphics namespace OpenRA.Graphics
{ {
public sealed class CursorManager : IDisposable public sealed class CursorManager
{ {
sealed class Cursor sealed class Cursor
{ {
@@ -39,16 +38,13 @@ namespace OpenRA.Graphics
readonly bool hardwareCursorsDisabled = false; readonly bool hardwareCursorsDisabled = false;
bool hardwareCursorsDoubled = false; bool hardwareCursorsDoubled = false;
public CursorManager(CursorProvider cursorProvider, int cursorSheetSize) public CursorManager(CursorProvider cursorProvider)
{ {
hardwareCursorsDisabled = Game.Settings.Graphics.DisableHardwareCursors; hardwareCursorsDisabled = Game.Settings.Graphics.DisableHardwareCursors;
graphicSettings = Game.Settings.Graphics; graphicSettings = Game.Settings.Graphics;
sheetBuilder = new SheetBuilder(SheetType.BGRA, cursorSheetSize); sheetBuilder = new SheetBuilder(SheetType.BGRA);
foreach (var kv in cursorProvider.Cursors)
// Sort the cursors for better packing onto the sheet.
foreach (var kv in cursorProvider.Cursors
.OrderBy(kvp => kvp.Value.Frames.Max(f => f.Size.Height)))
{ {
var frames = kv.Value.Frames; var frames = kv.Value.Frames;
var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? cursorProvider.Palettes[kv.Value.Palette] : null; var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? cursorProvider.Palettes[kv.Value.Palette] : null;
@@ -95,6 +91,10 @@ namespace OpenRA.Graphics
} }
CreateOrUpdateHardwareCursors(); CreateOrUpdateHardwareCursors();
foreach (var s in sheetBuilder.AllSheets)
s.ReleaseBuffer();
Update(); Update();
} }
@@ -128,8 +128,6 @@ namespace OpenRA.Graphics
} }
} }
sheetBuilder.Current.ReleaseBuffer();
hardwareCursorsDoubled = graphicSettings.CursorDouble; hardwareCursorsDoubled = graphicSettings.CursorDouble;
} }
@@ -231,10 +229,6 @@ namespace OpenRA.Graphics
var width = frame.Size.Width; var width = frame.Size.Width;
var height = frame.Size.Height; var height = frame.Size.Height;
if (width == 0 || height == 0)
return Array.Empty<byte>();
var data = new byte[4 * width * height]; var data = new byte[4 * width * height];
unsafe unsafe
{ {

View File

@@ -24,11 +24,10 @@ namespace OpenRA.Graphics
public CursorProvider(ModData modData) public CursorProvider(ModData modData)
{ {
var fileSystem = modData.DefaultFileSystem; var fileSystem = modData.DefaultFileSystem;
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var sequenceYaml = MiniYaml.Merge(modData.Manifest.Cursors.Select( var sequenceYaml = MiniYaml.Merge(modData.Manifest.Cursors.Select(
s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool))); s => MiniYaml.FromStream(fileSystem.Open(s), s)));
var cursorsYaml = new MiniYaml(null, sequenceYaml).NodeWithKey("Cursors").Value; var nodesDict = new MiniYaml(null, sequenceYaml).ToDictionary();
// Overwrite previous definitions if there are duplicates // Overwrite previous definitions if there are duplicates
var pals = new Dictionary<string, IProvidesCursorPaletteInfo>(); var pals = new Dictionary<string, IProvidesCursorPaletteInfo>();
@@ -36,14 +35,14 @@ namespace OpenRA.Graphics
if (p.Palette != null) if (p.Palette != null)
pals[p.Palette] = p; pals[p.Palette] = p;
Palettes = cursorsYaml.Nodes.Select(n => n.Value.Value) Palettes = nodesDict["Cursors"].Nodes.Select(n => n.Value.Value)
.Where(p => p != null) .Where(p => p != null)
.Distinct() .Distinct()
.ToDictionary(p => p, p => pals[p].ReadPalette(modData.DefaultFileSystem)); .ToDictionary(p => p, p => pals[p].ReadPalette(modData.DefaultFileSystem));
var frameCache = new FrameCache(fileSystem, modData.SpriteLoaders); var frameCache = new FrameCache(fileSystem, modData.SpriteLoaders);
var cursors = new Dictionary<string, CursorSequence>(); var cursors = new Dictionary<string, CursorSequence>();
foreach (var s in cursorsYaml.Nodes) foreach (var s in nodesDict["Cursors"].Nodes)
foreach (var sequence in s.Value.Nodes) foreach (var sequence in s.Value.Nodes)
cursors.Add(sequence.Key, new CursorSequence(frameCache, sequence.Key, s.Key, s.Value.Value, sequence.Value)); cursors.Add(sequence.Key, new CursorSequence(frameCache, sequence.Key, s.Key, s.Value.Value, sequence.Value));

View File

@@ -27,7 +27,7 @@ namespace OpenRA.Graphics
{ {
var d = info.ToDictionary(); var d = info.ToDictionary();
Start = Exts.ParseInt32Invariant(d["Start"].Value); Start = Exts.ParseIntegerInvariant(d["Start"].Value);
Palette = palette; Palette = palette;
Name = name; Name = name;
@@ -38,9 +38,9 @@ namespace OpenRA.Graphics
(d.TryGetValue("End", out yaml) && yaml.Value == "*")) (d.TryGetValue("End", out yaml) && yaml.Value == "*"))
Length = Frames.Length; Length = Frames.Length;
else if (d.TryGetValue("Length", out yaml)) else if (d.TryGetValue("Length", out yaml))
Length = Exts.ParseInt32Invariant(yaml.Value); Length = Exts.ParseIntegerInvariant(yaml.Value);
else if (d.TryGetValue("End", out yaml)) else if (d.TryGetValue("End", out yaml))
Length = Exts.ParseInt32Invariant(yaml.Value) - Start; Length = Exts.ParseIntegerInvariant(yaml.Value) - Start;
else else
Length = 1; Length = 1;
@@ -54,13 +54,13 @@ namespace OpenRA.Graphics
if (d.TryGetValue("X", out yaml)) if (d.TryGetValue("X", out yaml))
{ {
Exts.TryParseInt32Invariant(yaml.Value, out var x); Exts.TryParseIntegerInvariant(yaml.Value, out var x);
Hotspot = Hotspot.WithX(x); Hotspot = Hotspot.WithX(x);
} }
if (d.TryGetValue("Y", out yaml)) if (d.TryGetValue("Y", out yaml))
{ {
Exts.TryParseInt32Invariant(yaml.Value, out var y); Exts.TryParseIntegerInvariant(yaml.Value, out var y);
Hotspot = Hotspot.WithY(y); Hotspot = Hotspot.WithY(y);
} }
} }

View File

@@ -85,10 +85,7 @@ namespace OpenRA.Graphics
public void ReplacePalette(string name, IPalette p) public void ReplacePalette(string name, IPalette p)
{ {
if (mutablePalettes.ContainsKey(name)) if (mutablePalettes.ContainsKey(name))
{
palettes[name] = new ImmutablePalette(p);
CopyPaletteToBuffer(indices[name], mutablePalettes[name] = new MutablePalette(p)); CopyPaletteToBuffer(indices[name], mutablePalettes[name] = new MutablePalette(p));
}
else if (palettes.ContainsKey(name)) else if (palettes.ContainsKey(name))
CopyPaletteToBuffer(indices[name], palettes[name] = new ImmutablePalette(p)); CopyPaletteToBuffer(indices[name], palettes[name] = new ImmutablePalette(p));
else else

View File

@@ -1,56 +0,0 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.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; }
}
}

View File

@@ -10,8 +10,9 @@
#endregion #endregion
using System; using System;
using System.Collections.Generic;
using OpenRA.FileSystem;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Graphics namespace OpenRA.Graphics
{ {
@@ -29,14 +30,6 @@ namespace OpenRA.Graphics
Rectangle AggregateBounds { get; } 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 struct ModelRenderData
{ {
public readonly int Start; public readonly int Start;
@@ -51,13 +44,51 @@ namespace OpenRA.Graphics
} }
} }
public interface IModelCacheInfo : ITraitInfoInterface { } public interface IModelCache : IDisposable
public interface IModelCache
{ {
IModel GetModel(string model); IModel GetModel(string model);
IModel GetModelSequence(string model, string sequence); IModel GetModelSequence(string model, string sequence);
bool HasModelSequence(string model, string sequence); bool HasModelSequence(string model, string sequence);
IVertexBuffer<ModelVertex> VertexBuffer { get; } 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();
}
} }
} }

View File

@@ -12,11 +12,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using OpenRA.Graphics;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Cnc.Traits namespace OpenRA.Graphics
{ {
public class ModelRenderProxy public class ModelRenderProxy
{ {
@@ -34,14 +32,7 @@ namespace OpenRA.Mods.Cnc.Traits
} }
} }
[TraitLocation(SystemActors.World | SystemActors.EditorWorld)] public sealed class ModelRenderer : IDisposable
[Desc("Render voxels")]
public class ModelRendererInfo : TraitInfo, Requires<IModelCacheInfo>
{
public override object Create(ActorInitializer init) { return new ModelRenderer(this, init.Self); }
}
public sealed class ModelRenderer : IDisposable, IRenderer, INotifyActorDisposing
{ {
// Static constants // Static constants
static readonly float[] ShadowDiffuse = new float[] { 0, 0, 0 }; static readonly float[] ShadowDiffuse = new float[] { 0, 0, 0 };
@@ -55,32 +46,28 @@ namespace OpenRA.Mods.Cnc.Traits
readonly Renderer renderer; readonly Renderer renderer;
readonly IShader shader; readonly IShader shader;
public readonly IModelCache ModelCache;
readonly Dictionary<Sheet, IFrameBuffer> mappedBuffers = new(); readonly Dictionary<Sheet, IFrameBuffer> mappedBuffers = new();
readonly Stack<KeyValuePair<Sheet, IFrameBuffer>> unmappedBuffers = new(); readonly Stack<KeyValuePair<Sheet, IFrameBuffer>> unmappedBuffers = new();
readonly List<(Sheet Sheet, Action Func)> doRender = new(); readonly List<(Sheet Sheet, Action Func)> doRender = new();
readonly int sheetSize;
SheetBuilder sheetBuilderForFrame; SheetBuilder sheetBuilderForFrame;
bool isInFrame; bool isInFrame;
public void SetPalette(HardwarePalette palette) public ModelRenderer(Renderer renderer, IShader shader)
{ {
shader.SetTexture("Palette", palette.Texture); this.renderer = renderer;
shader.SetVec("PaletteRows", palette.Height); this.shader = shader;
} }
public ModelRenderer(ModelRendererInfo info, Actor self) public void SetPalette(ITexture palette)
{ {
renderer = Game.Renderer; shader.SetTexture("Palette", palette);
shader = renderer.CreateShader(new ModelShaderBindings()); }
renderer.WorldRenderers = renderer.WorldRenderers.Append(this).ToArray();
ModelCache = self.Trait<IModelCache>(); public void SetViewportParams()
{
sheetSize = Game.Settings.Graphics.SheetSize; var a = 2f / renderer.SheetSize;
var a = 2f / sheetSize;
var view = new[] var view = new[]
{ {
a, 0, 0, 0, a, 0, 0, 0,
@@ -189,12 +176,12 @@ namespace OpenRA.Mods.Cnc.Traits
var spriteCenter = new float2(sb.Left + sb.Width / 2, sb.Top + sb.Height / 2); var spriteCenter = new float2(sb.Left + sb.Width / 2, sb.Top + sb.Height / 2);
var shadowCenter = new float2(ssb.Left + ssb.Width / 2, ssb.Top + ssb.Height / 2); var shadowCenter = new float2(ssb.Left + ssb.Width / 2, ssb.Top + ssb.Height / 2);
var translateMtx = Util.TranslationMatrix(spriteCenter.X - spriteOffset.X, sheetSize - (spriteCenter.Y - spriteOffset.Y), 0); var translateMtx = Util.TranslationMatrix(spriteCenter.X - spriteOffset.X, renderer.SheetSize - (spriteCenter.Y - spriteOffset.Y), 0);
var shadowTranslateMtx = Util.TranslationMatrix(shadowCenter.X - shadowSpriteOffset.X, sheetSize - (shadowCenter.Y - shadowSpriteOffset.Y), 0); var shadowTranslateMtx = Util.TranslationMatrix(shadowCenter.X - shadowSpriteOffset.X, renderer.SheetSize - (shadowCenter.Y - shadowSpriteOffset.Y), 0);
var correctionTransform = Util.MatrixMultiply(translateMtx, FlipMtx); var correctionTransform = Util.MatrixMultiply(translateMtx, FlipMtx);
var shadowCorrectionTransform = Util.MatrixMultiply(shadowTranslateMtx, ShadowScaleFlipMtx); var shadowCorrectionTransform = Util.MatrixMultiply(shadowTranslateMtx, ShadowScaleFlipMtx);
void RenderFunc() doRender.Add((sprite.Sheet, () =>
{ {
foreach (var m in models) foreach (var m in models)
{ {
@@ -226,18 +213,16 @@ namespace OpenRA.Mods.Cnc.Traits
// Transform light vector from shadow -> world -> limb coords // Transform light vector from shadow -> world -> limb coords
var lightDirection = ExtractRotationVector(Util.MatrixMultiply(it, lightTransform)); var lightDirection = ExtractRotationVector(Util.MatrixMultiply(it, lightTransform));
Render(rd, ModelCache, Util.MatrixMultiply(transform, t), lightDirection, Render(rd, wr.World.ModelCache, Util.MatrixMultiply(transform, t), lightDirection,
lightAmbientColor, lightDiffuseColor, color.TextureIndex, normals.TextureIndex); lightAmbientColor, lightDiffuseColor, color.TextureMidIndex, normals.TextureMidIndex);
// Disable shadow normals by forcing zero diffuse and identity ambient light // Disable shadow normals by forcing zero diffuse and identity ambient light
if (m.ShowShadow) if (m.ShowShadow)
Render(rd, ModelCache, Util.MatrixMultiply(shadow, t), lightDirection, Render(rd, wr.World.ModelCache, Util.MatrixMultiply(shadow, t), lightDirection,
ShadowAmbient, ShadowDiffuse, shadowPalette.TextureIndex, normals.TextureIndex); ShadowAmbient, ShadowDiffuse, shadowPalette.TextureMidIndex, normals.TextureMidIndex);
} }
} }
} }));
doRender.Add((sprite.Sheet, RenderFunc));
var screenLightVector = Util.MatrixVectorMultiply(invShadowTransform, ZVector); var screenLightVector = Util.MatrixVectorMultiply(invShadowTransform, ZVector);
screenLightVector = Util.MatrixVectorMultiply(cameraTransform, screenLightVector); screenLightVector = Util.MatrixVectorMultiply(cameraTransform, screenLightVector);
@@ -252,9 +237,9 @@ namespace OpenRA.Mods.Cnc.Traits
// Width and height must be even to avoid rendering glitches // Width and height must be even to avoid rendering glitches
if ((width & 1) == 1) if ((width & 1) == 1)
width++; width += 1;
if ((height & 1) == 1) if ((height & 1) == 1)
height++; height += 1;
size = new Size(width, height); size = new Size(width, height);
} }
@@ -282,17 +267,17 @@ namespace OpenRA.Mods.Cnc.Traits
IModelCache cache, IModelCache cache,
float[] t, float[] lightDirection, float[] t, float[] lightDirection,
float[] ambientLight, float[] diffuseLight, float[] ambientLight, float[] diffuseLight,
float colorPaletteTextureIndex, float normalsPaletteTextureIndex) float colorPaletteTextureMidIndex, float normalsPaletteTextureMidIndex)
{ {
shader.SetTexture("DiffuseTexture", renderData.Sheet.GetTexture()); shader.SetTexture("DiffuseTexture", renderData.Sheet.GetTexture());
shader.SetVec("Palettes", colorPaletteTextureIndex, normalsPaletteTextureIndex); shader.SetVec("PaletteRows", colorPaletteTextureMidIndex, normalsPaletteTextureMidIndex);
shader.SetMatrix("TransformMatrix", t); shader.SetMatrix("TransformMatrix", t);
shader.SetVec("LightDirection", lightDirection, 4); shader.SetVec("LightDirection", lightDirection, 4);
shader.SetVec("AmbientLight", ambientLight, 3); shader.SetVec("AmbientLight", ambientLight, 3);
shader.SetVec("DiffuseLight", diffuseLight, 3); shader.SetVec("DiffuseLight", diffuseLight, 3);
shader.PrepareRender(); shader.PrepareRender();
renderer.DrawBatch(cache.VertexBuffer, shader, renderData.Start, renderData.Count, PrimitiveType.TriangleList); renderer.DrawBatch(cache.VertexBuffer, renderData.Start, renderData.Count, PrimitiveType.TriangleList);
} }
public void BeginFrame() public void BeginFrame()
@@ -313,14 +298,14 @@ namespace OpenRA.Mods.Cnc.Traits
Game.Renderer.Flush(); Game.Renderer.Flush();
fbo.Bind(); fbo.Bind();
Game.Renderer.EnableDepthBuffer(); Game.Renderer.Context.EnableDepthBuffer();
return fbo; return fbo;
} }
static void DisableFrameBuffer(IFrameBuffer fbo) static void DisableFrameBuffer(IFrameBuffer fbo)
{ {
Game.Renderer.Flush(); Game.Renderer.Flush();
Game.Renderer.DisableDepthBuffer(); Game.Renderer.Context.DisableDepthBuffer();
fbo.Unbind(); fbo.Unbind();
} }
@@ -368,7 +353,8 @@ namespace OpenRA.Mods.Cnc.Traits
return kv.Key; return kv.Key;
} }
var framebuffer = renderer.CreateFrameBuffer(new Size(sheetSize, sheetSize)); var size = new Size(renderer.SheetSize, renderer.SheetSize);
var framebuffer = renderer.Context.CreateFrameBuffer(size);
var sheet = new Sheet(SheetType.BGRA, framebuffer.Texture); var sheet = new Sheet(SheetType.BGRA, framebuffer.Texture);
mappedBuffers.Add(sheet, framebuffer); mappedBuffers.Add(sheet, framebuffer);
@@ -385,12 +371,6 @@ namespace OpenRA.Mods.Cnc.Traits
mappedBuffers.Clear(); mappedBuffers.Clear();
unmappedBuffers.Clear(); unmappedBuffers.Clear();
renderer.WorldRenderers = renderer.WorldRenderers.Where(r => r != this).ToArray();
}
void INotifyActorDisposing.Disposing(Actor a)
{
Dispose();
} }
} }
} }

View File

@@ -1,53 +0,0 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.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),
};
}
}

View File

@@ -30,7 +30,7 @@ namespace OpenRA.Graphics
public static Color GetColor(this IPalette palette, int index) public static Color GetColor(this IPalette palette, int index)
{ {
return Color.FromArgb(palette[index]); return Color.FromArgb((int)palette[index]);
} }
public static IPalette AsReadOnly(this IPalette palette) public static IPalette AsReadOnly(this IPalette palette)
@@ -103,7 +103,7 @@ namespace OpenRA.Graphics
: this(p) : this(p)
{ {
for (var i = 0; i < Palette.Size; i++) for (var i = 0; i < Palette.Size; i++)
colors[i] = r.GetRemappedColor(this.GetColor(i), i).ToArgb(); colors[i] = (uint)r.GetRemappedColor(this.GetColor(i), i).ToArgb();
} }
public ImmutablePalette(IPalette p) public ImmutablePalette(IPalette p)
@@ -142,7 +142,7 @@ namespace OpenRA.Graphics
public void SetColor(int index, Color color) public void SetColor(int index, Color color)
{ {
colors[index] = color.ToArgb(); colors[index] = (uint)color.ToArgb();
} }
public void SetFromPalette(IPalette p) public void SetFromPalette(IPalette p)
@@ -153,7 +153,7 @@ namespace OpenRA.Graphics
public void ApplyRemap(IPaletteRemap r) public void ApplyRemap(IPaletteRemap r)
{ {
for (var i = 0; i < Palette.Size; i++) for (var i = 0; i < Palette.Size; i++)
colors[i] = r.GetRemappedColor(this.GetColor(i), i).ToArgb(); colors[i] = (uint)r.GetRemappedColor(this.GetColor(i), i).ToArgb();
} }
} }
} }

View File

@@ -13,17 +13,19 @@ namespace OpenRA.Graphics
{ {
public sealed class PaletteReference public sealed class PaletteReference
{ {
readonly float index;
readonly HardwarePalette hardwarePalette; readonly HardwarePalette hardwarePalette;
public readonly string Name; public readonly string Name;
public IPalette Palette { get; internal set; } public IPalette Palette { get; internal set; }
public int TextureIndex { get; } public float TextureIndex => index / hardwarePalette.Height;
public float TextureMidIndex => (index + 0.5f) / hardwarePalette.Height;
public PaletteReference(string name, int index, IPalette palette, HardwarePalette hardwarePalette) public PaletteReference(string name, int index, IPalette palette, HardwarePalette hardwarePalette)
{ {
Name = name; Name = name;
Palette = palette; Palette = palette;
TextureIndex = index; this.index = index;
this.hardwarePalette = hardwarePalette; this.hardwarePalette = hardwarePalette;
} }

View File

@@ -20,13 +20,13 @@ namespace OpenRA
Automatic, Automatic,
ANGLE, ANGLE,
Modern, Modern,
Embedded Embedded,
Legacy
} }
public interface IPlatform public interface IPlatform
{ {
IPlatformWindow CreateWindow( IPlatformWindow CreateWindow(Size size, WindowMode windowMode, float scaleModifier, int batchSize, int videoDisplay, GLProfile profile, bool enableLegacyGL);
Size size, WindowMode windowMode, float scaleModifier, int vertexBatchSize, int indexBatchSize, int videoDisplay, GLProfile profile);
ISoundEngine CreateSound(string device); ISoundEngine CreateSound(string device);
IFont CreateFont(byte[] data); IFont CreateFont(byte[] data);
} }
@@ -83,18 +83,16 @@ namespace OpenRA
public interface IGraphicsContext : IDisposable public interface IGraphicsContext : IDisposable
{ {
IVertexBuffer<T> CreateVertexBuffer<T>(int size) where T : struct; IVertexBuffer<Vertex> CreateVertexBuffer(int size);
T[] CreateVertices<T>(int size) where T : struct; Vertex[] CreateVertices(int size);
IIndexBuffer CreateIndexBuffer(uint[] indices);
ITexture CreateTexture(); ITexture CreateTexture();
IFrameBuffer CreateFrameBuffer(Size s); IFrameBuffer CreateFrameBuffer(Size s);
IFrameBuffer CreateFrameBuffer(Size s, Color clearColor); IFrameBuffer CreateFrameBuffer(Size s, Color clearColor);
IShader CreateShader(IShaderBindings shaderBindings); IShader CreateShader(string name);
void EnableScissor(int x, int y, int width, int height); void EnableScissor(int x, int y, int width, int height);
void DisableScissor(); void DisableScissor();
void Present(); void Present();
void DrawPrimitives(PrimitiveType pt, int firstVertex, int numVertices); void DrawPrimitives(PrimitiveType pt, int firstVertex, int numVertices);
void DrawElements(int numIndices, int offset);
void Clear(); void Clear();
void EnableDepthBuffer(); void EnableDepthBuffer();
void DisableDepthBuffer(); void DisableDepthBuffer();
@@ -104,14 +102,7 @@ namespace OpenRA
string GLVersion { get; } string GLVersion { get; }
} }
public interface IRenderer public interface IVertexBuffer<T> : IDisposable
{
void BeginFrame();
void EndFrame();
void SetPalette(HardwarePalette palette);
}
public interface IVertexBuffer<T> : IDisposable where T : struct
{ {
void Bind(); void Bind();
void SetData(T[] vertices, int length); void SetData(T[] vertices, int length);
@@ -123,11 +114,6 @@ namespace OpenRA
void SetData(T[] vertices, int offset, int start, int length); void SetData(T[] vertices, int offset, int start, int length);
} }
public interface IIndexBuffer : IDisposable
{
void Bind();
}
public interface IShader public interface IShader
{ {
void SetBool(string name, bool value); void SetBool(string name, bool value);
@@ -138,17 +124,6 @@ namespace OpenRA
void SetTexture(string param, ITexture texture); void SetTexture(string param, ITexture texture);
void SetMatrix(string param, float[] mtx); void SetMatrix(string param, float[] mtx);
void PrepareRender(); void PrepareRender();
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 } public enum TextureScaleFilter { Nearest, Linear }
@@ -157,7 +132,6 @@ namespace OpenRA
{ {
void SetData(byte[] colors, int width, int height); void SetData(byte[] colors, int width, int height);
void SetFloatData(float[] data, int width, int height); void SetFloatData(float[] data, int width, int height);
void SetDataFromReadBuffer(Rectangle rect);
byte[] GetData(); byte[] GetData();
Size Size { get; } Size Size { get; }
TextureScaleFilter ScaleFilter { get; set; } TextureScaleFilter ScaleFilter { get; set; }

View File

@@ -1,64 +0,0 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.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),
};
}
}

View File

@@ -21,7 +21,7 @@ namespace OpenRA.Graphics
static readonly float3 Offset = new(0.5f, 0.5f, 0f); static readonly float3 Offset = new(0.5f, 0.5f, 0f);
readonly SpriteRenderer parent; readonly SpriteRenderer parent;
readonly Vertex[] vertices = new Vertex[4]; readonly Vertex[] vertices = new Vertex[6];
public RgbaColorRenderer(SpriteRenderer parent) public RgbaColorRenderer(SpriteRenderer parent)
{ {
@@ -45,12 +45,14 @@ namespace OpenRA.Graphics
var eb = endColor.B / 255.0f; var eb = endColor.B / 255.0f;
var ea = endColor.A / 255.0f; var ea = endColor.A / 255.0f;
vertices[0] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0); 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); 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); 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); 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);
parent.DrawRGBAQuad(vertices, blendMode); parent.DrawRGBAVertices(vertices, blendMode);
} }
public void DrawLine(in float3 start, in float3 end, float width, Color color, BlendMode blendMode = BlendMode.Alpha) public void DrawLine(in float3 start, in float3 end, float width, Color color, BlendMode blendMode = BlendMode.Alpha)
@@ -64,11 +66,13 @@ namespace OpenRA.Graphics
var b = color.B / 255.0f; var b = color.B / 255.0f;
var a = color.A / 255.0f; var a = color.A / 255.0f;
vertices[0] = new Vertex(start - corner + Offset, r, g, b, a, 0); 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); 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); 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); vertices[3] = new Vertex(end + corner + Offset, r, g, b, a, 0, 0);
parent.DrawRGBAQuad(vertices, blendMode); 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);
} }
/// <summary> /// <summary>
@@ -153,11 +157,13 @@ namespace OpenRA.Graphics
var cd = closed || i < limit - 1 ? IntersectionOf(end - corner, dir, end - nextCorner, nextDir) : end - corner; var cd = closed || i < limit - 1 ? IntersectionOf(end - corner, dir, end - nextCorner, nextDir) : end - corner;
// Fill segment // Fill segment
vertices[0] = new Vertex(ca + Offset, r, g, b, a, 0); vertices[0] = new Vertex(ca + Offset, r, g, b, a, 0, 0);
vertices[1] = new Vertex(cb + Offset, r, g, b, a, 0); vertices[1] = new Vertex(cb + Offset, r, g, b, a, 0, 0);
vertices[2] = new Vertex(cc + Offset, r, g, b, a, 0); vertices[2] = new Vertex(cc + Offset, r, g, b, a, 0, 0);
vertices[3] = new Vertex(cd + Offset, r, g, b, a, 0); vertices[3] = new Vertex(cc + Offset, r, g, b, a, 0, 0);
parent.DrawRGBAQuad(vertices, blendMode); 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);
// Advance line segment // Advance line segment
end = next; end = next;
@@ -194,6 +200,20 @@ namespace OpenRA.Graphics
DrawPolygon(new[] { tl, tr, br, bl }, width, color, blendMode); 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) 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); var tr = new float3(br.X, tl.Y, tl.Z);
@@ -209,22 +229,25 @@ namespace OpenRA.Graphics
var cb = color.B / 255.0f; var cb = color.B / 255.0f;
var ca = color.A / 255.0f; var ca = color.A / 255.0f;
vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0); vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0);
vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0); vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0, 0);
vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0); vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
vertices[3] = new Vertex(d + Offset, cr, cg, cb, ca, 0); vertices[3] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
parent.DrawRGBAQuad(vertices, blendMode); 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);
} }
public void FillRect(in float3 a, in float3 b, in float3 c, in float3 d, 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)
Color topLeftColor, Color topRightColor, Color bottomRightColor, Color bottomLeftColor, BlendMode blendMode = BlendMode.Alpha)
{ {
vertices[0] = VertexWithColor(a + Offset, topLeftColor); vertices[0] = VertexWithColor(a + Offset, topLeftColor);
vertices[1] = VertexWithColor(b + Offset, topRightColor); vertices[1] = VertexWithColor(b + Offset, topRightColor);
vertices[2] = VertexWithColor(c + Offset, bottomRightColor); vertices[2] = VertexWithColor(c + Offset, bottomRightColor);
vertices[3] = VertexWithColor(d + Offset, bottomLeftColor); vertices[3] = VertexWithColor(c + Offset, bottomRightColor);
vertices[4] = VertexWithColor(d + Offset, bottomLeftColor);
vertices[5] = VertexWithColor(a + Offset, topLeftColor);
parent.DrawRGBAQuad(vertices, blendMode); parent.DrawRGBAVertices(vertices, blendMode);
} }
static Vertex VertexWithColor(in float3 xyz, Color color) static Vertex VertexWithColor(in float3 xyz, Color color)
@@ -235,7 +258,7 @@ namespace OpenRA.Graphics
var cb = color.B / 255.0f; var cb = color.B / 255.0f;
var ca = color.A / 255.0f; var ca = color.A / 255.0f;
return new Vertex(xyz, cr, cg, cb, ca, 0); return new Vertex(xyz, cr, cg, cb, ca, 0, 0);
} }
public void FillEllipse(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha) public void FillEllipse(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha)

View File

@@ -94,7 +94,7 @@ namespace OpenRA.Graphics
foreach (var node in nodes) foreach (var node in nodes)
{ {
// Nodes starting with ^ are inheritable but never loaded directly // Nodes starting with ^ are inheritable but never loaded directly
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix)) if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal))
continue; continue;
images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node); images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node);

View File

@@ -1,70 +0,0 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.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);
}
}
}

View File

@@ -132,7 +132,6 @@ namespace OpenRA.Graphics
{ {
if (!Buffered) if (!Buffered)
return; return;
dirty = true; dirty = true;
releaseBufferOnCommit = true; releaseBufferOnCommit = true;
@@ -141,29 +140,6 @@ namespace OpenRA.Graphics
GetTexture(); GetTexture();
} }
public bool ReleaseBufferAndTryTransferTo(Sheet destination)
{
if (Size != destination.Size)
throw new ArgumentException("Destination sheet does not have the same size", nameof(destination));
var buffer = data;
ReleaseBuffer();
// We aren't commiting data to the GPU, so let's not delete our data.
if (Game.Renderer == null)
return false;
// Only transfer if the destination has no data that would be lost by overwriting.
if (buffer != null && destination.data == null && destination.texture == null)
{
Array.Clear(buffer, 0, buffer.Length);
destination.data = buffer;
return true;
}
return false;
}
public void Dispose() public void Dispose()
{ {
texture?.Dispose(); texture?.Dispose();

View File

@@ -82,16 +82,16 @@ namespace OpenRA.Graphics
this.margin = margin; this.margin = margin;
} }
public Sprite Add(ISpriteFrame frame, bool premultiplied = false) { return Add(frame.Data, frame.Type, frame.Size, 0, frame.Offset, premultiplied); } 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, bool premultiplied = false) { return Add(src, type, size, 0, float3.Zero, premultiplied); } 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, bool premultiplied = false) public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset)
{ {
// Don't bother allocating empty sprites // Don't bother allocating empty sprites
if (size.Width == 0 || size.Height == 0) if (size.Width == 0 || size.Height == 0)
return new Sprite(Current, Rectangle.Empty, 0, spriteOffset, CurrentChannel, BlendMode.Alpha); return new Sprite(Current, Rectangle.Empty, 0, spriteOffset, CurrentChannel, BlendMode.Alpha);
var rect = Allocate(size, zRamp, spriteOffset); var rect = Allocate(size, zRamp, spriteOffset);
Util.FastCopyIntoChannel(rect, src, type, premultiplied); Util.FastCopyIntoChannel(rect, src, type);
Current.CommitBufferedData(); Current.CommitBufferedData();
return rect; return rect;
} }
@@ -130,13 +130,8 @@ namespace OpenRA.Graphics
var next = NextChannel(CurrentChannel); var next = NextChannel(CurrentChannel);
if (next == null) if (next == null)
{ {
var previous = Current; Current.ReleaseBuffer();
Current = allocateSheet(); Current = allocateSheet();
// Reuse the backing buffer between sheets where possible.
// This avoids allocating additional buffers which the GC must clean up.
previous.ReleaseBufferAndTryTransferTo(Current);
sheets.Add(Current); sheets.Add(Current);
CurrentChannel = Type == SheetType.Indexed ? TextureChannel.Red : TextureChannel.RGBA; CurrentChannel = Type == SheetType.Indexed ? TextureChannel.Red : TextureChannel.RGBA;
} }
@@ -147,9 +142,7 @@ namespace OpenRA.Graphics
p = int2.Zero; p = int2.Zero;
} }
var rect = new Sprite( var rect = new Sprite(Current, new Rectangle(p.X + margin, p.Y + margin, imageSize.Width, imageSize.Height), zRamp, spriteOffset, CurrentChannel, BlendMode.Alpha, scale);
Current, new Rectangle(p.X + margin, p.Y + margin, imageSize.Width, imageSize.Height),
zRamp, spriteOffset, CurrentChannel, BlendMode.Alpha, scale);
p += new int2(imageSize.Width + margin, 0); p += new int2(imageSize.Width + margin, 0);
return rect; return rect;

View File

@@ -42,11 +42,11 @@ namespace OpenRA.Graphics
// in rendering a line of texels that sample outside the sprite rectangle. // 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 // Insetting the texture coordinates by a small fraction of a pixel avoids this
// with negligible impact on the 1:1 rendering case. // with negligible impact on the 1:1 rendering case.
const float Inset = 1 / 128f; var inset = 1 / 128f;
Left = (Math.Min(bounds.Left, bounds.Right) + Inset) / sheet.Size.Width; Left = (Math.Min(bounds.Left, bounds.Right) + inset) / sheet.Size.Width;
Top = (Math.Min(bounds.Top, bounds.Bottom) + Inset) / sheet.Size.Height; Top = (Math.Min(bounds.Top, bounds.Bottom) + inset) / sheet.Size.Height;
Right = (Math.Max(bounds.Left, bounds.Right) - Inset) / sheet.Size.Width; Right = (Math.Max(bounds.Left, bounds.Right) - inset) / sheet.Size.Width;
Bottom = (Math.Max(bounds.Top, bounds.Bottom) - Inset) / sheet.Size.Height; Bottom = (Math.Max(bounds.Top, bounds.Bottom) - inset) / sheet.Size.Height;
} }
} }

View File

@@ -14,30 +14,28 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using OpenRA.FileSystem; using OpenRA.FileSystem;
using OpenRA.Primitives;
namespace OpenRA.Graphics namespace OpenRA.Graphics
{ {
public delegate ISpriteFrame AdjustFrame(ISpriteFrame input, int index, int total);
public sealed class SpriteCache : IDisposable public sealed class SpriteCache : IDisposable
{ {
public readonly Dictionary<SheetType, SheetBuilder> SheetBuilders; public readonly Dictionary<SheetType, SheetBuilder> SheetBuilders;
readonly ISpriteLoader[] loaders; readonly ISpriteLoader[] loaders;
readonly IReadOnlyFileSystem fileSystem; readonly IReadOnlyFileSystem fileSystem;
readonly Dictionary< readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> spriteReservations = new();
int, readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> frameReservations = new();
(int[] Frames, MiniYamlNode.SourceLocation Location, AdjustFrame AdjustFrame, bool Premultiplied)> spriteReservations = new();
readonly Dictionary<string, List<int>> reservationsByFilename = new(); readonly Dictionary<string, List<int>> reservationsByFilename = new();
readonly Dictionary<int, ISpriteFrame[]> resolvedFrames = new();
readonly Dictionary<int, Sprite[]> resolvedSprites = new(); readonly Dictionary<int, Sprite[]> resolvedSprites = new();
readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = new(); readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = new();
int nextReservationToken = 1; int nextReservationToken = 1;
public SpriteCache( public SpriteCache(IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders, int bgraSheetSize, int indexedSheetSize, int bgraSheetMargin = 1, int indexedSheetMargin = 1)
IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders, int bgraSheetSize, int indexedSheetSize, int bgraSheetMargin = 1, int indexedSheetMargin = 1)
{ {
SheetBuilders = new Dictionary<SheetType, SheetBuilder> SheetBuilders = new Dictionary<SheetType, SheetBuilder>
{ {
@@ -49,110 +47,97 @@ namespace OpenRA.Graphics
this.loaders = loaders; this.loaders = loaders;
} }
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location, public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
AdjustFrame adjustFrame = null, bool premultiplied = false)
{ {
var token = nextReservationToken++; var token = nextReservationToken++;
spriteReservations[token] = (frames?.ToArray(), location, adjustFrame, premultiplied); spriteReservations[token] = (frames?.ToArray(), location);
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token); reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
return token; return token;
} }
static ISpriteFrame[] GetFrames(IReadOnlyFileSystem fileSystem, string filename, ISpriteLoader[] loaders) public int ReserveFrames(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
{ {
var token = nextReservationToken++;
frameReservations[token] = (frames?.ToArray(), location);
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
return token;
}
static ISpriteFrame[] GetFrames(IReadOnlyFileSystem fileSystem, string filename, ISpriteLoader[] loaders, out TypeDictionary metadata)
{
metadata = null;
if (!fileSystem.TryOpen(filename, out var stream)) if (!fileSystem.TryOpen(filename, out var stream))
return null; return null;
using (stream) using (stream)
{ {
foreach (var loader in loaders) foreach (var loader in loaders)
if (loader.TryParseSprite(stream, filename, out var frames, out _)) if (loader.TryParseSprite(stream, filename, out var frames, out metadata))
return frames; return frames;
return null; return null;
} }
} }
public ISpriteFrame[] LoadFramesUncached(string filename)
{
return GetFrames(fileSystem, filename, loaders);
}
public void LoadReservations(ModData modData) public void LoadReservations(ModData modData)
{ {
var pendingResolve = new List<( foreach (var sb in SheetBuilders.Values)
string Filename, sb.Current.CreateBuffer();
int FrameIndex,
bool Premultiplied, var spriteCache = new Dictionary<int, Sprite>();
AdjustFrame AdjustFrame,
ISpriteFrame Frame,
Sprite[] SpritesForToken)>();
foreach (var (filename, tokens) in reservationsByFilename) foreach (var (filename, tokens) in reservationsByFilename)
{ {
modData.LoadScreen?.Display(); modData.LoadScreen?.Display();
var loadedFrames = GetFrames(fileSystem, filename, loaders); var loadedFrames = GetFrames(fileSystem, filename, loaders, out _);
foreach (var token in tokens) foreach (var token in tokens)
{ {
if (spriteReservations.TryGetValue(token, out var rs)) 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 (loadedFrames != null) if (loadedFrames != null)
{ {
var resolved = new Sprite[loadedFrames.Length]; var resolved = new Sprite[loadedFrames.Length];
resolvedSprites[token] = resolved; var frames = r.Frames ?? Enumerable.Range(0, loadedFrames.Length);
if (rs.Frames != null && rs.Frames.Any(i => i >= loadedFrames.Length))
throw new InvalidOperationException($"{rs.Location}: {filename} does not contain frames: " +
string.Join(',', rs.Frames.Where(f => f >= loadedFrames.Length)));
var frames = rs.Frames ?? Enumerable.Range(0, loadedFrames.Length);
var total = rs.Frames?.Length ?? loadedFrames.Length;
var j = 0;
foreach (var i in frames) foreach (var i in frames)
{ resolved[i] = spriteCache.GetOrAdd(i,
var frame = loadedFrames[i]; f => SheetBuilders[SheetBuilder.FrameTypeToSheetType(loadedFrames[f].Type)].Add(loadedFrames[f]));
if (rs.AdjustFrame != null)
frame = rs.AdjustFrame(frame, j++, total); resolvedSprites[token] = resolved;
pendingResolve.Add((filename, i, rs.Premultiplied, rs.AdjustFrame, frame, resolved));
}
} }
else else
{ {
resolvedSprites[token] = null; resolvedSprites[token] = null;
missingFiles[token] = (filename, rs.Location); missingFiles[token] = (filename, r.Location);
} }
} }
} }
spriteCache.Clear();
} }
spriteReservations.Clear(); spriteReservations.Clear();
spriteReservations.TrimExcess(); frameReservations.Clear();
reservationsByFilename.Clear(); reservationsByFilename.Clear();
reservationsByFilename.TrimExcess();
// When the sheet builder is adding sprites, it reserves height for the tallest sprite seen along the row.
// We can achieve better sheet packing by keeping sprites with similar heights together.
var orderedPendingResolve = pendingResolve.OrderBy(x => x.Frame.Size.Height);
var spriteCache = new Dictionary<(
string Filename,
int FrameIndex,
bool Premultiplied,
AdjustFrame AdjustFrame),
Sprite>(pendingResolve.Count);
foreach (var (filename, frameIndex, premultiplied, adjustFrame, frame, spritesForToken) in orderedPendingResolve)
{
// Premultiplied and non-premultiplied sprites must be cached separately
// to cover the case where the same image is requested in both versions.
spritesForToken[frameIndex] = spriteCache.GetOrAdd(
(filename, frameIndex, premultiplied, adjustFrame),
_ =>
{
var sheetBuilder = SheetBuilders[SheetBuilder.FrameTypeToSheetType(frame.Type)];
return sheetBuilder.Add(frame, premultiplied);
});
modData.LoadScreen?.Display();
}
foreach (var sb in SheetBuilders.Values) foreach (var sb in SheetBuilders.Values)
sb.Current.ReleaseBuffer(); sb.Current.ReleaseBuffer();
@@ -160,11 +145,18 @@ namespace OpenRA.Graphics
public Sprite[] ResolveSprites(int token) public Sprite[] ResolveSprites(int token)
{ {
if (!resolvedSprites.Remove(token, out var resolved)) var resolved = resolvedSprites[token];
throw new InvalidOperationException($"{nameof(token)} {token} has either already been resolved, or was never reserved via {nameof(ReserveSprites)}"); resolvedSprites.Remove(token);
if (missingFiles.TryGetValue(token, out var r))
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);
resolvedSprites.TrimExcess(); return resolved;
}
public ISpriteFrame[] ResolveFrames(int token)
{
var resolved = resolvedFrames[token];
resolvedFrames.Remove(token);
if (missingFiles.TryGetValue(token, out var r)) if (missingFiles.TryGetValue(token, out var r))
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename); throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);

View File

@@ -27,7 +27,7 @@ namespace OpenRA.Graphics
float deviceScale; float deviceScale;
public SpriteFont(IPlatform platform, string name, byte[] data, int size, int ascender, float scale, SheetBuilder builder) public SpriteFont(string name, byte[] data, int size, int ascender, float scale, SheetBuilder builder)
{ {
if (builder.Type != SheetType.BGRA) if (builder.Type != SheetType.BGRA)
throw new ArgumentException("The sheet builder must create BGRA sheets.", nameof(builder)); throw new ArgumentException("The sheet builder must create BGRA sheets.", nameof(builder));
@@ -36,7 +36,7 @@ namespace OpenRA.Graphics
this.size = size; this.size = size;
this.builder = builder; this.builder = builder;
font = platform.CreateFont(data); font = Game.Renderer.CreateFont(data);
glyphs = new Cache<char, GlyphInfo>(CreateGlyph); glyphs = new Cache<char, GlyphInfo>(CreateGlyph);
contrastGlyphs = new Cache<(char, int), Sprite>(CreateContrastGlyph); contrastGlyphs = new Cache<(char, int), Sprite>(CreateContrastGlyph);
dilationElements = new Cache<int, float[]>(CreateCircularWeightMap); dilationElements = new Cache<int, float[]>(CreateCircularWeightMap);

View File

@@ -22,30 +22,20 @@ namespace OpenRA.Graphics
/// </summary> /// </summary>
public enum SpriteFrameType public enum SpriteFrameType
{ {
/// <summary> // 8 bit index into an external palette
/// 8 bit index into an external palette.
/// </summary>
Indexed8, Indexed8,
/// <summary> // 32 bit color such as returned by Color.ToArgb() or the bmp file format
/// 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!)
/// (remember that little-endian systems place the little bits in the first byte).
/// </summary>
Bgra32, Bgra32,
/// <summary> // Like BGRA, but without an alpha channel
/// Like BGRA, but without an alpha channel.
/// </summary>
Bgr24, Bgr24,
/// <summary> // 32 bit color in big-endian format, like png
/// 32 bit color in big-endian format, like png.
/// </summary>
Rgba32, Rgba32,
/// <summary> // Like RGBA, but without an alpha channel
/// Like RGBA, but without an alpha channel.
/// </summary>
Rgb24 Rgb24
} }

View File

@@ -11,7 +11,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices;
using OpenRA.Primitives; using OpenRA.Primitives;
namespace OpenRA.Graphics namespace OpenRA.Graphics
@@ -20,7 +19,6 @@ namespace OpenRA.Graphics
{ {
public const int SheetCount = 8; public const int SheetCount = 8;
static readonly string[] SheetIndexToTextureName = Exts.MakeArray(SheetCount, i => $"Texture{i}"); static readonly string[] SheetIndexToTextureName = Exts.MakeArray(SheetCount, i => $"Texture{i}");
static readonly int UintSize = Marshal.SizeOf<uint>();
readonly Renderer renderer; readonly Renderer renderer;
readonly IShader shader; readonly IShader shader;
@@ -29,21 +27,21 @@ namespace OpenRA.Graphics
readonly Sheet[] sheets = new Sheet[SheetCount]; readonly Sheet[] sheets = new Sheet[SheetCount];
BlendMode currentBlend = BlendMode.Alpha; BlendMode currentBlend = BlendMode.Alpha;
int vertexCount = 0; int nv = 0;
int sheetCount = 0; int ns = 0;
public SpriteRenderer(Renderer renderer, IShader shader) public SpriteRenderer(Renderer renderer, IShader shader)
{ {
this.renderer = renderer; this.renderer = renderer;
this.shader = shader; this.shader = shader;
vertices = renderer.Context.CreateVertices<Vertex>(renderer.TempVertexBufferSize); vertices = renderer.Context.CreateVertices(renderer.TempBufferSize);
} }
public void Flush() public void Flush()
{ {
if (vertexCount > 0) if (nv > 0)
{ {
for (var i = 0; i < sheetCount; i++) for (var i = 0; i < ns; i++)
{ {
shader.SetTexture(SheetIndexToTextureName[i], sheets[i].GetTexture()); shader.SetTexture(SheetIndexToTextureName[i], sheets[i].GetTexture());
sheets[i] = null; sheets[i] = null;
@@ -52,11 +50,12 @@ namespace OpenRA.Graphics
renderer.Context.SetBlendMode(currentBlend); renderer.Context.SetBlendMode(currentBlend);
shader.PrepareRender(); shader.PrepareRender();
renderer.DrawQuadBatch(ref vertices, shader, vertexCount); // PERF: The renderer may choose to replace vertices with a different temporary buffer.
renderer.DrawBatch(ref vertices, nv, PrimitiveType.TriangleList);
renderer.Context.SetBlendMode(BlendMode.None); renderer.Context.SetBlendMode(BlendMode.None);
vertexCount = 0; nv = 0;
sheetCount = 0; ns = 0;
} }
} }
@@ -64,7 +63,7 @@ namespace OpenRA.Graphics
{ {
renderer.CurrentBatchRenderer = this; renderer.CurrentBatchRenderer = this;
if (s.BlendMode != currentBlend || vertexCount + 4 > renderer.TempVertexBufferSize) if (s.BlendMode != currentBlend || nv + 6 > renderer.TempBufferSize)
Flush(); Flush();
currentBlend = s.BlendMode; currentBlend = s.BlendMode;
@@ -72,7 +71,7 @@ namespace OpenRA.Graphics
// Check if the sheet (or secondary data sheet) have already been mapped // Check if the sheet (or secondary data sheet) have already been mapped
var sheet = s.Sheet; var sheet = s.Sheet;
var sheetIndex = 0; var sheetIndex = 0;
for (; sheetIndex < sheetCount; sheetIndex++) for (; sheetIndex < ns; sheetIndex++)
if (sheets[sheetIndex] == sheet) if (sheets[sheetIndex] == sheet)
break; break;
@@ -81,7 +80,7 @@ namespace OpenRA.Graphics
if (ss != null) if (ss != null)
{ {
var secondarySheet = ss.SecondarySheet; var secondarySheet = ss.SecondarySheet;
for (; secondarySheetIndex < sheetCount; secondarySheetIndex++) for (; secondarySheetIndex < ns; secondarySheetIndex++)
if (sheets[secondarySheetIndex] == secondarySheet) if (sheets[secondarySheetIndex] == secondarySheet)
break; break;
@@ -100,22 +99,22 @@ namespace OpenRA.Graphics
secondarySheetIndex = ss != null && ss.SecondarySheet != sheet ? 1 : 0; secondarySheetIndex = ss != null && ss.SecondarySheet != sheet ? 1 : 0;
} }
if (sheetIndex >= sheetCount) if (sheetIndex >= ns)
{ {
sheets[sheetIndex] = sheet; sheets[sheetIndex] = sheet;
sheetCount++; ns++;
} }
if (secondarySheetIndex >= sheetCount && ss != null) if (secondarySheetIndex >= ns && ss != null)
{ {
sheets[secondarySheetIndex] = ss.SecondarySheet; sheets[secondarySheetIndex] = ss.SecondarySheet;
sheetCount++; ns++;
} }
return new int2(sheetIndex, secondarySheetIndex); return new int2(sheetIndex, secondarySheetIndex);
} }
static int ResolveTextureIndex(Sprite s, PaletteReference pal) static float ResolveTextureIndex(Sprite s, PaletteReference pal)
{ {
if (pal == null) if (pal == null)
return 0; return 0;
@@ -129,20 +128,20 @@ namespace OpenRA.Graphics
return pal.TextureIndex; return pal.TextureIndex;
} }
internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 location, in float3 scale, float rotation = 0f) internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, in float3 scale, float rotation = 0f)
{ {
var samplers = SetRenderStateForSprite(s); var samplers = SetRenderStateForSprite(s);
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, vertexCount, scale * s.Size, float3.Ones, Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, float3.Ones,
1f, rotation); 1f, rotation);
vertexCount += 4; nv += 6;
} }
internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 location, float scale, float rotation = 0f) internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, float scale, float rotation = 0f)
{ {
var samplers = SetRenderStateForSprite(s); var samplers = SetRenderStateForSprite(s);
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, vertexCount, scale * s.Size, float3.Ones, Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, float3.Ones,
1f, rotation); 1f, rotation);
vertexCount += 4; nv += 6;
} }
public void DrawSprite(Sprite s, PaletteReference pal, in float3 location, float scale = 1f, float rotation = 0f) public void DrawSprite(Sprite s, PaletteReference pal, in float3 location, float scale = 1f, float rotation = 0f)
@@ -150,13 +149,13 @@ namespace OpenRA.Graphics
DrawSprite(s, ResolveTextureIndex(s, pal), location, scale, rotation); DrawSprite(s, ResolveTextureIndex(s, pal), location, scale, rotation);
} }
internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 location, float scale, in float3 tint, float alpha, internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 location, float scale, in float3 tint, float alpha,
float rotation = 0f) float rotation = 0f)
{ {
var samplers = SetRenderStateForSprite(s); var samplers = SetRenderStateForSprite(s);
Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, vertexCount, scale * s.Size, tint, alpha, Util.FastCreateQuad(vertices, location + scale * s.Offset, s, samplers, paletteTextureIndex, nv, scale * s.Size, tint, alpha,
rotation); rotation);
vertexCount += 4; nv += 6;
} }
public void DrawSprite(Sprite s, PaletteReference pal, in float3 location, float scale, in float3 tint, float alpha, public void DrawSprite(Sprite s, PaletteReference pal, in float3 location, float scale, in float3 tint, float alpha,
@@ -165,14 +164,14 @@ namespace OpenRA.Graphics
DrawSprite(s, ResolveTextureIndex(s, pal), location, scale, tint, alpha, rotation); DrawSprite(s, ResolveTextureIndex(s, pal), location, scale, tint, alpha, rotation);
} }
internal void DrawSprite(Sprite s, int paletteTextureIndex, in float3 a, in float3 b, in float3 c, in float3 d, in float3 tint, float alpha) internal void DrawSprite(Sprite s, float paletteTextureIndex, in float3 a, in float3 b, in float3 c, in float3 d, in float3 tint, float alpha)
{ {
var samplers = SetRenderStateForSprite(s); var samplers = SetRenderStateForSprite(s);
Util.FastCreateQuad(vertices, a, b, c, d, s, samplers, paletteTextureIndex, tint, alpha, vertexCount); Util.FastCreateQuad(vertices, a, b, c, d, s, samplers, paletteTextureIndex, tint, alpha, nv);
vertexCount += 4; nv += 6;
} }
public void DrawVertexBuffer(IVertexBuffer<Vertex> buffer, IIndexBuffer indices, int start, int length, IEnumerable<Sheet> sheets, BlendMode blendMode) public void DrawVertexBuffer(IVertexBuffer<Vertex> buffer, int start, int length, PrimitiveType type, IEnumerable<Sheet> sheets, BlendMode blendMode)
{ {
var i = 0; var i = 0;
foreach (var s in sheets) foreach (var s in sheets)
@@ -186,7 +185,7 @@ namespace OpenRA.Graphics
renderer.Context.SetBlendMode(blendMode); renderer.Context.SetBlendMode(blendMode);
shader.PrepareRender(); shader.PrepareRender();
renderer.DrawQuadBatch(buffer, indices, shader, length, UintSize * start); renderer.DrawBatch(buffer, start, length, type);
renderer.Context.SetBlendMode(BlendMode.None); renderer.Context.SetBlendMode(BlendMode.None);
} }
@@ -197,32 +196,29 @@ namespace OpenRA.Graphics
} }
// For RGBAColorRenderer // For RGBAColorRenderer
internal void DrawRGBAQuad(Vertex[] v, BlendMode blendMode) internal void DrawRGBAVertices(Vertex[] v, BlendMode blendMode)
{ {
renderer.CurrentBatchRenderer = this; renderer.CurrentBatchRenderer = this;
if (currentBlend != blendMode || vertexCount + 4 > renderer.TempVertexBufferSize) if (currentBlend != blendMode || nv + v.Length > renderer.TempBufferSize)
Flush(); Flush();
currentBlend = blendMode; currentBlend = blendMode;
Array.Copy(v, 0, vertices, nv, v.Length);
Array.Copy(v, 0, vertices, vertexCount, v.Length); nv += v.Length;
vertexCount += 4;
} }
public void SetPalette(HardwarePalette palette) public void SetPalette(ITexture palette, ITexture colorShifts)
{ {
shader.SetTexture("Palette", palette.Texture); shader.SetTexture("Palette", palette);
shader.SetTexture("ColorShifts", palette.ColorShifts); shader.SetTexture("ColorShifts", colorShifts);
shader.SetVec("PaletteRows", palette.Height);
} }
public void SetViewportParams(Size sheetSize, int downscale, float depthMargin, int2 scroll) public void SetViewportParams(Size sheetSize, int downscale, float depthMargin, int2 scroll)
{ {
// OpenGL only renders x and y coordinates inside [-1, 1] range. We project world coordinates // Calculate the scale (r1) and offset (r2) that convert from OpenRA viewport pixels
// using p1 to values [0, 2] and then subtract by 1 using p2, where p stands for projection. It's // to OpenGL normalized device coordinates (NDC). OpenGL expects coordinates to vary from [-1, 1],
// standard practice for shaders to use a projection matrix, but as we project orthographically // so we rescale viewport pixels to the range [0, 2] using r1 then subtract 1 using r2.
// we are able to send less data to the GPU.
var width = 2f / (downscale * sheetSize.Width); var width = 2f / (downscale * sheetSize.Width);
var height = 2f / (downscale * sheetSize.Height); var height = 2f / (downscale * sheetSize.Height);
@@ -243,8 +239,8 @@ namespace OpenRA.Graphics
var depth = depthMargin != 0f ? 2f / (downscale * (sheetSize.Height + depthMargin)) : 0; var depth = depthMargin != 0f ? 2f / (downscale * (sheetSize.Height + depthMargin)) : 0;
shader.SetVec("DepthTextureScale", 128 * depth); shader.SetVec("DepthTextureScale", 128 * depth);
shader.SetVec("Scroll", scroll.X, scroll.Y, depthMargin != 0f ? scroll.Y : 0); shader.SetVec("Scroll", scroll.X, scroll.Y, depthMargin != 0f ? scroll.Y : 0);
shader.SetVec("p1", width, height, -depth); shader.SetVec("r1", width, height, -depth);
shader.SetVec("p2", -1, -1, depthMargin != 0f ? 1 : 0); shader.SetVec("r2", -1, -1, depthMargin != 0f ? 1 : 0);
} }
public void SetDepthPreview(bool enabled, float contrast, float offset) public void SetDepthPreview(bool enabled, float contrast, float offset)
@@ -253,9 +249,9 @@ namespace OpenRA.Graphics
shader.SetVec("DepthPreviewParams", contrast, offset); shader.SetVec("DepthPreviewParams", contrast, offset);
} }
public void EnablePixelArtScaling(bool enabled) public void SetAntialiasingPixelsPerTexel(float pxPerTx)
{ {
shader.SetBool("EnablePixelArtScaling", enabled); shader.SetVec("AntialiasPixelsPerTexel", pxPerTx);
} }
} }
} }

View File

@@ -12,15 +12,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
namespace OpenRA.Graphics namespace OpenRA.Graphics
{ {
public sealed class TerrainSpriteLayer : IDisposable public sealed class TerrainSpriteLayer : IDisposable
{ {
// PERF: we can reuse the IndexBuffer as all layers have the same size. static readonly int[] CornerVertexMap = { 0, 1, 2, 2, 3, 0 };
static readonly ConditionalWeakTable<World, IndexBufferRc> IndexBuffers = new();
readonly IndexBufferRc indexBufferWrapper;
public readonly BlendMode BlendMode; public readonly BlendMode BlendMode;
@@ -31,8 +28,7 @@ namespace OpenRA.Graphics
readonly Vertex[] vertices; readonly Vertex[] vertices;
readonly bool[] ignoreTint; readonly bool[] ignoreTint;
readonly HashSet<int> dirtyRows = new(); readonly HashSet<int> dirtyRows = new();
readonly int indexRowStride; readonly int rowStride;
readonly int vertexRowStride;
readonly bool restrictToBounds; readonly bool restrictToBounds;
readonly WorldRenderer worldRenderer; readonly WorldRenderer worldRenderer;
@@ -47,25 +43,19 @@ namespace OpenRA.Graphics
this.emptySprite = emptySprite; this.emptySprite = emptySprite;
sheets = new Sheet[SpriteRenderer.SheetCount]; sheets = new Sheet[SpriteRenderer.SheetCount];
BlendMode = blendMode; BlendMode = blendMode;
map = world.Map; map = world.Map;
rowStride = 6 * map.MapSize.X;
vertexRowStride = 4 * map.MapSize.X; vertices = new Vertex[rowStride * map.MapSize.Y];
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]; palettes = new PaletteReference[map.MapSize.X * map.MapSize.Y];
vertexBuffer = Game.Renderer.Context.CreateVertexBuffer(vertices.Length);
wr.PaletteInvalidated += UpdatePaletteIndices; wr.PaletteInvalidated += UpdatePaletteIndices;
if (wr.TerrainLighting != null) if (wr.TerrainLighting != null)
{ {
ignoreTint = new bool[vertexRowStride * map.MapSize.Y]; ignoreTint = new bool[rowStride * map.MapSize.Y];
wr.TerrainLighting.CellChanged += UpdateTint; wr.TerrainLighting.CellChanged += UpdateTint;
} }
} }
@@ -75,9 +65,8 @@ namespace OpenRA.Graphics
for (var i = 0; i < vertices.Length; i++) for (var i = 0; i < vertices.Length; i++)
{ {
var v = vertices[i]; var v = vertices[i];
var p = palettes[i / 4]?.TextureIndex ?? 0; var p = palettes[i / 6]?.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, p, v.C, v.R, v.G, v.B, v.A);
vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, c, v.R, v.G, v.B, v.A);
} }
for (var row = 0; row < map.MapSize.Y; row++) for (var row = 0; row < map.MapSize.Y; row++)
@@ -108,13 +97,13 @@ namespace OpenRA.Graphics
void UpdateTint(MPos uv) void UpdateTint(MPos uv)
{ {
var offset = vertexRowStride * uv.V + 4 * uv.U; var offset = rowStride * uv.V + 6 * uv.U;
if (ignoreTint[offset]) if (ignoreTint[offset])
{ {
for (var i = 0; i < 4; i++) for (var i = 0; i < 6; i++)
{ {
var v = vertices[offset + 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.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.P, v.C, v.A * float3.Ones, v.A);
} }
return; return;
@@ -136,10 +125,10 @@ namespace OpenRA.Graphics
// Apply tint directly to the underlying vertices // Apply tint directly to the underlying vertices
// This saves us from having to re-query the sprite information, which has not changed // This saves us from having to re-query the sprite information, which has not changed
for (var i = 0; i < 4; i++) for (var i = 0; i < 6; i++)
{ {
var v = vertices[offset + 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.C, v.A * weights[i], v.A); 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);
} }
dirtyRows.Add(uv.V); dirtyRows.Add(uv.V);
@@ -191,7 +180,7 @@ namespace OpenRA.Graphics
if (!map.Tiles.Contains(uv)) if (!map.Tiles.Contains(uv))
return; return;
var offset = vertexRowStride * uv.V + 4 * uv.U; var offset = rowStride * uv.V + 6 * uv.U;
Util.FastCreateQuad(vertices, pos, sprite, samplers, palette?.TextureIndex ?? 0, offset, scale * sprite.Size, alpha * float3.Ones, alpha); Util.FastCreateQuad(vertices, pos, sprite, samplers, palette?.TextureIndex ?? 0, offset, scale * sprite.Size, alpha * float3.Ones, alpha);
palettes[uv.V * map.MapSize.X + uv.U] = palette; palettes[uv.V * map.MapSize.X + uv.U] = palette;
@@ -220,13 +209,13 @@ namespace OpenRA.Graphics
if (!dirtyRows.Remove(row)) if (!dirtyRows.Remove(row))
continue; continue;
var rowOffset = vertexRowStride * row; var rowOffset = rowStride * row;
vertexBuffer.SetData(vertices, rowOffset, rowOffset, vertexRowStride); vertexBuffer.SetData(vertices, rowOffset, rowOffset, rowStride);
} }
Game.Renderer.WorldSpriteRenderer.DrawVertexBuffer( Game.Renderer.WorldSpriteRenderer.DrawVertexBuffer(
vertexBuffer, indexBufferWrapper.Buffer, indexRowStride * firstRow, vertexBuffer, rowStride * firstRow, rowStride * (lastRow - firstRow),
indexRowStride * (lastRow - firstRow), sheets, BlendMode); PrimitiveType.TriangleList, sheets, BlendMode);
Game.Renderer.Flush(); Game.Renderer.Flush();
} }
@@ -238,29 +227,6 @@ namespace OpenRA.Graphics
worldRenderer.TerrainLighting.CellChanged -= UpdateTint; worldRenderer.TerrainLighting.CellChanged -= UpdateTint;
vertexBuffer.Dispose(); 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();
}
} }
} }
} }

View File

@@ -21,8 +21,7 @@ namespace OpenRA.Graphics
readonly float alpha; readonly float alpha;
readonly float rotation = 0f; readonly float rotation = 0f;
public UISpriteRenderable(Sprite sprite, WPos effectiveWorldPos, int2 screenPos, int zOffset, PaletteReference palette, public UISpriteRenderable(Sprite sprite, WPos effectiveWorldPos, int2 screenPos, int zOffset, PaletteReference palette, float scale = 1f, float alpha = 1f, float rotation = 0f)
float scale = 1f, float alpha = 1f, float rotation = 0f)
{ {
this.sprite = sprite; this.sprite = sprite;
Pos = effectiveWorldPos; Pos = effectiveWorldPos;
@@ -48,8 +47,7 @@ namespace OpenRA.Graphics
public PaletteReference Palette { get; } public PaletteReference Palette { get; }
public int ZOffset { get; } public int ZOffset { get; }
public IPalettedRenderable WithPalette(PaletteReference newPalette) => public IPalettedRenderable WithPalette(PaletteReference newPalette) { return new UISpriteRenderable(sprite, Pos, screenPos, ZOffset, newPalette, scale, alpha, rotation); }
new UISpriteRenderable(sprite, Pos, screenPos, ZOffset, newPalette, scale, alpha, rotation);
public IRenderable WithZOffset(int newOffset) { return this; } public IRenderable WithZOffset(int newOffset) { return this; }
public IRenderable OffsetBy(in WVec vec) { return this; } public IRenderable OffsetBy(in WVec vec) { return this; }
public IRenderable AsDecoration() { return this; } public IRenderable AsDecoration() { return this; }

View File

@@ -10,7 +10,6 @@
#endregion #endregion
using System; using System;
using System.Runtime.InteropServices;
using OpenRA.FileFormats; using OpenRA.FileFormats;
using OpenRA.Primitives; using OpenRA.Primitives;
@@ -21,17 +20,7 @@ namespace OpenRA.Graphics
// yes, our channel order is nuts. // yes, our channel order is nuts.
static readonly int[] ChannelMasks = { 2, 1, 0, 3 }; static readonly int[] ChannelMasks = { 2, 1, 0, 3 };
public static uint[] CreateQuadIndices(int quads) public static void FastCreateQuad(Vertex[] vertices, in float3 o, Sprite r, int2 samplers, float paletteTextureIndex, int nv,
{
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) in float3 size, in float3 tint, float alpha, float rotation = 0f)
{ {
float3 a, b, c, d; float3 a, b, c, d;
@@ -73,7 +62,7 @@ namespace OpenRA.Graphics
public static void FastCreateQuad(Vertex[] vertices, public static void FastCreateQuad(Vertex[] vertices,
in float3 a, in float3 b, in float3 c, in float3 d, in float3 a, in float3 b, in float3 c, in float3 d,
Sprite r, int2 samplers, int paletteTextureIndex, Sprite r, int2 samplers, float paletteTextureIndex,
in float3 tint, float alpha, int nv) in float3 tint, float alpha, int nv)
{ {
float sl = 0; float sl = 0;
@@ -95,33 +84,76 @@ namespace OpenRA.Graphics
attribC |= samplers.Y << 9; attribC |= samplers.Y << 9;
} }
attribC |= (paletteTextureIndex & 0xFFFF) << 16; var fAttribC = (float)attribC;
vertices[nv] = new Vertex(a, r.Left, r.Top, sl, st, paletteTextureIndex, fAttribC, tint, alpha);
var uAttribC = (uint)attribC; vertices[nv + 1] = new Vertex(b, r.Right, r.Top, sr, st, paletteTextureIndex, fAttribC, tint, alpha);
vertices[nv] = new Vertex(a, r.Left, r.Top, sl, st, uAttribC, tint, alpha); vertices[nv + 2] = new Vertex(c, r.Right, r.Bottom, sr, sb, paletteTextureIndex, fAttribC, tint, alpha);
vertices[nv + 1] = new Vertex(b, r.Right, r.Top, sr, st, uAttribC, tint, alpha); vertices[nv + 3] = new Vertex(c, r.Right, r.Bottom, sr, sb, paletteTextureIndex, fAttribC, tint, alpha);
vertices[nv + 2] = new Vertex(c, r.Right, r.Bottom, sr, sb, uAttribC, tint, alpha); vertices[nv + 4] = new Vertex(d, r.Left, r.Bottom, sl, sb, paletteTextureIndex, fAttribC, tint, alpha);
vertices[nv + 3] = new Vertex(d, r.Left, r.Bottom, sl, sb, uAttribC, tint, alpha); vertices[nv + 5] = new Vertex(a, r.Left, r.Top, sl, st, paletteTextureIndex, fAttribC, tint, alpha);
} }
public static void FastCopyIntoChannel(Sprite dest, byte[] src, SpriteFrameType srcType, bool premultiplied = false) public static void FastCopyIntoChannel(Sprite dest, byte[] src, SpriteFrameType srcType)
{ {
var destData = dest.Sheet.GetData(); 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 width = dest.Bounds.Width;
var height = dest.Bounds.Height; var height = dest.Bounds.Height;
if (dest.Channel == TextureChannel.RGBA) if (dest.Channel == TextureChannel.RGBA)
{ {
CopyIntoRgba(src, srcType, premultiplied, destData, x, y, width, height, stride); 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();
}
}
}
}
} }
else else
{ {
// Copy into single channel of destination. var destStride = dest.Sheet.Size.Width * 4;
var destStride = stride * 4; var destOffset = destStride * dest.Bounds.Top + dest.Bounds.Left * 4 + ChannelMasks[(int)dest.Channel];
var destOffset = destStride * y + x * 4 + ChannelMasks[(int)dest.Channel];
var destSkip = destStride - 4 * width; var destSkip = destStride - 4 * width;
var srcOffset = 0; var srcOffset = 0;
@@ -138,119 +170,56 @@ 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) public static void FastCopyIntoSprite(Sprite dest, Png src)
{ {
var destData = dest.Sheet.GetData(); var destData = dest.Sheet.GetData();
var stride = dest.Sheet.Size.Width; var destStride = dest.Sheet.Size.Width;
var x = dest.Bounds.Left;
var y = dest.Bounds.Top;
var width = dest.Bounds.Width; var width = dest.Bounds.Width;
var height = dest.Bounds.Height; var height = dest.Bounds.Height;
var si = 0; unsafe
var di = y * stride + x; {
var d = MemoryMarshal.Cast<byte, uint>(destData); // 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;
for (var h = 0; h < height; h++) var k = 0;
for (var j = 0; j < height; j++)
{ {
for (var w = 0; w < width; w++) for (var i = 0; i < width; i++)
{ {
Color c; Color cc;
switch (src.Type) switch (src.Type)
{ {
case SpriteFrameType.Indexed8: case SpriteFrameType.Indexed8:
c = src.Palette[src.Data[si++]]; {
cc = src.Palette[src.Data[k++]];
break; break;
}
case SpriteFrameType.Rgba32: case SpriteFrameType.Rgba32:
case SpriteFrameType.Rgb24: case SpriteFrameType.Rgb24:
var r = src.Data[si++]; {
var g = src.Data[si++]; var r = src.Data[k++];
var b = src.Data[si++]; var g = src.Data[k++];
var a = src.Type == SpriteFrameType.Rgba32 ? src.Data[si++] : byte.MaxValue; var b = src.Data[k++];
c = Color.FromArgb(a, r, g, b); var a = src.Type == SpriteFrameType.Rgba32 ? src.Data[k++] : (byte)255;
cc = Color.FromArgb(a, r, g, b);
break; break;
}
// PNGs don't support BGR[A], so no need to include them here // Pngs don't support BGR[A], so no need to include them here
default: default:
throw new InvalidOperationException($"Unknown SpriteFrameType {src.Type}"); throw new InvalidOperationException($"Unknown SpriteFrameType {src.Type}");
} }
d[di++] = PremultiplyAlpha(c).ToArgb(); data[(y + j) * destStride + x + i] = PremultiplyAlpha(cc).ToArgb();
}
}
} }
di += stride - width;
} }
} }
@@ -336,5 +305,239 @@ namespace OpenRA.Graphics
(int)((byte)(t * a2 * c2.G + 0.5f) + (1 - t) * (byte)(a1 * c1.G + 0.5f)), (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)))); (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;
}
} }
} }

View File

@@ -23,42 +23,27 @@ namespace OpenRA.Graphics
public readonly float S, T, U, V; public readonly float S, T, U, V;
// Palette and channel flags // Palette and channel flags
public readonly uint C; public readonly float P, C;
// Color tint // Color tint
public readonly float R, G, B, A; public readonly float R, G, B, A;
public Vertex(in float3 xyz, float s, float t, float u, float v, uint c) 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, c, float3.Ones, 1f) { } : 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, in float3 tint, float a) 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, c, tint.X, tint.Y, tint.Z, a) { } : this(xyz.X, xyz.Y, xyz.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) 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, c, tint.X, tint.Y, tint.Z, 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, float r, float g, float b, float 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)
{ {
X = x; Y = y; Z = z; X = x; Y = y; Z = z;
S = s; T = t; S = s; T = t;
U = u; V = v; U = u; V = v;
C = c; P = p; C = c;
R = r; G = g; B = b; A = a; 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)
};
}
} }

View File

@@ -215,9 +215,7 @@ namespace OpenRA.Graphics
MinZoom = CalculateMinimumZoom(range.X, range.Y) * viewportSizes.DefaultScale; MinZoom = CalculateMinimumZoom(range.X, range.Y) * viewportSizes.DefaultScale;
} }
MaxZoom = Math.Min( MaxZoom = Math.Min(MinZoom * viewportSizes.MaxZoomScale, Game.Renderer.NativeResolution.Height * viewportSizes.DefaultScale / viewportSizes.MaxZoomWindowHeight);
MinZoom * viewportSizes.MaxZoomScale,
Game.Renderer.NativeResolution.Height * viewportSizes.DefaultScale / viewportSizes.MaxZoomWindowHeight);
if (unlockMinZoom) if (unlockMinZoom)
{ {
@@ -233,12 +231,11 @@ namespace OpenRA.Graphics
else else
Zoom = Zoom.Clamp(MinZoom, MaxZoom); Zoom = Zoom.Clamp(MinZoom, MaxZoom);
var minZoom = unlockMinZoom ? unlockedMinZoom : MinZoom; var maxSize = 1f / (unlockMinZoom ? unlockedMinZoom : MinZoom) * new float2(Game.Renderer.NativeResolution);
var maxSize = 1f / minZoom * new float2(Game.Renderer.NativeResolution);
Game.Renderer.SetMaximumViewportSize(new Size((int)maxSize.X, (int)maxSize.Y)); Game.Renderer.SetMaximumViewportSize(new Size((int)maxSize.X, (int)maxSize.Y));
foreach (var t in worldRenderer.World.WorldActor.TraitsImplementing<INotifyViewportZoomExtentsChanged>()) foreach (var t in worldRenderer.World.WorldActor.TraitsImplementing<INotifyViewportZoomExtentsChanged>())
t.ViewportZoomExtentsChanged(minZoom, MaxZoom); t.ViewportZoomExtentsChanged(MinZoom, MaxZoom);
} }
public CPos ViewToWorld(int2 view) public CPos ViewToWorld(int2 view)
@@ -285,23 +282,11 @@ namespace OpenRA.Graphics
IEnumerable<MPos> CandidateMouseoverCells(int2 world) IEnumerable<MPos> CandidateMouseoverCells(int2 world)
{ {
var map = worldRenderer.World.Map; var map = worldRenderer.World.Map;
var tileScale = map.Grid.TileScale / 2;
var minPos = worldRenderer.ProjectedPosition(world); var minPos = worldRenderer.ProjectedPosition(world);
// Find all the cells that could potentially have been clicked. // Find all the cells that could potentially have been clicked
MPos a; var a = map.CellContaining(minPos - new WVec(1024, 0, 0)).ToMPos(map.Grid.Type);
MPos b; var b = map.CellContaining(minPos + new WVec(512, 512 * map.Grid.MaximumTerrainHeight, 0)).ToMPos(map.Grid.Type);
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 v = b.V; v >= a.V; v--)
for (var u = b.U; u >= a.U; u--) for (var u = b.U; u >= a.U; u--)
@@ -314,13 +299,10 @@ namespace OpenRA.Graphics
public void Center(IEnumerable<Actor> actors) public void Center(IEnumerable<Actor> actors)
{ {
var actorsCollection = actors as IReadOnlyCollection<Actor>; if (!actors.Any())
actorsCollection ??= actors.ToList();
if (actorsCollection.Count == 0)
return; return;
Center(actorsCollection.Select(a => a.CenterPosition).Average()); Center(actors.Select(a => a.CenterPosition).Average());
} }
public void Center(WPos pos) public void Center(WPos pos)

View File

@@ -44,8 +44,6 @@ namespace OpenRA.Graphics
readonly List<IFinalizedRenderable> preparedAnnotationRenderables = new(); readonly List<IFinalizedRenderable> preparedAnnotationRenderables = new();
readonly List<IRenderable> renderablesBuffer = new(); readonly List<IRenderable> renderablesBuffer = new();
readonly IRenderer[] renderers;
readonly IRenderPostProcessPass[] postProcessPasses;
internal WorldRenderer(ModData modData, World world) internal WorldRenderer(ModData modData, World world)
{ {
@@ -62,29 +60,15 @@ namespace OpenRA.Graphics
foreach (var pal in world.TraitDict.ActorsWithTrait<ILoadsPalettes>()) foreach (var pal in world.TraitDict.ActorsWithTrait<ILoadsPalettes>())
pal.Trait.LoadPalettes(this); pal.Trait.LoadPalettes(this);
Player.SetupRelationshipColors(world.Players, world.LocalPlayer, this, true); foreach (var p in world.Players)
UpdatePalettesForPlayer(p.InternalName, p.Color, false);
palette.Initialize(); palette.Initialize();
TerrainLighting = world.WorldActor.TraitOrDefault<ITerrainLighting>(); TerrainLighting = world.WorldActor.TraitOrDefault<ITerrainLighting>();
renderers = world.WorldActor.TraitsImplementing<IRenderer>().ToArray();
terrainRenderer = world.WorldActor.TraitOrDefault<IRenderTerrain>(); terrainRenderer = world.WorldActor.TraitOrDefault<IRenderTerrain>();
debugVis = Exts.Lazy(() => world.WorldActor.TraitOrDefault<DebugVisualizations>()); debugVis = Exts.Lazy(() => world.WorldActor.TraitOrDefault<DebugVisualizations>());
postProcessPasses = world.WorldActor.TraitsImplementing<IRenderPostProcessPass>().ToArray();
}
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) public void UpdatePalettesForPlayer(string internalName, Color color, bool replaceExisting)
@@ -286,8 +270,6 @@ namespace OpenRA.Graphics
if (enableDepthBuffer) if (enableDepthBuffer)
Game.Renderer.ClearDepthBuffer(); Game.Renderer.ClearDepthBuffer();
ApplyPostProcessing(PostProcessPassType.AfterActors);
World.ApplyToActorsWithTrait<IRenderAboveWorld>((actor, trait) => World.ApplyToActorsWithTrait<IRenderAboveWorld>((actor, trait) =>
{ {
if (actor.IsInWorld && !actor.Disposed) if (actor.IsInWorld && !actor.Disposed)
@@ -297,8 +279,6 @@ namespace OpenRA.Graphics
if (enableDepthBuffer) if (enableDepthBuffer)
Game.Renderer.ClearDepthBuffer(); Game.Renderer.ClearDepthBuffer();
ApplyPostProcessing(PostProcessPassType.AfterWorld);
World.ApplyToActorsWithTrait<IRenderShroud>((actor, trait) => trait.RenderShroud(this)); World.ApplyToActorsWithTrait<IRenderShroud>((actor, trait) => trait.RenderShroud(this));
if (enableDepthBuffer) if (enableDepthBuffer)
@@ -312,23 +292,9 @@ namespace OpenRA.Graphics
foreach (var r in g) foreach (var r in g)
r.Render(this); r.Render(this);
ApplyPostProcessing(PostProcessPassType.AfterShroud);
Game.Renderer.Flush(); 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() public void DrawAnnotations()
{ {
Game.Renderer.EnableAntialiasingFilter(); Game.Renderer.EnableAntialiasingFilter();

View File

@@ -16,19 +16,11 @@ namespace OpenRA
{ {
public sealed class HotkeyDefinition public sealed class HotkeyDefinition
{ {
public const string ContextFluentPrefix = "hotkey-context";
public readonly string Name; public readonly string Name;
public readonly Hotkey Default = Hotkey.Invalid; public readonly Hotkey Default = Hotkey.Invalid;
[FluentReference]
public readonly string Description = ""; public readonly string Description = "";
public readonly HashSet<string> Types = new(); public readonly HashSet<string> Types = new();
[FluentReference]
public readonly HashSet<string> Contexts = new(); public readonly HashSet<string> Contexts = new();
public readonly bool Readonly = false; public readonly bool Readonly = false;
public bool HasDuplicates { get; internal set; } public bool HasDuplicates { get; internal set; }
@@ -39,27 +31,29 @@ namespace OpenRA
if (!string.IsNullOrEmpty(node.Value)) if (!string.IsNullOrEmpty(node.Value))
Default = FieldLoader.GetValue<Hotkey>("value", node.Value); Default = FieldLoader.GetValue<Hotkey>("value", node.Value);
var nodeDict = node.ToDictionary(); var descriptionNode = node.Nodes.FirstOrDefault(n => n.Key == "Description");
if (descriptionNode != null)
Description = descriptionNode.Value.Value;
if (nodeDict.TryGetValue("Description", out var descriptionYaml)) var typesNode = node.Nodes.FirstOrDefault(n => n.Key == "Types");
Description = descriptionYaml.Value; if (typesNode != null)
Types = FieldLoader.GetValue<HashSet<string>>("Types", typesNode.Value.Value);
if (nodeDict.TryGetValue("Types", out var typesYaml)) var contextsNode = node.Nodes.FirstOrDefault(n => n.Key == "Contexts");
Types = FieldLoader.GetValue<HashSet<string>>("Types", typesYaml.Value); if (contextsNode != null)
Contexts = FieldLoader.GetValue<HashSet<string>>("Contexts", contextsNode.Value.Value);
if (nodeDict.TryGetValue("Contexts", out var contextYaml)) var platformNode = node.Nodes.FirstOrDefault(n => n.Key == "Platform");
Contexts = FieldLoader.GetValue<HashSet<string>>("Contexts", contextYaml.Value) if (platformNode != null)
.Select(c => ContextFluentPrefix + "." + c).ToHashSet();
if (nodeDict.TryGetValue("Platform", out var platformYaml))
{ {
var platformOverride = platformYaml.NodeWithKeyOrDefault(Platform.CurrentPlatform.ToString()); var platformOverride = platformNode.Value.Nodes.FirstOrDefault(n => n.Key == Platform.CurrentPlatform.ToString());
if (platformOverride != null) if (platformOverride != null)
Default = FieldLoader.GetValue<Hotkey>("value", platformOverride.Value.Value); Default = FieldLoader.GetValue<Hotkey>("value", platformOverride.Value.Value);
} }
if (nodeDict.TryGetValue("Readonly", out var readonlyYaml)) var readonlyNode = node.Nodes.FirstOrDefault(n => n.Key == "Readonly");
Readonly = FieldLoader.GetValue<bool>("Readonly", readonlyYaml.Value); if (readonlyNode != null)
Readonly = FieldLoader.GetValue<bool>("Readonly", readonlyNode.Value.Value);
} }
} }
} }

View File

@@ -91,16 +91,16 @@ namespace OpenRA
var ret = KeycodeExts.DisplayString(Key); var ret = KeycodeExts.DisplayString(Key);
if (Modifiers.HasModifier(Modifiers.Shift)) if (Modifiers.HasModifier(Modifiers.Shift))
ret = $"{ModifiersExts.DisplayString(Modifiers.Shift)} + {ret}"; ret = "Shift + " + ret;
if (Modifiers.HasModifier(Modifiers.Alt)) if (Modifiers.HasModifier(Modifiers.Alt))
ret = $"{ModifiersExts.DisplayString(Modifiers.Alt)} + {ret}"; ret = "Alt + " + ret;
if (Modifiers.HasModifier(Modifiers.Ctrl)) if (Modifiers.HasModifier(Modifiers.Ctrl))
ret = $"{ModifiersExts.DisplayString(Modifiers.Ctrl)} + {ret}"; ret = "Ctrl + " + ret;
if (Modifiers.HasModifier(Modifiers.Meta)) if (Modifiers.HasModifier(Modifiers.Meta))
ret = $"{ModifiersExts.DisplayString(Modifiers.Meta)} + {ret}"; ret = (Platform.CurrentPlatform == PlatformType.OSX ? "Cmd + " : "Meta + ") + ret;
return ret; return ret;
} }

View File

@@ -10,7 +10,6 @@
#endregion #endregion
using System; using System;
using System.Collections.Generic;
namespace OpenRA namespace OpenRA
{ {
@@ -62,33 +61,6 @@ namespace OpenRA
Meta = 8, Meta = 8,
} }
public static class ModifiersExts
{
[FluentReference]
public const string Cmd = "keycode-modifier.cmd";
[FluentReference(Traits.LintDictionaryReference.Values)]
public static readonly IReadOnlyDictionary<Modifiers, string> ModifierFluentKeys = new Dictionary<Modifiers, string>()
{
{ Modifiers.None, "keycode-modifier.none" },
{ Modifiers.Shift, "keycode-modifier.shift" },
{ Modifiers.Alt, "keycode-modifier.alt" },
{ Modifiers.Ctrl, "keycode-modifier.ctrl" },
{ Modifiers.Meta, "keycode-modifier.meta" },
};
public static string DisplayString(Modifiers m)
{
if (m == Modifiers.Meta && Platform.CurrentPlatform == PlatformType.OSX)
return FluentProvider.GetMessage(Cmd);
if (!ModifierFluentKeys.TryGetValue(m, out var fluentKey))
return m.ToString();
return FluentProvider.GetMessage(fluentKey);
}
}
public enum KeyInputEvent { Down, Up } public enum KeyInputEvent { Down, Up }
public struct KeyInput public struct KeyInput
{ {

View File

@@ -259,255 +259,254 @@ namespace OpenRA
public static class KeycodeExts public static class KeycodeExts
{ {
[FluentReference(Traits.LintDictionaryReference.Values)] static readonly Dictionary<Keycode, string> KeyNames = new()
public static readonly IReadOnlyDictionary<Keycode, string> KeycodeFluentKeys = new Dictionary<Keycode, string>()
{ {
{ Keycode.UNKNOWN, "keycode.unknown" }, { Keycode.UNKNOWN, "Undefined" },
{ Keycode.RETURN, "keycode.return" }, { Keycode.RETURN, "Return" },
{ Keycode.ESCAPE, "keycode.escape" }, { Keycode.ESCAPE, "Escape" },
{ Keycode.BACKSPACE, "keycode.backspace" }, { Keycode.BACKSPACE, "Backspace" },
{ Keycode.TAB, "keycode.tab" }, { Keycode.TAB, "Tab" },
{ Keycode.SPACE, "keycode.space" }, { Keycode.SPACE, "Space" },
{ Keycode.EXCLAIM, "keycode.exclaim" }, { Keycode.EXCLAIM, "!" },
{ Keycode.QUOTEDBL, "keycode.quotedbl" }, { Keycode.QUOTEDBL, "\"" },
{ Keycode.HASH, "keycode.hash" }, { Keycode.HASH, "#" },
{ Keycode.PERCENT, "keycode.percent" }, { Keycode.PERCENT, "%" },
{ Keycode.DOLLAR, "keycode.dollar" }, { Keycode.DOLLAR, "$" },
{ Keycode.AMPERSAND, "keycode.ampersand" }, { Keycode.AMPERSAND, "&" },
{ Keycode.QUOTE, "keycode.quote" }, { Keycode.QUOTE, "'" },
{ Keycode.LEFTPAREN, "keycode.leftparen" }, { Keycode.LEFTPAREN, "(" },
{ Keycode.RIGHTPAREN, "keycode.rightparen" }, { Keycode.RIGHTPAREN, ")" },
{ Keycode.ASTERISK, "keycode.asterisk" }, { Keycode.ASTERISK, "*" },
{ Keycode.PLUS, "keycode.plus" }, { Keycode.PLUS, "+" },
{ Keycode.COMMA, "keycode.comma" }, { Keycode.COMMA, "," },
{ Keycode.MINUS, "keycode.minus" }, { Keycode.MINUS, "-" },
{ Keycode.PERIOD, "keycode.period" }, { Keycode.PERIOD, "." },
{ Keycode.SLASH, "keycode.slash" }, { Keycode.SLASH, "/" },
{ Keycode.NUMBER_0, "keycode.number_0" }, { Keycode.NUMBER_0, "0" },
{ Keycode.NUMBER_1, "keycode.number_1" }, { Keycode.NUMBER_1, "1" },
{ Keycode.NUMBER_2, "keycode.number_2" }, { Keycode.NUMBER_2, "2" },
{ Keycode.NUMBER_3, "keycode.number_3" }, { Keycode.NUMBER_3, "3" },
{ Keycode.NUMBER_4, "keycode.number_4" }, { Keycode.NUMBER_4, "4" },
{ Keycode.NUMBER_5, "keycode.number_5" }, { Keycode.NUMBER_5, "5" },
{ Keycode.NUMBER_6, "keycode.number_6" }, { Keycode.NUMBER_6, "6" },
{ Keycode.NUMBER_7, "keycode.number_7" }, { Keycode.NUMBER_7, "7" },
{ Keycode.NUMBER_8, "keycode.number_8" }, { Keycode.NUMBER_8, "8" },
{ Keycode.NUMBER_9, "keycode.number_9" }, { Keycode.NUMBER_9, "9" },
{ Keycode.COLON, "keycode.colon" }, { Keycode.COLON, ":" },
{ Keycode.SEMICOLON, "keycode.semicolon" }, { Keycode.SEMICOLON, ";" },
{ Keycode.LESS, "keycode.less" }, { Keycode.LESS, "<" },
{ Keycode.EQUALS, "keycode.equals" }, { Keycode.EQUALS, "=" },
{ Keycode.GREATER, "keycode.greater" }, { Keycode.GREATER, ">" },
{ Keycode.QUESTION, "keycode.question" }, { Keycode.QUESTION, "?" },
{ Keycode.AT, "keycode.at" }, { Keycode.AT, "@" },
{ Keycode.LEFTBRACKET, "keycode.leftbracket" }, { Keycode.LEFTBRACKET, "[" },
{ Keycode.BACKSLASH, "keycode.backslash" }, { Keycode.BACKSLASH, "\\" },
{ Keycode.RIGHTBRACKET, "keycode.rightbracket" }, { Keycode.RIGHTBRACKET, "]" },
{ Keycode.CARET, "keycode.caret" }, { Keycode.CARET, "^" },
{ Keycode.UNDERSCORE, "keycode.underscore" }, { Keycode.UNDERSCORE, "_" },
{ Keycode.BACKQUOTE, "keycode.backquote" }, { Keycode.BACKQUOTE, "`" },
{ Keycode.A, "keycode.a" }, { Keycode.A, "A" },
{ Keycode.B, "keycode.b" }, { Keycode.B, "B" },
{ Keycode.C, "keycode.c" }, { Keycode.C, "C" },
{ Keycode.D, "keycode.d" }, { Keycode.D, "D" },
{ Keycode.E, "keycode.e" }, { Keycode.E, "E" },
{ Keycode.F, "keycode.f" }, { Keycode.F, "F" },
{ Keycode.G, "keycode.g" }, { Keycode.G, "G" },
{ Keycode.H, "keycode.h" }, { Keycode.H, "H" },
{ Keycode.I, "keycode.i" }, { Keycode.I, "I" },
{ Keycode.J, "keycode.j" }, { Keycode.J, "J" },
{ Keycode.K, "keycode.k" }, { Keycode.K, "K" },
{ Keycode.L, "keycode.l" }, { Keycode.L, "L" },
{ Keycode.M, "keycode.m" }, { Keycode.M, "M" },
{ Keycode.N, "keycode.n" }, { Keycode.N, "N" },
{ Keycode.O, "keycode.o" }, { Keycode.O, "O" },
{ Keycode.P, "keycode.p" }, { Keycode.P, "P" },
{ Keycode.Q, "keycode.q" }, { Keycode.Q, "Q" },
{ Keycode.R, "keycode.r" }, { Keycode.R, "R" },
{ Keycode.S, "keycode.s" }, { Keycode.S, "S" },
{ Keycode.T, "keycode.t" }, { Keycode.T, "T" },
{ Keycode.U, "keycode.u" }, { Keycode.U, "U" },
{ Keycode.V, "keycode.v" }, { Keycode.V, "V" },
{ Keycode.W, "keycode.w" }, { Keycode.W, "W" },
{ Keycode.X, "keycode.x" }, { Keycode.X, "X" },
{ Keycode.Y, "keycode.y" }, { Keycode.Y, "Y" },
{ Keycode.Z, "keycode.z" }, { Keycode.Z, "Z" },
{ Keycode.CAPSLOCK, "keycode.capslock" }, { Keycode.CAPSLOCK, "CapsLock" },
{ Keycode.F1, "keycode.f1" }, { Keycode.F1, "F1" },
{ Keycode.F2, "keycode.f2" }, { Keycode.F2, "F2" },
{ Keycode.F3, "keycode.f3" }, { Keycode.F3, "F3" },
{ Keycode.F4, "keycode.f4" }, { Keycode.F4, "F4" },
{ Keycode.F5, "keycode.f5" }, { Keycode.F5, "F5" },
{ Keycode.F6, "keycode.f6" }, { Keycode.F6, "F6" },
{ Keycode.F7, "keycode.f7" }, { Keycode.F7, "F7" },
{ Keycode.F8, "keycode.f8" }, { Keycode.F8, "F8" },
{ Keycode.F9, "keycode.f9" }, { Keycode.F9, "F9" },
{ Keycode.F10, "keycode.f10" }, { Keycode.F10, "F10" },
{ Keycode.F11, "keycode.f11" }, { Keycode.F11, "F11" },
{ Keycode.F12, "keycode.f12" }, { Keycode.F12, "F12" },
{ Keycode.PRINTSCREEN, "keycode.printscreen" }, { Keycode.PRINTSCREEN, "PrintScreen" },
{ Keycode.SCROLLLOCK, "keycode.scrolllock" }, { Keycode.SCROLLLOCK, "ScrollLock" },
{ Keycode.PAUSE, "keycode.pause" }, { Keycode.PAUSE, "Pause" },
{ Keycode.INSERT, "keycode.insert" }, { Keycode.INSERT, "Insert" },
{ Keycode.HOME, "keycode.home" }, { Keycode.HOME, "Home" },
{ Keycode.PAGEUP, "keycode.pageup" }, { Keycode.PAGEUP, "PageUp" },
{ Keycode.DELETE, "keycode.delete" }, { Keycode.DELETE, "Delete" },
{ Keycode.END, "keycode.end" }, { Keycode.END, "End" },
{ Keycode.PAGEDOWN, "keycode.pagedown" }, { Keycode.PAGEDOWN, "PageDown" },
{ Keycode.RIGHT, "keycode.right" }, { Keycode.RIGHT, "Right" },
{ Keycode.LEFT, "keycode.left" }, { Keycode.LEFT, "Left" },
{ Keycode.DOWN, "keycode.down" }, { Keycode.DOWN, "Down" },
{ Keycode.UP, "keycode.up" }, { Keycode.UP, "Up" },
{ Keycode.NUMLOCKCLEAR, "keycode.numlockclear" }, { Keycode.NUMLOCKCLEAR, "Numlock" },
{ Keycode.KP_DIVIDE, "keycode.kp_divide" }, { Keycode.KP_DIVIDE, "Keypad /" },
{ Keycode.KP_MULTIPLY, "keycode.kp_multiply" }, { Keycode.KP_MULTIPLY, "Keypad *" },
{ Keycode.KP_MINUS, "keycode.kp_minus" }, { Keycode.KP_MINUS, "Keypad -" },
{ Keycode.KP_PLUS, "keycode.kp_plus" }, { Keycode.KP_PLUS, "Keypad +" },
{ Keycode.KP_ENTER, "keycode.kp_enter" }, { Keycode.KP_ENTER, "Keypad Enter" },
{ Keycode.KP_1, "keycode.kp_1" }, { Keycode.KP_1, "Keypad 1" },
{ Keycode.KP_2, "keycode.kp_2" }, { Keycode.KP_2, "Keypad 2" },
{ Keycode.KP_3, "keycode.kp_3" }, { Keycode.KP_3, "Keypad 3" },
{ Keycode.KP_4, "keycode.kp_4" }, { Keycode.KP_4, "Keypad 4" },
{ Keycode.KP_5, "keycode.kp_5" }, { Keycode.KP_5, "Keypad 5" },
{ Keycode.KP_6, "keycode.kp_6" }, { Keycode.KP_6, "Keypad 6" },
{ Keycode.KP_7, "keycode.kp_7" }, { Keycode.KP_7, "Keypad 7" },
{ Keycode.KP_8, "keycode.kp_8" }, { Keycode.KP_8, "Keypad 8" },
{ Keycode.KP_9, "keycode.kp_9" }, { Keycode.KP_9, "Keypad 9" },
{ Keycode.KP_0, "keycode.kp_0" }, { Keycode.KP_0, "Keypad 0" },
{ Keycode.KP_PERIOD, "keycode.kp_period" }, { Keycode.KP_PERIOD, "Keypad ." },
{ Keycode.APPLICATION, "keycode.application" }, { Keycode.APPLICATION, "Application" },
{ Keycode.POWER, "keycode.power" }, { Keycode.POWER, "Power" },
{ Keycode.KP_EQUALS, "keycode.kp_equals" }, { Keycode.KP_EQUALS, "Keypad =" },
{ Keycode.F13, "keycode.f13" }, { Keycode.F13, "F13" },
{ Keycode.F14, "keycode.f14" }, { Keycode.F14, "F14" },
{ Keycode.F15, "keycode.f15" }, { Keycode.F15, "F15" },
{ Keycode.F16, "keycode.f16" }, { Keycode.F16, "F16" },
{ Keycode.F17, "keycode.f17" }, { Keycode.F17, "F17" },
{ Keycode.F18, "keycode.f18" }, { Keycode.F18, "F18" },
{ Keycode.F19, "keycode.f19" }, { Keycode.F19, "F19" },
{ Keycode.F20, "keycode.f20" }, { Keycode.F20, "F20" },
{ Keycode.F21, "keycode.f21" }, { Keycode.F21, "F21" },
{ Keycode.F22, "keycode.f22" }, { Keycode.F22, "F22" },
{ Keycode.F23, "keycode.f23" }, { Keycode.F23, "F23" },
{ Keycode.F24, "keycode.f24" }, { Keycode.F24, "F24" },
{ Keycode.EXECUTE, "keycode.execute" }, { Keycode.EXECUTE, "Execute" },
{ Keycode.HELP, "keycode.help" }, { Keycode.HELP, "Help" },
{ Keycode.MENU, "keycode.menu" }, { Keycode.MENU, "Menu" },
{ Keycode.SELECT, "keycode.select" }, { Keycode.SELECT, "Select" },
{ Keycode.STOP, "keycode.stop" }, { Keycode.STOP, "Stop" },
{ Keycode.AGAIN, "keycode.again" }, { Keycode.AGAIN, "Again" },
{ Keycode.UNDO, "keycode.undo" }, { Keycode.UNDO, "Undo" },
{ Keycode.CUT, "keycode.cut" }, { Keycode.CUT, "Cut" },
{ Keycode.COPY, "keycode.copy" }, { Keycode.COPY, "Copy" },
{ Keycode.PASTE, "keycode.paste" }, { Keycode.PASTE, "Paste" },
{ Keycode.FIND, "keycode.find" }, { Keycode.FIND, "Find" },
{ Keycode.MUTE, "keycode.mute" }, { Keycode.MUTE, "Mute" },
{ Keycode.VOLUMEUP, "keycode.volumeup" }, { Keycode.VOLUMEUP, "VolumeUp" },
{ Keycode.VOLUMEDOWN, "keycode.volumedown" }, { Keycode.VOLUMEDOWN, "VolumeDown" },
{ Keycode.KP_COMMA, "keycode.kp_comma" }, { Keycode.KP_COMMA, "Keypad }," },
{ Keycode.KP_EQUALSAS400, "keycode.kp_equalsas400" }, { Keycode.KP_EQUALSAS400, "Keypad, (AS400)" },
{ Keycode.ALTERASE, "keycode.alterase" }, { Keycode.ALTERASE, "AltErase" },
{ Keycode.SYSREQ, "keycode.sysreq" }, { Keycode.SYSREQ, "SysReq" },
{ Keycode.CANCEL, "keycode.cancel" }, { Keycode.CANCEL, "Cancel" },
{ Keycode.CLEAR, "keycode.clear" }, { Keycode.CLEAR, "Clear" },
{ Keycode.PRIOR, "keycode.prior" }, { Keycode.PRIOR, "Prior" },
{ Keycode.RETURN2, "keycode.return2" }, { Keycode.RETURN2, "Return" },
{ Keycode.SEPARATOR, "keycode.separator" }, { Keycode.SEPARATOR, "Separator" },
{ Keycode.OUT, "keycode.out" }, { Keycode.OUT, "Out" },
{ Keycode.OPER, "keycode.oper" }, { Keycode.OPER, "Oper" },
{ Keycode.CLEARAGAIN, "keycode.clearagain" }, { Keycode.CLEARAGAIN, "Clear / Again" },
{ Keycode.CRSEL, "keycode.crsel" }, { Keycode.CRSEL, "CrSel" },
{ Keycode.EXSEL, "keycode.exsel" }, { Keycode.EXSEL, "ExSel" },
{ Keycode.KP_00, "keycode.kp_00" }, { Keycode.KP_00, "Keypad 00" },
{ Keycode.KP_000, "keycode.kp_000" }, { Keycode.KP_000, "Keypad 000" },
{ Keycode.THOUSANDSSEPARATOR, "keycode.thousandsseparator" }, { Keycode.THOUSANDSSEPARATOR, "ThousandsSeparator" },
{ Keycode.DECIMALSEPARATOR, "keycode.decimalseparator" }, { Keycode.DECIMALSEPARATOR, "DecimalSeparator" },
{ Keycode.CURRENCYUNIT, "keycode.currencyunit" }, { Keycode.CURRENCYUNIT, "CurrencyUnit" },
{ Keycode.CURRENCYSUBUNIT, "keycode.currencysubunit" }, { Keycode.CURRENCYSUBUNIT, "CurrencySubUnit" },
{ Keycode.KP_LEFTPAREN, "keycode.kp_leftparen" }, { Keycode.KP_LEFTPAREN, "Keypad (" },
{ Keycode.KP_RIGHTPAREN, "keycode.kp_rightparen" }, { Keycode.KP_RIGHTPAREN, "Keypad )" },
{ Keycode.KP_LEFTBRACE, "keycode.kp_leftbrace" }, { Keycode.KP_LEFTBRACE, "Keypad {" },
{ Keycode.KP_RIGHTBRACE, "keycode.kp_rightbrace" }, { Keycode.KP_RIGHTBRACE, "Keypad }" },
{ Keycode.KP_TAB, "keycode.kp_tab" }, { Keycode.KP_TAB, "Keypad Tab" },
{ Keycode.KP_BACKSPACE, "keycode.kp_backspace" }, { Keycode.KP_BACKSPACE, "Keypad Backspace" },
{ Keycode.KP_A, "keycode.kp_a" }, { Keycode.KP_A, "Keypad A" },
{ Keycode.KP_B, "keycode.kp_b" }, { Keycode.KP_B, "Keypad B" },
{ Keycode.KP_C, "keycode.kp_c" }, { Keycode.KP_C, "Keypad C" },
{ Keycode.KP_D, "keycode.kp_d" }, { Keycode.KP_D, "Keypad D" },
{ Keycode.KP_E, "keycode.kp_e" }, { Keycode.KP_E, "Keypad E" },
{ Keycode.KP_F, "keycode.kp_f" }, { Keycode.KP_F, "Keypad F" },
{ Keycode.KP_XOR, "keycode.kp_xor" }, { Keycode.KP_XOR, "Keypad XOR" },
{ Keycode.KP_POWER, "keycode.kp_power" }, { Keycode.KP_POWER, "Keypad ^" },
{ Keycode.KP_PERCENT, "keycode.kp_percent" }, { Keycode.KP_PERCENT, "Keypad %" },
{ Keycode.KP_LESS, "keycode.kp_less" }, { Keycode.KP_LESS, "Keypad <" },
{ Keycode.KP_GREATER, "keycode.kp_greater" }, { Keycode.KP_GREATER, "Keypad >" },
{ Keycode.KP_AMPERSAND, "keycode.kp_ampersand" }, { Keycode.KP_AMPERSAND, "Keypad &" },
{ Keycode.KP_DBLAMPERSAND, "keycode.kp_dblampersand" }, { Keycode.KP_DBLAMPERSAND, "Keypad &&" },
{ Keycode.KP_VERTICALBAR, "keycode.kp_verticalbar" }, { Keycode.KP_VERTICALBAR, "Keypad |" },
{ Keycode.KP_DBLVERTICALBAR, "keycode.kp_dblverticalbar" }, { Keycode.KP_DBLVERTICALBAR, "Keypad ||" },
{ Keycode.KP_COLON, "keycode.kp_colon" }, { Keycode.KP_COLON, "Keypad :" },
{ Keycode.KP_HASH, "keycode.kp_hash" }, { Keycode.KP_HASH, "Keypad #" },
{ Keycode.KP_SPACE, "keycode.kp_space" }, { Keycode.KP_SPACE, "Keypad Space" },
{ Keycode.KP_AT, "keycode.kp_at" }, { Keycode.KP_AT, "Keypad @" },
{ Keycode.KP_EXCLAM, "keycode.kp_exclam" }, { Keycode.KP_EXCLAM, "Keypad !" },
{ Keycode.KP_MEMSTORE, "keycode.kp_memstore" }, { Keycode.KP_MEMSTORE, "Keypad MemStore" },
{ Keycode.KP_MEMRECALL, "keycode.kp_memrecall" }, { Keycode.KP_MEMRECALL, "Keypad MemRecall" },
{ Keycode.KP_MEMCLEAR, "keycode.kp_memclear" }, { Keycode.KP_MEMCLEAR, "Keypad MemClear" },
{ Keycode.KP_MEMADD, "keycode.kp_memadd" }, { Keycode.KP_MEMADD, "Keypad MemAdd" },
{ Keycode.KP_MEMSUBTRACT, "keycode.kp_memsubtract" }, { Keycode.KP_MEMSUBTRACT, "Keypad MemSubtract" },
{ Keycode.KP_MEMMULTIPLY, "keycode.kp_memmultiply" }, { Keycode.KP_MEMMULTIPLY, "Keypad MemMultiply" },
{ Keycode.KP_MEMDIVIDE, "keycode.kp_memdivide" }, { Keycode.KP_MEMDIVIDE, "Keypad MemDivide" },
{ Keycode.KP_PLUSMINUS, "keycode.kp_plusminus" }, { Keycode.KP_PLUSMINUS, "Keypad +/-" },
{ Keycode.KP_CLEAR, "keycode.kp_clear" }, { Keycode.KP_CLEAR, "Keypad Clear" },
{ Keycode.KP_CLEARENTRY, "keycode.kp_clearentry" }, { Keycode.KP_CLEARENTRY, "Keypad ClearEntry" },
{ Keycode.KP_BINARY, "keycode.kp_binary" }, { Keycode.KP_BINARY, "Keypad Binary" },
{ Keycode.KP_OCTAL, "keycode.kp_octal" }, { Keycode.KP_OCTAL, "Keypad Octal" },
{ Keycode.KP_DECIMAL, "keycode.kp_decimal" }, { Keycode.KP_DECIMAL, "Keypad Decimal" },
{ Keycode.KP_HEXADECIMAL, "keycode.kp_hexadecimal" }, { Keycode.KP_HEXADECIMAL, "Keypad Hexadecimal" },
{ Keycode.LCTRL, "keycode.lctrl" }, { Keycode.LCTRL, "Left Ctrl" },
{ Keycode.LSHIFT, "keycode.lshift" }, { Keycode.LSHIFT, "Left Shift" },
{ Keycode.LALT, "keycode.lalt" }, { Keycode.LALT, "Left Alt" },
{ Keycode.LGUI, "keycode.lgui" }, { Keycode.LGUI, "Left GUI" },
{ Keycode.RCTRL, "keycode.rctrl" }, { Keycode.RCTRL, "Right Ctrl" },
{ Keycode.RSHIFT, "keycode.rshift" }, { Keycode.RSHIFT, "Right Shift" },
{ Keycode.RALT, "keycode.ralt" }, { Keycode.RALT, "Right Alt" },
{ Keycode.RGUI, "keycode.rgui" }, { Keycode.RGUI, "Right GUI" },
{ Keycode.MODE, "keycode.mode" }, { Keycode.MODE, "ModeSwitch" },
{ Keycode.AUDIONEXT, "keycode.audionext" }, { Keycode.AUDIONEXT, "AudioNext" },
{ Keycode.AUDIOPREV, "keycode.audioprev" }, { Keycode.AUDIOPREV, "AudioPrev" },
{ Keycode.AUDIOSTOP, "keycode.audiostop" }, { Keycode.AUDIOSTOP, "AudioStop" },
{ Keycode.AUDIOPLAY, "keycode.audioplay" }, { Keycode.AUDIOPLAY, "AudioPlay" },
{ Keycode.AUDIOMUTE, "keycode.audiomute" }, { Keycode.AUDIOMUTE, "AudioMute" },
{ Keycode.MEDIASELECT, "keycode.mediaselect" }, { Keycode.MEDIASELECT, "MediaSelect" },
{ Keycode.WWW, "keycode.www" }, { Keycode.WWW, "WWW" },
{ Keycode.MAIL, "keycode.mail" }, { Keycode.MAIL, "Mail" },
{ Keycode.CALCULATOR, "keycode.calculator" }, { Keycode.CALCULATOR, "Calculator" },
{ Keycode.COMPUTER, "keycode.computer" }, { Keycode.COMPUTER, "Computer" },
{ Keycode.AC_SEARCH, "keycode.ac_search" }, { Keycode.AC_SEARCH, "AC Search" },
{ Keycode.AC_HOME, "keycode.ac_home" }, { Keycode.AC_HOME, "AC Home" },
{ Keycode.AC_BACK, "keycode.ac_back" }, { Keycode.AC_BACK, "AC Back" },
{ Keycode.AC_FORWARD, "keycode.ac_forward" }, { Keycode.AC_FORWARD, "AC Forward" },
{ Keycode.AC_STOP, "keycode.ac_stop" }, { Keycode.AC_STOP, "AC Stop" },
{ Keycode.AC_REFRESH, "keycode.ac_refresh" }, { Keycode.AC_REFRESH, "AC Refresh" },
{ Keycode.AC_BOOKMARKS, "keycode.ac_bookmarks" }, { Keycode.AC_BOOKMARKS, "AC Bookmarks" },
{ Keycode.BRIGHTNESSDOWN, "keycode.brightnessdown" }, { Keycode.BRIGHTNESSDOWN, "BrightnessDown" },
{ Keycode.BRIGHTNESSUP, "keycode.brightnessup" }, { Keycode.BRIGHTNESSUP, "BrightnessUp" },
{ Keycode.DISPLAYSWITCH, "keycode.displayswitch" }, { Keycode.DISPLAYSWITCH, "DisplaySwitch" },
{ Keycode.KBDILLUMTOGGLE, "keycode.kbdillumtoggle" }, { Keycode.KBDILLUMTOGGLE, "KBDIllumToggle" },
{ Keycode.KBDILLUMDOWN, "keycode.kbdillumdown" }, { Keycode.KBDILLUMDOWN, "KBDIllumDown" },
{ Keycode.KBDILLUMUP, "keycode.kbdillumup" }, { Keycode.KBDILLUMUP, "KBDIllumUp" },
{ Keycode.EJECT, "keycode.eject" }, { Keycode.EJECT, "Eject" },
{ Keycode.SLEEP, "keycode.sleep" }, { Keycode.SLEEP, "Sleep" },
{ Keycode.MOUSE4, "keycode.mouse4" }, { Keycode.MOUSE4, "Mouse 4" },
{ Keycode.MOUSE5, "keycode.mouse5" }, { Keycode.MOUSE5, "Mouse 5" },
}; };
public static string DisplayString(Keycode k) public static string DisplayString(Keycode k)
{ {
if (!KeycodeFluentKeys.TryGetValue(k, out var fluentKey)) if (!KeyNames.TryGetValue(k, out var ret))
return k.ToString(); return k.ToString();
return FluentProvider.GetMessage(fluentKey); return ret;
} }
} }
} }

View File

@@ -84,11 +84,10 @@ namespace OpenRA
{ {
var client = HttpClientFactory.Create(); var client = HttpClientFactory.Create();
var url = playerDatabase.Profile + Fingerprint; var httpResponseMessage = await client.GetAsync(playerDatabase.Profile + Fingerprint);
var httpResponseMessage = await client.GetAsync(url);
var result = await httpResponseMessage.Content.ReadAsStreamAsync(); var result = await httpResponseMessage.Content.ReadAsStreamAsync();
var yaml = MiniYaml.FromStream(result, url).First(); var yaml = MiniYaml.FromStream(result).First();
if (yaml.Key == "Player") if (yaml.Key == "Player")
{ {
innerData = FieldLoader.Load<PlayerProfile>(yaml.Value); innerData = FieldLoader.Load<PlayerProfile>(yaml.Value);

View File

@@ -43,22 +43,25 @@ 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 class ModMetadata
{ {
// FieldLoader used here, must matching naming in YAML. public string Title;
#pragma warning disable IDE1006 // Naming Styles public string Version;
[FluentReference] public string Website;
readonly string Title; public string WebIcon32;
public readonly string Version; public string WindowTitle;
public readonly string Website; public bool Hidden;
public readonly string WebIcon32;
[FluentReference]
readonly string WindowTitle;
public readonly bool Hidden;
#pragma warning restore IDE1006 // Naming Styles
public string TitleTranslated => FluentProvider.GetMessage(Title);
public string WindowTitleTranslated => WindowTitle != null ? FluentProvider.GetMessage(WindowTitle) : null;
} }
/// <summary>Describes what is to be loaded in order to run a mod.</summary> /// <summary>Describes what is to be loaded in order to run a mod.</summary>
@@ -69,34 +72,27 @@ namespace OpenRA
public readonly ModMetadata Metadata; public readonly ModMetadata Metadata;
public readonly string[] public readonly string[]
Rules, ServerTraits, Rules, ServerTraits,
Sequences, ModelSequences, Cursors, Chrome, ChromeLayout, Sequences, ModelSequences, Cursors, Chrome, Assemblies, ChromeLayout,
Weapons, Voices, Notifications, Music, FluentMessages, TileSets, Weapons, Voices, Notifications, Music, Translations, TileSets,
ChromeMetrics, MapCompatibility, Missions, Hotkeys; ChromeMetrics, MapCompatibility, Missions, Hotkeys;
public readonly IReadOnlyDictionary<string, string> Packages;
public readonly IReadOnlyDictionary<string, string> MapFolders; public readonly IReadOnlyDictionary<string, string> MapFolders;
public readonly MiniYaml FileSystem;
public readonly MiniYaml LoadScreen; public readonly MiniYaml LoadScreen;
public readonly string DefaultOrderGenerator; public readonly string DefaultOrderGenerator;
public readonly string[] Assemblies = Array.Empty<string>();
public readonly string[] SoundFormats = Array.Empty<string>(); public readonly string[] SoundFormats = Array.Empty<string>();
public readonly string[] SpriteFormats = Array.Empty<string>(); public readonly string[] SpriteFormats = Array.Empty<string>();
public readonly string[] PackageFormats = Array.Empty<string>(); public readonly string[] PackageFormats = Array.Empty<string>();
public readonly string[] VideoFormats = Array.Empty<string>(); public readonly string[] VideoFormats = Array.Empty<string>();
public readonly int FontSheetSize = 512;
public readonly int CursorSheetSize = 512;
// TODO: This should be controlled by a user-selected translation bundle!
public readonly string FluentCulture = "en";
public readonly bool AllowUnusedFluentMessagesInExternalPackages = true;
readonly string[] reservedModuleNames = readonly string[] reservedModuleNames =
{ {
"Include", "Metadata", "FileSystem", "MapFolders", "Rules", "Include", "Metadata", "Folders", "MapFolders", "Packages", "Rules",
"Sequences", "ModelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons", "Sequences", "ModelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons",
"Voices", "Notifications", "Music", "FluentMessages", "TileSets", "ChromeMetrics", "Missions", "Hotkeys", "Voices", "Notifications", "Music", "Translations", "TileSets", "ChromeMetrics", "Missions", "Hotkeys",
"ServerTraits", "LoadScreen", "DefaultOrderGenerator", "SupportsMapsFrom", "SoundFormats", "SpriteFormats", "VideoFormats", "ServerTraits", "LoadScreen", "DefaultOrderGenerator", "SupportsMapsFrom", "SoundFormats", "SpriteFormats", "VideoFormats",
"RequiresMods", "PackageFormats", "AllowUnusedFluentMessagesInExternalPackages", "FontSheetSize", "CursorSheetSize" "RequiresMods", "PackageFormats"
}; };
readonly TypeDictionary modules = new(); readonly TypeDictionary modules = new();
@@ -109,8 +105,7 @@ namespace OpenRA
Id = modId; Id = modId;
Package = package; Package = package;
var stringPool = new HashSet<string>(); // Reuse common strings in YAML var nodes = MiniYaml.FromStream(package.GetStream("mod.yaml"), "mod.yaml");
var nodes = MiniYaml.FromStream(package.GetStream("mod.yaml"), $"{package.Name}:mod.yaml", stringPool: stringPool);
for (var i = nodes.Count - 1; i >= 0; i--) for (var i = nodes.Count - 1; i >= 0; i--)
{ {
if (nodes[i].Key != "Include") if (nodes[i].Key != "Include")
@@ -123,7 +118,7 @@ namespace OpenRA
throw new YamlException($"{nodes[i].Location}: File `{filename}` not found."); throw new YamlException($"{nodes[i].Location}: File `{filename}` not found.");
nodes.RemoveAt(i); nodes.RemoveAt(i);
nodes.InsertRange(i, MiniYaml.FromStream(contents, $"{package.Name}:{filename}", stringPool: stringPool)); nodes.InsertRange(i, MiniYaml.FromStream(contents, filename));
} }
// Merge inherited overrides // Merge inherited overrides
@@ -134,20 +129,21 @@ namespace OpenRA
// TODO: Use fieldloader // TODO: Use fieldloader
MapFolders = YamlDictionary(yaml, "MapFolders"); MapFolders = YamlDictionary(yaml, "MapFolders");
if (!yaml.TryGetValue("FileSystem", out FileSystem)) if (yaml.TryGetValue("Packages", out var packages))
throw new InvalidDataException("`FileSystem` section is not defined."); Packages = packages.ToDictionary(x => x.Value);
Rules = YamlList(yaml, "Rules"); Rules = YamlList(yaml, "Rules");
Sequences = YamlList(yaml, "Sequences"); Sequences = YamlList(yaml, "Sequences");
ModelSequences = YamlList(yaml, "ModelSequences"); ModelSequences = YamlList(yaml, "ModelSequences");
Cursors = YamlList(yaml, "Cursors"); Cursors = YamlList(yaml, "Cursors");
Chrome = YamlList(yaml, "Chrome"); Chrome = YamlList(yaml, "Chrome");
Assemblies = YamlList(yaml, "Assemblies");
ChromeLayout = YamlList(yaml, "ChromeLayout"); ChromeLayout = YamlList(yaml, "ChromeLayout");
Weapons = YamlList(yaml, "Weapons"); Weapons = YamlList(yaml, "Weapons");
Voices = YamlList(yaml, "Voices"); Voices = YamlList(yaml, "Voices");
Notifications = YamlList(yaml, "Notifications"); Notifications = YamlList(yaml, "Notifications");
Music = YamlList(yaml, "Music"); Music = YamlList(yaml, "Music");
FluentMessages = YamlList(yaml, "FluentMessages"); Translations = YamlList(yaml, "Translations");
TileSets = YamlList(yaml, "TileSets"); TileSets = YamlList(yaml, "TileSets");
ChromeMetrics = YamlList(yaml, "ChromeMetrics"); ChromeMetrics = YamlList(yaml, "ChromeMetrics");
Missions = YamlList(yaml, "Missions"); Missions = YamlList(yaml, "Missions");
@@ -169,9 +165,6 @@ namespace OpenRA
if (yaml.TryGetValue("DefaultOrderGenerator", out entry)) if (yaml.TryGetValue("DefaultOrderGenerator", out entry))
DefaultOrderGenerator = entry.Value; DefaultOrderGenerator = entry.Value;
if (yaml.TryGetValue("Assemblies", out entry))
Assemblies = FieldLoader.GetValue<string[]>("Assemblies", entry.Value);
if (yaml.TryGetValue("PackageFormats", out entry)) if (yaml.TryGetValue("PackageFormats", out entry))
PackageFormats = FieldLoader.GetValue<string[]>("PackageFormats", entry.Value); PackageFormats = FieldLoader.GetValue<string[]>("PackageFormats", entry.Value);
@@ -183,16 +176,6 @@ namespace OpenRA
if (yaml.TryGetValue("VideoFormats", out entry)) if (yaml.TryGetValue("VideoFormats", out entry))
VideoFormats = FieldLoader.GetValue<string[]>("VideoFormats", entry.Value); VideoFormats = FieldLoader.GetValue<string[]>("VideoFormats", entry.Value);
if (yaml.TryGetValue("AllowUnusedFluentMessagesInExternalPackages", out entry))
AllowUnusedFluentMessagesInExternalPackages =
FieldLoader.GetValue<bool>("AllowUnusedFluentMessagesInExternalPackages", entry.Value);
if (yaml.TryGetValue("FontSheetSize", out entry))
FontSheetSize = FieldLoader.GetValue<int>("FontSheetSize", entry.Value);
if (yaml.TryGetValue("CursorSheetSize", out entry))
CursorSheetSize = FieldLoader.GetValue<int>("CursorSheetSize", entry.Value);
} }
public void LoadCustomData(ObjectCreator oc) public void LoadCustomData(ObjectCreator oc)
@@ -228,18 +211,18 @@ namespace OpenRA
static string[] YamlList(Dictionary<string, MiniYaml> yaml, string key) static string[] YamlList(Dictionary<string, MiniYaml> yaml, string key)
{ {
if (!yaml.TryGetValue(key, out var value)) if (!yaml.ContainsKey(key))
return Array.Empty<string>(); return Array.Empty<string>();
return value.Nodes.Select(n => n.Key).ToArray(); return yaml[key].ToDictionary().Keys.ToArray();
} }
static IReadOnlyDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key) static IReadOnlyDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
{ {
if (!yaml.TryGetValue(key, out var value)) if (!yaml.ContainsKey(key))
return new Dictionary<string, string>(); return new Dictionary<string, string>();
return value.ToDictionary(my => my.Value); return yaml[key].ToDictionary(my => my.Value);
} }
public bool Contains<T>() where T : IGlobalModData public bool Contains<T>() where T : IGlobalModData

View File

@@ -154,7 +154,7 @@ namespace OpenRA
public virtual void Initialize(MiniYaml yaml) public virtual void Initialize(MiniYaml yaml)
{ {
Initialize(FieldLoader.GetValue<T>(nameof(value), yaml.Value)); Initialize((T)FieldLoader.GetValue(nameof(value), typeof(T), yaml.Value));
} }
public virtual void Initialize(T value) public virtual void Initialize(T value)
@@ -175,7 +175,8 @@ namespace OpenRA
protected CompositeActorInit(TraitInfo info) protected CompositeActorInit(TraitInfo info)
: base(info.InstanceName) { } : base(info.InstanceName) { }
protected CompositeActorInit() { } protected CompositeActorInit()
: base() { }
public virtual void Initialize(MiniYaml yaml) public virtual void Initialize(MiniYaml yaml)
{ {

View File

@@ -84,7 +84,7 @@ namespace OpenRA
public MiniYaml Save(Func<ActorInit, bool> initFilter = null) public MiniYaml Save(Func<ActorInit, bool> initFilter = null)
{ {
var nodes = new List<MiniYamlNode>(); var ret = new MiniYaml(Type);
foreach (var o in initDict.Value) foreach (var o in initDict.Value)
{ {
if (o is not ActorInit init || o is ISuppressInitExport) if (o is not ActorInit init || o is ISuppressInitExport)
@@ -98,10 +98,10 @@ namespace OpenRA
if (!string.IsNullOrEmpty(init.InstanceName)) if (!string.IsNullOrEmpty(init.InstanceName))
initName += ActorInfo.TraitInstanceSeparator + init.InstanceName; initName += ActorInfo.TraitInstanceSeparator + init.InstanceName;
nodes.Add(new MiniYamlNode(initName, init.Save())); ret.Nodes.Add(new MiniYamlNode(initName, init.Save()));
} }
return new MiniYaml(Type, nodes); return ret;
} }
public IEnumerator<object> GetEnumerator() { return initDict.Value.GetEnumerator(); } public IEnumerator<object> GetEnumerator() { return initDict.Value.GetEnumerator(); }
@@ -139,7 +139,7 @@ namespace OpenRA
return removed; return removed;
} }
public IReadOnlyCollection<T> GetAll<T>() where T : ActorInit public IEnumerable<T> GetAll<T>() where T : ActorInit
{ {
return initDict.Value.WithInterface<T>(); return initDict.Value.WithInterface<T>();
} }
@@ -152,8 +152,7 @@ namespace OpenRA
// If a more specific init is not available, fall back to an unnamed init. // 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 duplicate inits are defined, take the last to match standard yaml override expectations
if (info != null && !string.IsNullOrEmpty(info.InstanceName)) if (info != null && !string.IsNullOrEmpty(info.InstanceName))
return return inits.LastOrDefault(i => i.InstanceName == info.InstanceName) ??
inits.LastOrDefault(i => i.InstanceName == info.InstanceName) ??
inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName)); inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName));
// Untagged traits will only use untagged inits // Untagged traits will only use untagged inits

View File

@@ -1,95 +0,0 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections;
using System.Collections.Generic;
namespace OpenRA
{
public readonly struct CellCoordsRegion : IEnumerable<CPos>
{
public struct CellCoordsEnumerator : IEnumerator<CPos>
{
readonly CellCoordsRegion r;
public CellCoordsEnumerator(CellCoordsRegion region)
: this()
{
r = region;
Reset();
}
public bool MoveNext()
{
var x = Current.X + 1;
var y = Current.Y;
// Check for column overflow.
if (x > r.BottomRight.X)
{
y++;
x = r.TopLeft.X;
// Check for row overflow.
if (y > r.BottomRight.Y)
return false;
}
Current = new CPos(x, y);
return true;
}
public void Reset()
{
Current = new CPos(r.TopLeft.X - 1, r.TopLeft.Y);
}
public CPos Current { get; private set; }
readonly object IEnumerator.Current => Current;
public readonly void Dispose() { }
}
public CellCoordsRegion(CPos topLeft, CPos bottomRight)
{
TopLeft = topLeft;
BottomRight = bottomRight;
}
public bool Contains(CPos cell)
{
return cell.X >= TopLeft.X && cell.X <= BottomRight.X && cell.Y >= TopLeft.Y && cell.Y <= BottomRight.Y;
}
public override string ToString()
{
return $"{TopLeft}->{BottomRight}";
}
public CellCoordsEnumerator GetEnumerator()
{
return new CellCoordsEnumerator(this);
}
IEnumerator<CPos> IEnumerable<CPos>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public CPos TopLeft { get; }
public CPos BottomRight { get; }
}
}

View File

@@ -12,6 +12,7 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace OpenRA namespace OpenRA
{ {
@@ -63,9 +64,9 @@ namespace OpenRA
} }
/// <summary>Returns the minimal region that covers at least the specified cells.</summary> /// <summary>Returns the minimal region that covers at least the specified cells.</summary>
public static CellRegion BoundingRegion(MapGridType shape, IReadOnlyCollection<CPos> cells) public static CellRegion BoundingRegion(MapGridType shape, IEnumerable<CPos> cells)
{ {
if (cells == null || cells.Count == 0) if (cells == null || !cells.Any())
throw new ArgumentException("cells must not be null or empty.", nameof(cells)); throw new ArgumentException("cells must not be null or empty.", nameof(cells));
var minU = int.MaxValue; var minU = int.MaxValue;
@@ -102,7 +103,6 @@ namespace OpenRA
} }
public MapCoordsRegion MapCoords => new(mapTopLeft, mapBottomRight); public MapCoordsRegion MapCoords => new(mapTopLeft, mapBottomRight);
public CellCoordsRegion CellCoords => new(TopLeft, BottomRight);
public CellRegionEnumerator GetEnumerator() public CellRegionEnumerator GetEnumerator()
{ {
@@ -136,12 +136,12 @@ namespace OpenRA
public bool MoveNext() public bool MoveNext()
{ {
u++; u += 1;
// Check for column overflow // Check for column overflow
if (u > r.mapBottomRight.U) if (u > r.mapBottomRight.U)
{ {
v++; v += 1;
u = r.mapTopLeft.U; u = r.mapTopLeft.U;
// Check for row overflow // Check for row overflow
@@ -162,8 +162,8 @@ namespace OpenRA
} }
public CPos Current { get; private set; } public CPos Current { get; private set; }
readonly object IEnumerator.Current => Current; object IEnumerator.Current => Current;
public readonly void Dispose() { } public void Dispose() { }
} }
} }
} }

View File

@@ -11,7 +11,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@@ -91,13 +90,13 @@ namespace OpenRA
throw new InvalidOperationException("Map does not have a field/property " + fieldName); throw new InvalidOperationException("Map does not have a field/property " + fieldName);
var t = field != null ? field.FieldType : property.PropertyType; var t = field != null ? field.FieldType : property.PropertyType;
type = t == typeof(IReadOnlyCollection<MiniYamlNode>) ? Type.NodeList : type = t == typeof(List<MiniYamlNode>) ? Type.NodeList :
t == typeof(MiniYaml) ? Type.MiniYaml : Type.Normal; t == typeof(MiniYaml) ? Type.MiniYaml : Type.Normal;
} }
public void Deserialize(Map map, MiniYaml yaml) public void Deserialize(Map map, List<MiniYamlNode> nodes)
{ {
var node = yaml.NodeWithKeyOrDefault(key); var node = nodes.FirstOrDefault(n => n.Key == key);
if (node == null) if (node == null)
{ {
if (required) if (required)
@@ -131,14 +130,14 @@ namespace OpenRA
var value = field != null ? field.GetValue(map) : property.GetValue(map, null); var value = field != null ? field.GetValue(map) : property.GetValue(map, null);
if (type == Type.NodeList) if (type == Type.NodeList)
{ {
var listValue = (IReadOnlyCollection<MiniYamlNode>)value; var listValue = (List<MiniYamlNode>)value;
if (required || listValue.Count > 0) if (required || listValue.Count > 0)
nodes.Add(new MiniYamlNode(key, null, listValue)); nodes.Add(new MiniYamlNode(key, null, listValue));
} }
else if (type == Type.MiniYaml) else if (type == Type.MiniYaml)
{ {
var yamlValue = (MiniYaml)value; var yamlValue = (MiniYaml)value;
if (required || (yamlValue != null && (yamlValue.Value != null || yamlValue.Nodes.Length > 0))) if (required || (yamlValue != null && (yamlValue.Value != null || yamlValue.Nodes.Count > 0)))
nodes.Add(new MiniYamlNode(key, yamlValue)); nodes.Add(new MiniYamlNode(key, yamlValue));
} }
else else
@@ -159,26 +158,26 @@ namespace OpenRA
/// <summary>Defines the order of the fields in map.yaml.</summary> /// <summary>Defines the order of the fields in map.yaml.</summary>
static readonly MapField[] YamlFields = static readonly MapField[] YamlFields =
{ {
new("MapFormat"), new MapField("MapFormat"),
new("RequiresMod"), new MapField("RequiresMod"),
new("Title"), new MapField("Title"),
new("Author"), new MapField("Author"),
new("Tileset"), new MapField("Tileset"),
new("MapSize"), new MapField("MapSize"),
new("Bounds"), new MapField("Bounds"),
new("Visibility"), new MapField("Visibility"),
new("Categories"), new MapField("Categories"),
new("LockPreview", required: false, ignoreIfValue: "False"), new MapField("LockPreview", required: false, ignoreIfValue: "False"),
new("Players", nameof(PlayerDefinitions)), new MapField("Players", "PlayerDefinitions"),
new("Actors", nameof(ActorDefinitions)), new MapField("Actors", "ActorDefinitions"),
new("Rules", nameof(RuleDefinitions), required: false), new MapField("Rules", "RuleDefinitions", required: false),
new("FluentMessages", nameof(FluentMessageDefinitions), required: false), new MapField("Translations", "TranslationDefinitions", required: false),
new("Sequences", nameof(SequenceDefinitions), required: false), new MapField("Sequences", "SequenceDefinitions", required: false),
new("ModelSequences", nameof(ModelSequenceDefinitions), required: false), new MapField("ModelSequences", "ModelSequenceDefinitions", required: false),
new("Weapons", nameof(WeaponDefinitions), required: false), new MapField("Weapons", "WeaponDefinitions", required: false),
new("Voices", nameof(VoiceDefinitions), required: false), new MapField("Voices", "VoiceDefinitions", required: false),
new("Music", nameof(MusicDefinitions), required: false), new MapField("Music", "MusicDefinitions", required: false),
new("Notifications", nameof(NotificationDefinitions), required: false), new MapField("Notifications", "NotificationDefinitions", required: false),
}; };
// Format versions // Format versions
@@ -198,18 +197,18 @@ namespace OpenRA
public int2 MapSize { get; private set; } public int2 MapSize { get; private set; }
// Player and actor yaml. Public for access by the map importers and lint checks. // Player and actor yaml. Public for access by the map importers and lint checks.
public IReadOnlyCollection<MiniYamlNode> PlayerDefinitions = ImmutableArray<MiniYamlNode>.Empty; public List<MiniYamlNode> PlayerDefinitions = new();
public IReadOnlyCollection<MiniYamlNode> ActorDefinitions = ImmutableArray<MiniYamlNode>.Empty; public List<MiniYamlNode> ActorDefinitions = new();
// Custom map yaml. Public for access by the map importers and lint checks // Custom map yaml. Public for access by the map importers and lint checks
public MiniYaml RuleDefinitions; public readonly MiniYaml RuleDefinitions;
public MiniYaml FluentMessageDefinitions; public readonly MiniYaml TranslationDefinitions;
public MiniYaml SequenceDefinitions; public readonly MiniYaml SequenceDefinitions;
public MiniYaml ModelSequenceDefinitions; public readonly MiniYaml ModelSequenceDefinitions;
public MiniYaml WeaponDefinitions; public readonly MiniYaml WeaponDefinitions;
public MiniYaml VoiceDefinitions; public readonly MiniYaml VoiceDefinitions;
public MiniYaml MusicDefinitions; public readonly MiniYaml MusicDefinitions;
public MiniYaml NotificationDefinitions; public readonly MiniYaml NotificationDefinitions;
public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = new(); public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = new();
@@ -275,10 +274,7 @@ namespace OpenRA
try try
{ {
foreach (var filename in contents) foreach (var filename in contents)
if (filename.EndsWith(".yaml", StringComparison.Ordinal) || if (filename.EndsWith(".yaml") || filename.EndsWith(".bin") || filename.EndsWith(".lua") || (format >= 12 && filename == "map.png"))
filename.EndsWith(".bin", StringComparison.Ordinal) ||
filename.EndsWith(".lua", StringComparison.Ordinal) ||
(format >= 12 && filename == "map.png"))
streams.Add(package.GetStream(filename)); streams.Add(package.GetStream(filename));
// Take the SHA1 // Take the SHA1
@@ -361,15 +357,15 @@ namespace OpenRA
if (!Package.Contains("map.yaml") || !Package.Contains("map.bin")) if (!Package.Contains("map.yaml") || !Package.Contains("map.bin"))
throw new InvalidDataException($"Not a valid map\n File: {package.Name}"); throw new InvalidDataException($"Not a valid map\n File: {package.Name}");
var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), $"{package.Name}:map.yaml")); var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), package.Name));
foreach (var field in YamlFields) foreach (var field in YamlFields)
field.Deserialize(this, yaml); field.Deserialize(this, yaml.Nodes);
if (MapFormat < SupportedMapFormat) if (MapFormat < SupportedMapFormat)
throw new InvalidDataException($"Map format {MapFormat} is not supported.\n File: {package.Name}"); throw new InvalidDataException($"Map format {MapFormat} is not supported.\n File: {package.Name}");
PlayerDefinitions = yaml.NodeWithKeyOrDefault("Players")?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty; PlayerDefinitions = MiniYaml.NodesOrEmpty(yaml, "Players");
ActorDefinitions = yaml.NodeWithKeyOrDefault("Actors")?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty; ActorDefinitions = MiniYaml.NodesOrEmpty(yaml, "Actors");
Grid = modData.Manifest.Get<MapGrid>(); Grid = modData.Manifest.Get<MapGrid>();
@@ -611,7 +607,7 @@ namespace OpenRA
// Odd-height ramps get bumped up a level to the next even height layer // Odd-height ramps get bumped up a level to the next even height layer
if ((height & 1) == 1 && Ramp[uv] != 0) if ((height & 1) == 1 && Ramp[uv] != 0)
height++; height += 1;
var candidates = new List<PPos>(); var candidates = new List<PPos>();
@@ -650,24 +646,21 @@ namespace OpenRA
foreach (var file in Package.Contents) foreach (var file in Package.Contents)
toPackage.Update(file, Package.GetStream(file).ReadAllBytes()); toPackage.Update(file, Package.GetStream(file).ReadAllBytes());
void UpdatePackage(string filename, byte[] data)
{
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);
}
}
if (!LockPreview) if (!LockPreview)
UpdatePackage("map.png", SavePreview()); {
var previewData = SavePreview();
if (Package != toPackage || !Enumerable.SequenceEqual(previewData, Package.GetStream("map.png").ReadAllBytes()))
toPackage.Update("map.png", previewData);
}
// Update the package with the new map data // Update the package with the new map data
UpdatePackage("map.yaml", Encoding.UTF8.GetBytes(root.WriteToString())); var textData = Encoding.UTF8.GetBytes(root.WriteToString());
UpdatePackage("map.bin", SaveBinaryData()); if (Package != toPackage || !Enumerable.SequenceEqual(textData, Package.GetStream("map.yaml").ReadAllBytes()))
toPackage.Update("map.yaml", textData);
var binaryData = SaveBinaryData();
if (Package != toPackage || !Enumerable.SequenceEqual(binaryData, Package.GetStream("map.bin").ReadAllBytes()))
toPackage.Update("map.bin", binaryData);
Package = toPackage; Package = toPackage;
@@ -688,16 +681,16 @@ namespace OpenRA
writer.Write((ushort)MapSize.Y); writer.Write((ushort)MapSize.Y);
// Data offsets // Data offsets
const int TilesOffset = 17; var tilesOffset = 17;
var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.X * MapSize.Y + 17 : 0; var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.X * MapSize.Y + 17 : 0;
var resourcesOffset = (Grid.MaximumTerrainHeight > 0 ? 4 : 3) * MapSize.X * MapSize.Y + 17; 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)heightsOffset);
writer.Write((uint)resourcesOffset); writer.Write((uint)resourcesOffset);
// Tile data // Tile data
if (TilesOffset != 0) if (tilesOffset != 0)
{ {
for (var i = 0; i < MapSize.X; i++) for (var i = 0; i < MapSize.X; i++)
{ {
@@ -779,10 +772,19 @@ namespace OpenRA
if (Grid.MaximumTerrainHeight > 0) if (Grid.MaximumTerrainHeight > 0)
{ {
(top, bottom) = GetCellSpaceBounds(); // 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 (top == int.MaxValue || bottom == int.MinValue) if (allBottom.Count > 0)
throw new InvalidDataException("The map has invalid boundaries"); bottom = Math.Max(bottom, allBottom.MaxBy(uv => uv.V).V);
}
} }
else else
{ {
@@ -799,7 +801,7 @@ namespace OpenRA
bitmapWidth = 2 * bitmapWidth - 1; bitmapWidth = 2 * bitmapWidth - 1;
var stride = bitmapWidth * 4; var stride = bitmapWidth * 4;
const int PxStride = 4; var pxStride = 4;
var minimapData = new byte[stride * height]; var minimapData = new byte[stride * height];
(Color Left, Color Right) terrainColor = default; (Color Left, Color Right) terrainColor = default;
@@ -821,10 +823,10 @@ namespace OpenRA
{ {
// Odd rows are shifted right by 1px // Odd rows are shifted right by 1px
var dx = uv.V & 1; var dx = uv.V & 1;
var xOffset = PxStride * (2 * x + dx); var xOffset = pxStride * (2 * x + dx);
if (x + dx > 0) if (x + dx > 0)
{ {
var z = y * stride + xOffset - PxStride; var z = y * stride + xOffset - pxStride;
var c = actorColor.A == 0 ? terrainColor.Left : actorColor; var c = actorColor.A == 0 ? terrainColor.Left : actorColor;
minimapData[z++] = c.R; minimapData[z++] = c.R;
minimapData[z++] = c.G; minimapData[z++] = c.G;
@@ -844,7 +846,7 @@ namespace OpenRA
} }
else else
{ {
var z = y * stride + PxStride * x; var z = y * stride + pxStride * x;
var c = actorColor.A == 0 ? terrainColor.Left : actorColor; var c = actorColor.A == 0 ? terrainColor.Left : actorColor;
minimapData[z++] = c.R; minimapData[z++] = c.R;
minimapData[z++] = c.G; minimapData[z++] = c.G;
@@ -858,28 +860,6 @@ namespace OpenRA
return png.Save(); 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) public bool Contains(CPos cell)
{ {
if (Grid.Type == MapGridType.RectangularIsometric) if (Grid.Type == MapGridType.RectangularIsometric)
@@ -1200,7 +1180,7 @@ namespace OpenRA
// Project this guessed cell and take the first available cell // Project this guessed cell and take the first available cell
// If it is projected outside the layer, then make another guess. // If it is projected outside the layer, then make another guess.
var allProjected = ProjectedCellsCovering(uv); var allProjected = ProjectedCellsCovering(uv);
var projected = allProjected.Length > 0 ? allProjected[0] var projected = allProjected.Length > 0 ? allProjected.First()
: new PPos(uv.U, uv.V.Clamp(Bounds.Top, Bounds.Bottom)); : new PPos(uv.U, uv.V.Clamp(Bounds.Top, Bounds.Bottom));
// Clamp the projected cell to the map area // Clamp the projected cell to the map area
@@ -1269,7 +1249,7 @@ namespace OpenRA
PPos edge; PPos edge;
if (allProjected.Length > 0) if (allProjected.Length > 0)
{ {
var puv = allProjected[0]; var puv = allProjected.First();
var horizontalBound = (puv.U - Bounds.Left < Bounds.Width / 2) ? Bounds.Left : Bounds.Right; 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; var verticalBound = (puv.V - Bounds.Top < Bounds.Height / 2) ? Bounds.Top : Bounds.Bottom;
@@ -1369,10 +1349,6 @@ namespace OpenRA
throw new ArgumentOutOfRangeException(nameof(maxRange), throw new ArgumentOutOfRangeException(nameof(maxRange),
$"The requested range ({maxRange}) cannot exceed the value of MaximumTileSearchRange ({Grid.MaximumTileSearchRange})"); $"The requested range ({maxRange}) cannot exceed the value of MaximumTileSearchRange ({Grid.MaximumTileSearchRange})");
return FindTilesInAnnulus();
IEnumerable<CPos> FindTilesInAnnulus()
{
for (var i = minRange; i <= maxRange; i++) for (var i = minRange; i <= maxRange; i++)
{ {
foreach (var offset in Grid.TilesByDistance[i]) foreach (var offset in Grid.TilesByDistance[i])
@@ -1383,7 +1359,6 @@ namespace OpenRA
} }
} }
} }
}
public IEnumerable<CPos> FindTilesInCircle(CPos center, int maxRange, bool allowOutsideBounds = false) public IEnumerable<CPos> FindTilesInCircle(CPos center, int maxRange, bool allowOutsideBounds = false)
{ {
@@ -1427,11 +1402,11 @@ namespace OpenRA
return modData.DefaultFileSystem.Exists(filename); return modData.DefaultFileSystem.Exists(filename);
} }
public bool IsExternalFile(string filename) public bool IsExternalModFile(string filename)
{ {
// Explicit package paths never refer to a map // Explicit package paths never refer to a map
if (filename.Contains('|')) if (filename.Contains('|'))
return modData.DefaultFileSystem.IsExternalFile(filename); return modData.DefaultFileSystem.IsExternalModFile(filename);
return false; return false;
} }

View File

@@ -14,7 +14,6 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using OpenRA.FileSystem; using OpenRA.FileSystem;
@@ -39,7 +38,7 @@ namespace OpenRA
readonly object syncRoot = new(); readonly object syncRoot = new();
readonly Queue<MapPreview> generateMinimap = new(); readonly Queue<MapPreview> generateMinimap = new();
public HashSet<string> StringPool { get; } = new(); public Dictionary<string, string> StringPool { get; } = new Dictionary<string, string>();
readonly List<MapDirectoryTracker> mapDirectoryTrackers = new(); readonly List<MapDirectoryTracker> mapDirectoryTrackers = new();
@@ -98,7 +97,7 @@ namespace OpenRA
? MapClassification.Unknown : Enum<MapClassification>.Parse(kv.Value); ? MapClassification.Unknown : Enum<MapClassification>.Parse(kv.Value);
IReadOnlyPackage package; IReadOnlyPackage package;
var optional = name.StartsWith('~'); var optional = name.StartsWith("~", StringComparison.Ordinal);
if (optional) if (optional)
name = name[1..]; name = name[1..];
@@ -107,7 +106,7 @@ namespace OpenRA
// HACK: If the path is inside the support directory then we may need to create it // 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 // Assume that the path is a directory if there is not an existing file with the same name
var resolved = Platform.ResolvePath(name); var resolved = Platform.ResolvePath(name);
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && !File.Exists(resolved)) if (resolved.StartsWith(Platform.SupportDir) && !File.Exists(resolved))
Directory.CreateDirectory(resolved); Directory.CreateDirectory(resolved);
package = modData.ModFiles.OpenPackage(name); package = modData.ModFiles.OpenPackage(name);
@@ -141,8 +140,7 @@ namespace OpenRA
LoadMapInternal(map, package, classification, mapGrid, oldMap, null); LoadMapInternal(map, package, classification, mapGrid, oldMap, null);
} }
void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap, void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap, IEnumerable<List<MiniYamlNode>> modDataRules)
IEnumerable<List<MiniYamlNode>> modDataRules)
{ {
IReadOnlyPackage mapPackage = null; IReadOnlyPackage mapPackage = null;
try try
@@ -155,9 +153,6 @@ namespace OpenRA
var uid = Map.ComputeUID(mapPackage); var uid = Map.ComputeUID(mapPackage);
previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type, modDataRules); previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type, modDataRules);
// Freeing the package to save memory if there is a lot of Maps
previews[uid].DisposePackage();
if (oldMap != uid) if (oldMap != uid)
{ {
LastModifiedMap = uid; LastModifiedMap = uid;
@@ -195,13 +190,13 @@ namespace OpenRA
continue; continue;
var name = kv.Key; var name = kv.Key;
var optional = name.StartsWith('~'); var optional = name.StartsWith("~", StringComparison.Ordinal);
if (optional) if (optional)
name = name[1..]; name = name[1..];
// Don't try to open the map directory in the support directory if it doesn't exist // Don't try to open the map directory in the support directory if it doesn't exist
var resolved = Platform.ResolvePath(name); var resolved = Platform.ResolvePath(name);
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && (!Directory.Exists(resolved) || !File.Exists(resolved))) if (resolved.StartsWith(Platform.SupportDir) && (!Directory.Exists(resolved) || !File.Exists(resolved)))
continue; continue;
using (var package = (IReadWritePackage)modData.ModFiles.OpenPackage(name)) using (var package = (IReadWritePackage)modData.ModFiles.OpenPackage(name))
@@ -228,8 +223,7 @@ namespace OpenRA
yield return mapPackage; yield return mapPackage;
} }
public void QueryRemoteMapDetails(string repositoryUrl, IEnumerable<string> uids, public void QueryRemoteMapDetails(string repositoryUrl, IEnumerable<string> uids, Action<MapPreview> mapDetailsReceived = null, Action<MapPreview> mapQueryFailed = null)
Action<MapPreview> mapDetailsReceived = null, Action<MapPreview> mapQueryFailed = null)
{ {
var queryUids = uids.Distinct() var queryUids = uids.Distinct()
.Where(uid => uid != null) .Where(uid => uid != null)
@@ -239,53 +233,31 @@ namespace OpenRA
.ToList(); .ToList();
foreach (var uid in queryUids) foreach (var uid in queryUids)
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null, null); previews[uid].UpdateRemoteSearch(MapStatus.Searching, null);
Task.Run(async () => Task.Run(async () =>
{ {
var client = HttpClientFactory.Create(); var client = HttpClientFactory.Create();
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var buffer = new StringBuilder();
// Limit each query to 50 maps at a time to avoid request size limits // Limit each query to 50 maps at a time to avoid request size limits
for (var i = 0; i < queryUids.Count; i += 50) for (var i = 0; i < queryUids.Count; i += 50)
{ {
var batchUids = queryUids.Skip(i).Take(50).ToList(); var batchUids = queryUids.Skip(i).Take(50).ToList();
var url = repositoryUrl + "hash/" + string.Join(",", batchUids) + "/yaml"; var url = repositoryUrl + "hash/" + string.Join(",", batchUids) + "/yaml";
using (new PerfTimer("RemoteMapDetails"))
{
try try
{ {
await using (var resultStream = await client.GetStreamAsync(url)) var httpResponseMessage = await client.GetAsync(url);
{ var result = await httpResponseMessage.Content.ReadAsStreamAsync();
using (var resultReader = new StreamReader(resultStream))
{ var yaml = MiniYaml.FromStream(result);
while (true)
{
var line = await resultReader.ReadLineAsync();
if (line == null || !line.StartsWith('\t'))
{
var yaml = MiniYaml.FromString(buffer.ToString(), url, stringPool: stringPool);
buffer.Clear();
foreach (var kv in yaml) foreach (var kv in yaml)
previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, modData.Manifest.MapCompatibility, mapDetailsReceived); previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, mapDetailsReceived);
if (line == null)
break;
}
buffer.Append(line);
buffer.Append('\n');
}
}
}
foreach (var uid in batchUids) foreach (var uid in batchUids)
{ {
var p = previews[uid]; var p = previews[uid];
if (p.Status != MapStatus.DownloadAvailable) if (p.Status != MapStatus.DownloadAvailable)
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null); p.UpdateRemoteSearch(MapStatus.Unavailable, null);
} }
} }
catch (Exception e) catch (Exception e)
@@ -297,12 +269,11 @@ namespace OpenRA
foreach (var uid in batchUids) foreach (var uid in batchUids)
{ {
var p = previews[uid]; var p = previews[uid];
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null); p.UpdateRemoteSearch(MapStatus.Unavailable, null);
mapQueryFailed?.Invoke(p); mapQueryFailed?.Invoke(p);
} }
} }
} }
}
}); });
} }
@@ -311,11 +282,11 @@ namespace OpenRA
Log.Write("debug", "MapCache.LoadAsyncInternal started"); Log.Write("debug", "MapCache.LoadAsyncInternal started");
// Milliseconds to wait on one loop when nothing to do // Milliseconds to wait on one loop when nothing to do
const int EmptyDelay = 50; var emptyDelay = 50;
// Keep the thread alive for at least 5 seconds after the last minimap generation // Keep the thread alive for at least 5 seconds after the last minimap generation
const int MaxKeepAlive = 5000 / EmptyDelay; var maxKeepAlive = 5000 / emptyDelay;
var keepAlive = MaxKeepAlive; var keepAlive = maxKeepAlive;
while (true) while (true)
{ {
@@ -335,11 +306,11 @@ namespace OpenRA
if (todo.Count == 0) if (todo.Count == 0)
{ {
Thread.Sleep(EmptyDelay); Thread.Sleep(emptyDelay);
continue; continue;
} }
else else
keepAlive = MaxKeepAlive; keepAlive = maxKeepAlive;
// Render the minimap into the shared sheet // Render the minimap into the shared sheet
foreach (var p in todo) foreach (var p in todo)
@@ -377,8 +348,8 @@ namespace OpenRA
while (this[uid].Status != MapStatus.Available) while (this[uid].Status != MapStatus.Available)
{ {
if (mapUpdates.TryGetValue(uid, out var newUid)) if (mapUpdates.ContainsKey(uid))
uid = newUid; uid = mapUpdates[uid];
else else
return null; return null;
} }
@@ -435,16 +406,10 @@ namespace OpenRA
{ {
UpdateMaps(); UpdateMaps();
var map = string.IsNullOrEmpty(initialUid) ? null : previews[initialUid]; var map = string.IsNullOrEmpty(initialUid) ? null : previews[initialUid];
if (map == null || if (map == null || map.Status != MapStatus.Available || !map.Visibility.HasFlag(MapVisibility.Lobby) || (map.Class != MapClassification.System && map.Class != MapClassification.User))
map.Status != MapStatus.Available ||
!map.Visibility.HasFlag(MapVisibility.Lobby) ||
(map.Class != MapClassification.System && map.Class != MapClassification.User))
{ {
var selected = previews.Values.Where(IsSuitableInitialMap).RandomOrDefault(random) ?? var selected = previews.Values.Where(IsSuitableInitialMap).RandomOrDefault(random) ??
previews.Values.FirstOrDefault(m => previews.Values.FirstOrDefault(m => m.Status == MapStatus.Available && m.Visibility.HasFlag(MapVisibility.Lobby) && (m.Class == MapClassification.System || m.Class == MapClassification.User));
m.Status == MapStatus.Available &&
m.Visibility.HasFlag(MapVisibility.Lobby) &&
(m.Class == MapClassification.System || m.Class == MapClassification.User));
return selected == null ? string.Empty : selected.Uid; return selected == null ? string.Empty : selected.Uid;
} }

View File

@@ -35,7 +35,7 @@ namespace OpenRA
// Check for column overflow // Check for column overflow
if (u > r.BottomRight.U) if (u > r.BottomRight.U)
{ {
v++; v += 1;
u = r.TopLeft.U; u = r.TopLeft.U;
// Check for row overflow // Check for row overflow
@@ -53,8 +53,8 @@ namespace OpenRA
} }
public MPos Current { get; private set; } public MPos Current { get; private set; }
readonly object IEnumerator.Current => Current; object IEnumerator.Current => Current;
public readonly void Dispose() { } public void Dispose() { }
} }
public MapCoordsRegion(MPos mapTopLeft, MPos mapBottomRight) public MapCoordsRegion(MPos mapTopLeft, MPos mapBottomRight)

View File

@@ -86,7 +86,7 @@ namespace OpenRA
dirty = false; dirty = false;
foreach (var mapAction in mapActionQueue) foreach (var mapAction in mapActionQueue)
{ {
var map = mapcache.FirstOrDefault(x => x.PackageName == mapAction.Key && x.Status == MapStatus.Available); var map = mapcache.FirstOrDefault(x => x.Package?.Name == mapAction.Key && x.Status == MapStatus.Available);
if (map != null) if (map != null)
{ {
if (mapAction.Value == MapAction.Delete) if (mapAction.Value == MapAction.Delete)

View File

@@ -116,12 +116,12 @@ namespace OpenRA
public readonly WVec[] SubCellOffsets = public readonly WVec[] SubCellOffsets =
{ {
new(0, 0, 0), // full cell - index 0 new WVec(0, 0, 0), // full cell - index 0
new(-299, -256, 0), // top left - index 1 new WVec(-299, -256, 0), // top left - index 1
new(256, -256, 0), // top right - index 2 new WVec(256, -256, 0), // top right - index 2
new(0, 0, 0), // center - index 3 new WVec(0, 0, 0), // center - index 3
new(-299, 256, 0), // bottom left - index 4 new WVec(-299, 256, 0), // bottom left - index 4
new(256, 256, 0), // bottom right - index 5 new WVec(256, 256, 0), // bottom right - index 5
}; };
public CellRamp[] Ramps { get; } public CellRamp[] Ramps { get; }

View File

@@ -59,7 +59,6 @@ namespace OpenRA
public readonly string rules; public readonly string rules;
public readonly string players_block; public readonly string players_block;
public readonly int mapformat; public readonly int mapformat;
public readonly string game_mod;
} }
public sealed class MapPreview : IDisposable, IReadOnlyFileSystem public sealed class MapPreview : IDisposable, IReadOnlyFileSystem
@@ -90,9 +89,8 @@ namespace OpenRA
public MiniYaml NotificationDefinitions; public MiniYaml NotificationDefinitions;
public MiniYaml SequenceDefinitions; public MiniYaml SequenceDefinitions;
public MiniYaml ModelSequenceDefinitions; public MiniYaml ModelSequenceDefinitions;
public MiniYaml FluentMessageDefinitions;
public FluentBundle FluentBundle { get; private set; } public Translation Translation { get; private set; }
public ActorInfo WorldActorInfo { get; private set; } public ActorInfo WorldActorInfo { get; private set; }
public ActorInfo PlayerActorInfo { get; private set; } public ActorInfo PlayerActorInfo { get; private set; }
@@ -122,32 +120,13 @@ namespace OpenRA
NotificationDefinitions = LoadRuleSection(yaml, "Notifications"); NotificationDefinitions = LoadRuleSection(yaml, "Notifications");
SequenceDefinitions = LoadRuleSection(yaml, "Sequences"); SequenceDefinitions = LoadRuleSection(yaml, "Sequences");
ModelSequenceDefinitions = LoadRuleSection(yaml, "ModelSequences"); ModelSequenceDefinitions = LoadRuleSection(yaml, "ModelSequences");
FluentMessageDefinitions = LoadRuleSection(yaml, "FluentMessages");
Translation = yaml.TryGetValue("Translations", out var node) && node != null
? new Translation(Game.Settings.Player.Language, FieldLoader.GetValue<string[]>("value", node.Value), fileSystem)
: null;
try try
{ {
if (FluentMessageDefinitions != null)
{
var files = Array.Empty<string>();
if (FluentMessageDefinitions.Value != null)
files = FieldLoader.GetValue<string[]>("value", FluentMessageDefinitions.Value);
string text = null;
if (FluentMessageDefinitions.Nodes.Length > 0)
{
var builder = new StringBuilder();
foreach (var node in FluentMessageDefinitions.Nodes)
if (node.Key == "base64")
builder.Append(Encoding.UTF8.GetString(Convert.FromBase64String(node.Value.Value)));
text = builder.ToString();
}
FluentBundle = new FluentBundle(modData.Manifest.FluentCulture, files, fileSystem, text);
}
else
FluentBundle = null;
// PERF: Implement a minimal custom loader for custom world and player actors to minimize loading time // PERF: Implement a minimal custom loader for custom world and player actors to minimize loading time
// This assumes/enforces that these actor types can only inherit abstract definitions (starting with ^) // This assumes/enforces that these actor types can only inherit abstract definitions (starting with ^)
if (RuleDefinitions != null) if (RuleDefinitions != null)
@@ -160,22 +139,15 @@ namespace OpenRA
files = files.Append(mapFiles); files = files.Append(mapFiles);
} }
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var sources = var sources =
modDataRules.Select(x => x.Where(IsLoadableRuleDefinition).ToList()) modDataRules.Select(x => x.Where(IsLoadableRuleDefinition).ToList())
.Concat(files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool).Where(IsLoadableRuleDefinition).ToList())); .Concat(files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s).Where(IsLoadableRuleDefinition).ToList()));
if (RuleDefinitions.Nodes.Length > 0) if (RuleDefinitions.Nodes.Count > 0)
sources = sources.Append(RuleDefinitions.Nodes.Where(IsLoadableRuleDefinition).ToList()); sources = sources.Append(RuleDefinitions.Nodes.Where(IsLoadableRuleDefinition).ToList());
var yamlNodes = MiniYaml.Merge(sources); var yamlNodes = MiniYaml.Merge(sources);
WorldActorInfo = new ActorInfo( WorldActorInfo = new ActorInfo(modData.ObjectCreator, "world", yamlNodes.First(n => string.Equals(n.Key, "world", StringComparison.InvariantCultureIgnoreCase)).Value);
modData.ObjectCreator, PlayerActorInfo = new ActorInfo(modData.ObjectCreator, "player", yamlNodes.First(n => string.Equals(n.Key, "player", StringComparison.InvariantCultureIgnoreCase)).Value);
"world",
yamlNodes.First(n => string.Equals(n.Key, "world", StringComparison.InvariantCultureIgnoreCase)).Value);
PlayerActorInfo = new ActorInfo(
modData.ObjectCreator,
"player",
yamlNodes.First(n => string.Equals(n.Key, "player", StringComparison.InvariantCultureIgnoreCase)).Value);
return; return;
} }
} }
@@ -200,19 +172,7 @@ namespace OpenRA
readonly ModData modData; readonly ModData modData;
public readonly string Uid; public readonly string Uid;
public string PackageName { get; private set; } public IReadOnlyPackage Package { get; private set; }
IReadOnlyPackage package;
public IReadOnlyPackage Package
{
get
{
package ??= parentPackage.OpenPackage(PackageName, modData.ModFiles);
return package;
}
private set => package = value;
}
IReadOnlyPackage parentPackage; IReadOnlyPackage parentPackage;
volatile InnerData innerData; volatile InnerData innerData;
@@ -244,31 +204,16 @@ namespace OpenRA
public int DownloadPercentage { get; private set; } public int DownloadPercentage { get; private set; }
/// <summary> /// <summary>
/// Functionality mirrors <see cref="FluentProvider.GetMessage"/>, except instead of using /// Functionality mirrors <see cref="TranslationProvider.GetString"/>, except instead of using
/// loaded <see cref="Map"/>'s fluent bundle as backup, we use this <see cref="MapPreview"/>'s. /// loaded <see cref="Map"/>'s translations as backup, we use this <see cref="MapPreview"/>'s.
/// </summary> /// </summary>
public string GetMessage(string key, object[] args = null) public string GetLocalisedString(string key, IDictionary<string, object> args = null)
{ {
if (TryGetMessage(key, out var message, args)) // PERF: instead of loading mod level Translation per each MapPreview, reuse the already loaded one in TranslationProvider.
if (TranslationProvider.TryGetModString(key, out var message, args))
return message; return message;
return key; return innerData.Translation?.GetString(key, args) ?? key;
}
/// <summary>
/// Functionality mirrors <see cref="FluentProvider.TryGetMessage"/>, except instead of using
/// loaded <see cref="Map"/>'s fluent bundle as backup, we use this <see cref="MapPreview"/>'s.
/// </summary>
public bool TryGetMessage(string key, out string message, object[] args = null)
{
// PERF: instead of loading mod level strings per each MapPreview, reuse the already loaded one in FluentProvider.
if (FluentProvider.TryGetModMessage(key, out message, args))
return true;
if (innerData.FluentBundle == null)
return false;
return innerData.FluentBundle.TryGetMessage(key, out message, args);
} }
Sprite minimap; Sprite minimap;
@@ -339,7 +284,7 @@ namespace OpenRA
cache = modData.MapCache; cache = modData.MapCache;
Uid = map.Uid; Uid = map.Uid;
PackageName = map.Package.Name; Package = map.Package;
var mapPlayers = new MapPlayers(map.PlayerDefinitions); var mapPlayers = new MapPlayers(map.PlayerDefinitions);
var spawns = new List<CPos>(); var spawns = new List<CPos>();
@@ -370,7 +315,7 @@ namespace OpenRA
innerData.SetCustomRules(modData, this, new Dictionary<string, MiniYaml>() innerData.SetCustomRules(modData, this, new Dictionary<string, MiniYaml>()
{ {
{ "Rules", map.RuleDefinitions }, { "Rules", map.RuleDefinitions },
{ "FluentMessages", map.FluentMessageDefinitions }, { "Translations", map.TranslationDefinitions },
{ "Weapons", map.WeaponDefinitions }, { "Weapons", map.WeaponDefinitions },
{ "Voices", map.VoiceDefinitions }, { "Voices", map.VoiceDefinitions },
{ "Music", map.MusicDefinitions }, { "Music", map.MusicDefinitions },
@@ -380,8 +325,7 @@ namespace OpenRA
}, null); }, null);
} }
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType, IEnumerable<List<MiniYamlNode>> modDataRules)
string[] mapCompatibility, MapGridType gridType, IEnumerable<List<MiniYamlNode>> modDataRules)
{ {
Dictionary<string, MiniYaml> yaml; Dictionary<string, MiniYaml> yaml;
using (var yamlStream = p.GetStream("map.yaml")) using (var yamlStream = p.GetStream("map.yaml"))
@@ -389,10 +333,10 @@ namespace OpenRA
if (yamlStream == null) if (yamlStream == null)
throw new FileNotFoundException("Required file map.yaml not present in this map"); throw new FileNotFoundException("Required file map.yaml not present in this map");
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, $"{p.Name}:map.yaml", stringPool: cache.StringPool)).ToDictionary(); yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, "map.yaml", stringPool: cache.StringPool)).ToDictionary();
} }
PackageName = p.Name; Package = p;
parentPackage = parent; parentPackage = parent;
var newData = innerData.Clone(); var newData = innerData.Clone();
@@ -483,7 +427,7 @@ namespace OpenRA
innerData = newData; innerData = newData;
} }
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, string[] mapCompatibility, Action<MapPreview> parseMetadata = null) public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action<MapPreview> parseMetadata = null)
{ {
var newData = innerData.Clone(); var newData = innerData.Clone();
newData.Status = status; newData.Status = status;
@@ -530,19 +474,11 @@ namespace OpenRA
} }
var playersString = Encoding.UTF8.GetString(Convert.FromBase64String(r.players_block)); 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 rulesString = Encoding.UTF8.GetString(Convert.FromBase64String(r.rules));
var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString, var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString)).ToDictionary();
$"{yaml.NodeWithKey(nameof(r.rules)).Location.Name}:{nameof(r.rules)}")).ToDictionary();
newData.SetCustomRules(modData, this, rulesYaml, null); 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) catch (Exception e)
{ {
@@ -641,15 +577,10 @@ namespace OpenRA
public void Dispose() public void Dispose()
{ {
DisposePackage(); if (Package != null)
}
public void DisposePackage()
{ {
if (package != null) Package.Dispose();
{ Package = null;
package.Dispose();
package = null;
} }
} }
@@ -696,11 +627,11 @@ namespace OpenRA
return modData.DefaultFileSystem.Exists(filename); return modData.DefaultFileSystem.Exists(filename);
} }
bool IReadOnlyFileSystem.IsExternalFile(string filename) bool IReadOnlyFileSystem.IsExternalModFile(string filename)
{ {
// Explicit package paths never refer to a map // Explicit package paths never refer to a map
if (filename.Contains('|')) if (filename.Contains('|'))
return modData.DefaultFileSystem.IsExternalFile(filename); return modData.DefaultFileSystem.IsExternalModFile(filename);
return false; return false;
} }

View File

@@ -9,7 +9,6 @@
*/ */
#endregion #endregion
using System;
using OpenRA.Primitives; using OpenRA.Primitives;
namespace OpenRA namespace OpenRA
@@ -54,15 +53,5 @@ namespace OpenRA
{ {
return Bounds.Contains(uv.U, uv.V); return Bounds.Contains(uv.U, uv.V);
} }
public int IndexOf(T value, int startIndex)
{
return Array.IndexOf(Entries, value, startIndex);
}
public void SetAll(T value)
{
Array.Fill(Entries, value);
}
} }
} }

View File

@@ -93,12 +93,12 @@ namespace OpenRA
public bool MoveNext() public bool MoveNext()
{ {
u++; u += 1;
// Check for column overflow // Check for column overflow
if (u > r.BottomRight.U) if (u > r.BottomRight.U)
{ {
v++; v += 1;
u = r.TopLeft.U; u = r.TopLeft.U;
// Check for row overflow // Check for row overflow
@@ -118,8 +118,8 @@ namespace OpenRA
} }
public PPos Current { get; private set; } public PPos Current { get; private set; }
readonly object IEnumerator.Current => Current; object IEnumerator.Current => Current;
public readonly void Dispose() { } public void Dispose() { }
} }
} }
} }

View File

@@ -20,36 +20,18 @@ namespace OpenRA
{ {
public static class MiniYamlExts public static class MiniYamlExts
{ {
public static void WriteToFile(this IEnumerable<MiniYamlNode> y, string filename) public static void WriteToFile(this List<MiniYamlNode> y, string filename)
{ {
File.WriteAllLines(filename, y.ToLines().Select(x => x.TrimEnd()).ToArray()); File.WriteAllLines(filename, y.ToLines().Select(x => x.TrimEnd()).ToArray());
} }
public static string WriteToString(this IEnumerable<MiniYamlNode> y) public static string WriteToString(this List<MiniYamlNode> y)
{ {
// Remove all trailing newlines and restore the final EOF newline // Remove all trailing newlines and restore the final EOF newline
return y.ToLines().JoinWith("\n").TrimEnd('\n') + "\n"; return y.ToLines().JoinWith("\n").TrimEnd('\n') + "\n";
} }
public static IEnumerable<string> ToLines(this IEnumerable<MiniYamlNode> y) public static IEnumerable<string> ToLines(this List<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 kv in y)
foreach (var line in kv.Value.ToLines(kv.Key, kv.Comment)) foreach (var line in kv.Value.ToLines(kv.Key, kv.Comment))
@@ -61,29 +43,22 @@ namespace OpenRA
{ {
public readonly struct SourceLocation public readonly struct SourceLocation
{ {
public readonly string Name; public readonly string Filename;
public readonly int Line; public readonly int Line;
public SourceLocation(string name, int line) public SourceLocation(string filename, int line)
{ {
Name = name; Filename = filename;
Line = line; Line = line;
} }
public override string ToString() { return $"{Name}:{Line}"; } public override string ToString() { return $"{Filename}:{Line}"; }
} }
public readonly SourceLocation Location; public SourceLocation Location;
public readonly string Key; public string Key;
public readonly MiniYaml Value; public MiniYaml Value;
public readonly string Comment; public 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) public MiniYamlNode(string k, MiniYaml v, string c = null)
{ {
@@ -99,15 +74,26 @@ namespace OpenRA
} }
public MiniYamlNode(string k, string v, string c = null) public MiniYamlNode(string k, string v, string c = null)
: this(k, new MiniYaml(v, Enumerable.Empty<MiniYamlNode>()), c) { } : this(k, v, c, null) { }
public MiniYamlNode(string k, string v, IEnumerable<MiniYamlNode> n) public MiniYamlNode(string k, string v, List<MiniYamlNode> n)
: this(k, new MiniYaml(v, n), null) { } : 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() public override string ToString()
{ {
return $"{{YamlNode: {Key} @ {Location}}}"; return $"{{YamlNode: {Key} @ {Location}}}";
} }
public MiniYamlNode Clone()
{
return new MiniYamlNode(Key, Value.Clone(), Comment, Location);
}
} }
public sealed class MiniYaml public sealed class MiniYaml
@@ -115,59 +101,15 @@ namespace OpenRA
const int SpacesPerLevel = 4; const int SpacesPerLevel = 4;
static readonly Func<string, string> StringIdentity = s => s; static readonly Func<string, string> StringIdentity = s => s;
static readonly Func<MiniYaml, MiniYaml> MiniYamlIdentity = my => my; static readonly Func<MiniYaml, MiniYaml> MiniYamlIdentity = my => my;
static readonly Dictionary<string, MiniYamlNode> ConflictScratch = new(); public string Value;
public List<MiniYamlNode> Nodes;
public readonly string Value; public MiniYaml Clone()
public readonly ImmutableArray<MiniYamlNode> Nodes;
public MiniYaml WithValue(string value)
{ {
if (Value == value) var clonedNodes = new List<MiniYamlNode>(Nodes.Count);
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) foreach (var node in Nodes)
{ clonedNodes.Add(node.Clone());
if (node.Key != key) return new MiniYaml(Value, clonedNodes);
continue;
if (!first)
throw new InvalidDataException($"Duplicate key '{node.Key}' in {node.Location}");
first = false;
result = node;
}
return result;
} }
public Dictionary<string, MiniYaml> ToDictionary() public Dictionary<string, MiniYaml> ToDictionary()
@@ -183,7 +125,7 @@ namespace OpenRA
public Dictionary<TKey, TElement> ToDictionary<TKey, TElement>( public Dictionary<TKey, TElement> ToDictionary<TKey, TElement>(
Func<string, TKey> keySelector, Func<MiniYaml, TElement> elementSelector) Func<string, TKey> keySelector, Func<MiniYaml, TElement> elementSelector)
{ {
var ret = new Dictionary<TKey, TElement>(Nodes.Length); var ret = new Dictionary<TKey, TElement>(Nodes.Count);
foreach (var y in Nodes) foreach (var y in Nodes)
{ {
var key = keySelector(y.Key); var key = keySelector(y.Key);
@@ -196,27 +138,28 @@ namespace OpenRA
} }
public MiniYaml(string value) public MiniYaml(string value)
: this(value, Enumerable.Empty<MiniYamlNode>()) { } : this(value, null) { }
public MiniYaml(string value, IEnumerable<MiniYamlNode> nodes) public MiniYaml(string value, List<MiniYamlNode> nodes)
{ {
Value = value; Value = value;
Nodes = ImmutableArray.CreateRange(nodes); Nodes = nodes ?? new List<MiniYamlNode>();
} }
static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string name, bool discardCommentsAndWhitespace, HashSet<string> stringPool) public static List<MiniYamlNode> NodesOrEmpty(MiniYaml y, string s)
{ {
// YAML config often contains repeated strings for key, values, comments. var nd = y.ToDictionary();
// Pool these strings so we only need one copy of each unique string. return nd.TryGetValue(s, out var v) ? v.Nodes : new List<MiniYamlNode>();
// 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>();
var result = new List<List<MiniYamlNode>> static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string filename, bool discardCommentsAndWhitespace, Dictionary<string, string> stringPool)
{ {
new() stringPool ??= new Dictionary<string, string>();
var levels = new List<List<MiniYamlNode>>
{
new List<MiniYamlNode>()
}; };
var parsedLines = new List<(int Level, string Key, string Value, string Comment, MiniYamlNode.SourceLocation Location)>();
var lineNo = 0; var lineNo = 0;
foreach (var ll in lines) foreach (var ll in lines)
@@ -232,7 +175,7 @@ namespace OpenRA
ReadOnlySpan<char> key = default; ReadOnlySpan<char> key = default;
ReadOnlySpan<char> value = default; ReadOnlySpan<char> value = default;
ReadOnlySpan<char> comment = default; ReadOnlySpan<char> comment = default;
var location = new MiniYamlNode.SourceLocation(name, lineNo); var location = new MiniYamlNode.SourceLocation(filename, lineNo);
if (line.Length > 0) if (line.Length > 0)
{ {
@@ -263,6 +206,15 @@ 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>` // Extract key, value, comment from line as `<key>: <value>#<comment>`
// The # character is allowed in the value if escaped (\#). // The # character is allowed in the value if escaped (\#).
// Leading and trailing whitespace is always trimmed from keys. // Leading and trailing whitespace is always trimmed from keys.
@@ -284,7 +236,7 @@ namespace OpenRA
if (commentStart < 0 && line[i] == '#' && (i == 0 || line[i - 1] != '\\')) if (commentStart < 0 && line[i] == '#' && (i == 0 || line[i - 1] != '\\'))
{ {
commentStart = i + 1; commentStart = i + 1;
if (i <= keyStart + keyLength) if (commentStart <= keyLength)
keyLength = i - keyStart; keyLength = i - keyStart;
else else
valueLength = i - valueStart; valueLength = i - valueStart;
@@ -322,81 +274,46 @@ namespace OpenRA
if (!key.IsEmpty || !discardCommentsAndWhitespace) 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 keyString = key.IsEmpty ? null : key.ToString();
var valueString = value.IsEmpty ? null : value.ToString(); var valueString = value.IsEmpty ? null : value.ToString();
// Note: We need to support empty comments here to ensure that empty comments // Note: We need to support empty comments here to ensure that empty comments
// (i.e. a lone # at the end of a line) can be correctly re-serialized // (i.e. a lone # at the end of a line) can be correctly re-serialized
var commentString = comment == ReadOnlySpan<char>.Empty ? null : comment.ToString(); var commentString = comment == default ? null : comment.ToString();
keyString = keyString == null ? null : stringPool.GetOrAdd(keyString); keyString = keyString == null ? null : stringPool.GetOrAdd(keyString, keyString);
valueString = valueString == null ? null : stringPool.GetOrAdd(valueString); valueString = valueString == null ? null : stringPool.GetOrAdd(valueString, valueString);
commentString = commentString == null ? null : stringPool.GetOrAdd(commentString); commentString = commentString == null ? null : stringPool.GetOrAdd(commentString, commentString);
parsedLines.Add((level, keyString, valueString, commentString, location)); var nodes = new List<MiniYamlNode>();
levels[level].Add(new MiniYamlNode(keyString, valueString, commentString, nodes, location));
levels.Add(nodes);
} }
} }
if (parsedLines.Count > 0) foreach (var nodes in levels)
BuildCompletedSubNode(0); nodes.TrimExcess();
return result[0]; return levels[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; public static List<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = 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, HashSet<string> stringPool = null)
{ {
return FromStream(File.OpenRead(path), path, discardCommentsAndWhitespace, stringPool); return FromStream(File.OpenRead(path), path, discardCommentsAndWhitespace, stringPool);
} }
public static List<MiniYamlNode> FromStream(Stream s, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null) public static List<MiniYamlNode> FromStream(Stream s, string fileName = "<no filename available>", bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
{ {
return FromLines(s.ReadAllLinesAsMemory(), name, discardCommentsAndWhitespace, stringPool); return FromLines(s.ReadAllLinesAsMemory(), fileName, discardCommentsAndWhitespace, stringPool);
} }
public static List<MiniYamlNode> FromString(string text, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null) public static List<MiniYamlNode> FromString(string text, string fileName = "<no filename available>", bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
{ {
return FromLines(text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(s => s.AsMemory()), name, discardCommentsAndWhitespace, stringPool); return FromLines(text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(s => s.AsMemory()), fileName, discardCommentsAndWhitespace, stringPool);
} }
public static List<MiniYamlNode> Merge(IEnumerable<IReadOnlyCollection<MiniYamlNode>> sources) public static List<MiniYamlNode> Merge(IEnumerable<List<MiniYamlNode>> sources)
{ {
var sourcesList = sources.ToList(); var sourcesList = sources.ToList();
if (sourcesList.Count == 0) if (sourcesList.Count == 0)
@@ -419,41 +336,28 @@ namespace OpenRA
} }
// Resolve any top-level removals (e.g. removing whole actor blocks) // Resolve any top-level removals (e.g. removing whole actor blocks)
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value))); var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)).ToList());
var result = ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty); return ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty);
return result as List<MiniYamlNode> ?? result.ToList();
} }
static void MergeIntoResolved(MiniYamlNode overrideNode, List<MiniYamlNode> existingNodes, HashSet<string> existingNodeKeys, static void MergeIntoResolved(MiniYamlNode overrideNode, List<MiniYamlNode> existingNodes, HashSet<string> existingNodeKeys,
Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited) Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
{ {
var existingNodeIndex = -1; if (existingNodeKeys.Add(overrideNode.Key))
MiniYamlNode existingNode = null;
if (!existingNodeKeys.Add(overrideNode.Key))
{ {
existingNodeIndex = IndexOfKey(existingNodes, overrideNode.Key); existingNodes.Add(overrideNode.Clone());
existingNode = existingNodes[existingNodeIndex]; return;
} }
var value = MergePartial(existingNode?.Value, overrideNode.Value); var existingNode = existingNodes.Find(n => n.Key == overrideNode.Key);
var nodes = ResolveInherits(value, tree, inherited); existingNode.Value = MergePartial(existingNode.Value, overrideNode.Value);
if (!value.Nodes.SequenceEqual(nodes)) existingNode.Value.Nodes = ResolveInherits(existingNode.Value, tree, inherited);
value = value.WithNodes(nodes);
if (existingNode != null)
existingNodes[existingNodeIndex] = existingNode.WithValue(value);
else
existingNodes.Add(overrideNode.WithValue(value));
} }
static IReadOnlyCollection<MiniYamlNode> ResolveInherits( static List<MiniYamlNode> ResolveInherits(MiniYaml node, Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
MiniYaml node, Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
{ {
if (node.Nodes.Length == 0) var resolved = new List<MiniYamlNode>(node.Nodes.Count);
return node.Nodes; 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) foreach (var n in node.Nodes)
{ {
@@ -469,14 +373,13 @@ namespace OpenRA
} }
catch (ArgumentException) catch (ArgumentException)
{ {
throw new YamlException( throw new YamlException($"{n.Location}: Parent type `{n.Value.Value}` was already inherited by this yaml tree at {inherited[n.Value.Value]} (note: may be from a derived tree)");
$"{n.Location}: Parent type `{n.Value.Value}` was already inherited by this yaml tree at {inherited[n.Value.Value]} (note: may be from a derived tree)");
} }
foreach (var r in ResolveInherits(parent, tree, inherited)) foreach (var r in ResolveInherits(parent, tree, inherited))
MergeIntoResolved(r, resolved, resolvedKeys, tree, inherited); MergeIntoResolved(r, resolved, resolvedKeys, tree, inherited);
} }
else if (n.Key.StartsWith('-')) else if (n.Key.StartsWith("-", StringComparison.Ordinal))
{ {
var removed = n.Key[1..]; var removed = n.Key[1..];
if (resolved.RemoveAll(r => r.Key == removed) == 0) if (resolved.RemoveAll(r => r.Key == removed) == 0)
@@ -487,6 +390,7 @@ namespace OpenRA
MergeIntoResolved(n, resolved, resolvedKeys, tree, inherited); MergeIntoResolved(n, resolved, resolvedKeys, tree, inherited);
} }
resolved.TrimExcess();
return resolved; return resolved;
} }
@@ -494,11 +398,8 @@ namespace OpenRA
/// Merges any duplicate keys that are defined within the same set of nodes. /// Merges any duplicate keys that are defined within the same set of nodes.
/// Does not resolve inheritance or node removals. /// Does not resolve inheritance or node removals.
/// </summary> /// </summary>
static IReadOnlyCollection<MiniYamlNode> MergeSelfPartial(IReadOnlyCollection<MiniYamlNode> existingNodes) static List<MiniYamlNode> MergeSelfPartial(List<MiniYamlNode> existingNodes)
{ {
if (existingNodes.Count == 0)
return existingNodes;
var keys = new HashSet<string>(existingNodes.Count); var keys = new HashSet<string>(existingNodes.Count);
var ret = new List<MiniYamlNode>(existingNodes.Count); var ret = new List<MiniYamlNode>(existingNodes.Count);
foreach (var n in existingNodes) foreach (var n in existingNodes)
@@ -508,26 +409,19 @@ namespace OpenRA
else else
{ {
// Node with the same key has already been added: merge new node over the existing one // Node with the same key has already been added: merge new node over the existing one
var originalIndex = IndexOfKey(ret, n.Key); var original = ret.First(r => r.Key == n.Key);
var original = ret[originalIndex]; original.Value = MergePartial(original.Value, n.Value);
ret[originalIndex] = original.WithValue(MergePartial(original.Value, n.Value));
} }
} }
ret.TrimExcess();
return ret; return ret;
} }
static MiniYaml MergePartial(MiniYaml existingNodes, MiniYaml overrideNodes) static MiniYaml MergePartial(MiniYaml existingNodes, MiniYaml overrideNodes)
{ {
lock (ConflictScratch) existingNodes?.Nodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
{ overrideNodes?.Nodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
// PERF: Reuse ConflictScratch for all conflict checks to avoid allocations.
existingNodes?.Nodes.IntoDictionaryWithConflictLog(
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
overrideNodes?.Nodes.IntoDictionaryWithConflictLog(
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
ConflictScratch.Clear();
}
if (existingNodes == null) if (existingNodes == null)
return overrideNodes; return overrideNodes;
@@ -538,7 +432,7 @@ namespace OpenRA
return new MiniYaml(overrideNodes.Value ?? existingNodes.Value, MergePartial(existingNodes.Nodes, overrideNodes.Nodes)); return new MiniYaml(overrideNodes.Value ?? existingNodes.Value, MergePartial(existingNodes.Nodes, overrideNodes.Nodes));
} }
static IReadOnlyCollection<MiniYamlNode> MergePartial(IReadOnlyCollection<MiniYamlNode> existingNodes, IReadOnlyCollection<MiniYamlNode> overrideNodes) static List<MiniYamlNode> MergePartial(List<MiniYamlNode> existingNodes, List<MiniYamlNode> overrideNodes)
{ {
if (existingNodes.Count == 0) if (existingNodes.Count == 0)
return overrideNodes; return overrideNodes;
@@ -558,7 +452,7 @@ namespace OpenRA
{ {
// Append Removal nodes to the result. // Append Removal nodes to the result.
// Therefore: we know the remainder of the method deals with a plain node. // Therefore: we know the remainder of the method deals with a plain node.
if (node.Key.StartsWith('-')) if (node.Key.StartsWith("-", StringComparison.Ordinal))
{ {
ret.Add(node); ret.Add(node);
return; return;
@@ -574,8 +468,9 @@ namespace OpenRA
// A Removal node is closer than the previous node. // 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. // 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. // Instead, append it so the previous node is applied, then removed, then the new node is applied.
var previousNodeIndex = LastIndexOfKey(ret, node.Key); var removalKey = $"-{node.Key}";
var previousRemovalNodeIndex = LastIndexOfKey(ret, $"-{node.Key}"); var previousNodeIndex = ret.FindLastIndex(n => n.Key == node.Key);
var previousRemovalNodeIndex = ret.FindLastIndex(n => n.Key == removalKey);
if (previousRemovalNodeIndex != -1 && previousRemovalNodeIndex > previousNodeIndex) if (previousRemovalNodeIndex != -1 && previousRemovalNodeIndex > previousNodeIndex)
{ {
ret.Add(node); ret.Add(node);
@@ -584,30 +479,13 @@ namespace OpenRA
// A previous node is present with no intervening Removal. // A previous node is present with no intervening Removal.
// We should merge the new one into it, in place. // We should merge the new one into it, in place.
ret[previousNodeIndex] = node.WithValue(MergePartial(ret[previousNodeIndex].Value, node.Value)); ret[previousNodeIndex] = new MiniYamlNode(node.Key, MergePartial(ret[previousNodeIndex].Value, node.Value), node.Comment, node.Location);
} }
ret.TrimExcess();
return ret; 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) public IEnumerable<string> ToLines(string key, string comment = null)
{ {
var hasKey = !string.IsNullOrEmpty(key); var hasKey = !string.IsNullOrEmpty(key);
@@ -630,100 +508,14 @@ namespace OpenRA
files = files.Append(mapFiles); files = files.Append(mapFiles);
} }
var stringPool = new HashSet<string>(); // Reuse common strings in YAML var yaml = files.Select(s => FromStream(fileSystem.Open(s), s));
IEnumerable<IReadOnlyCollection<MiniYamlNode>> yaml = files.Select(s => FromStream(fileSystem.Open(s), s, stringPool: stringPool)); if (mapRules != null && mapRules.Nodes.Count > 0)
if (mapRules != null && mapRules.Nodes.Length > 0)
yaml = yaml.Append(mapRules.Nodes); yaml = yaml.Append(mapRules.Nodes);
return Merge(yaml); 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] [Serializable]
public class YamlException : Exception public class YamlException : Exception
{ {

View File

@@ -33,10 +33,9 @@ namespace OpenRA
public readonly ISpriteLoader[] SpriteLoaders; public readonly ISpriteLoader[] SpriteLoaders;
public readonly ITerrainLoader TerrainLoader; public readonly ITerrainLoader TerrainLoader;
public readonly ISpriteSequenceLoader SpriteSequenceLoader; public readonly ISpriteSequenceLoader SpriteSequenceLoader;
public readonly IModelSequenceLoader ModelSequenceLoader;
public readonly IVideoLoader[] VideoLoaders; public readonly IVideoLoader[] VideoLoaders;
public readonly HotkeyManager Hotkeys; public readonly HotkeyManager Hotkeys;
public readonly IFileSystemLoader FileSystemLoader;
public ILoadScreen LoadScreen { get; } public ILoadScreen LoadScreen { get; }
public CursorProvider CursorProvider { get; private set; } public CursorProvider CursorProvider { get; private set; }
public FS ModFiles; public FS ModFiles;
@@ -56,17 +55,11 @@ namespace OpenRA
Manifest = new Manifest(mod.Id, mod.Package); Manifest = new Manifest(mod.Id, mod.Package);
ObjectCreator = new ObjectCreator(Manifest, mods); ObjectCreator = new ObjectCreator(Manifest, mods);
PackageLoaders = ObjectCreator.GetLoaders<IPackageLoader>(Manifest.PackageFormats, "package"); PackageLoaders = ObjectCreator.GetLoaders<IPackageLoader>(Manifest.PackageFormats, "package");
ModFiles = new FS(mod.Id, mods, PackageLoaders); ModFiles = new FS(mod.Id, mods, PackageLoaders);
ModFiles.LoadFromManifest(Manifest);
FileSystemLoader = ObjectCreator.GetLoader<IFileSystemLoader>(Manifest.FileSystem.Value, "filesystem");
FieldLoader.Load(FileSystemLoader, Manifest.FileSystem);
FileSystemLoader.Mount(ModFiles, ObjectCreator);
ModFiles.TrimExcess();
Manifest.LoadCustomData(ObjectCreator); Manifest.LoadCustomData(ObjectCreator);
FluentProvider.Initialize(this, DefaultFileSystem);
if (useLoadScreen) if (useLoadScreen)
{ {
LoadScreen = ObjectCreator.CreateObject<ILoadScreen>(Manifest.LoadScreen.Value); LoadScreen = ObjectCreator.CreateObject<ILoadScreen>(Manifest.LoadScreen.Value);
@@ -97,6 +90,15 @@ namespace OpenRA
SpriteSequenceLoader = (ISpriteSequenceLoader)sequenceCtor.Invoke(new[] { this }); 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); Hotkeys = new HotkeyManager(ModFiles, Game.Settings.Keys, Manifest);
defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this)); defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this));
@@ -132,7 +134,7 @@ namespace OpenRA
// horribly when you use ModData in unexpected ways. // horribly when you use ModData in unexpected ways.
ChromeMetrics.Initialize(this); ChromeMetrics.Initialize(this);
ChromeProvider.Initialize(this); ChromeProvider.Initialize(this);
FluentProvider.Initialize(this, fileSystem); TranslationProvider.Initialize(this, fileSystem);
Game.Sound.Initialize(SoundLoaders, fileSystem); Game.Sound.Initialize(SoundLoaders, fileSystem);
@@ -166,8 +168,7 @@ namespace OpenRA
public List<MiniYamlNode>[] GetRulesYaml() public List<MiniYamlNode>[] GetRulesYaml()
{ {
var stringPool = new HashSet<string>(); // Reuse common strings in YAML return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s)).ToArray();
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s, stringPool: stringPool)).ToArray();
} }
public void Dispose() public void Dispose()
@@ -198,9 +199,4 @@ namespace OpenRA
/// <summary>Called when the engine expects to connect to a server/replay or load the shellmap.</summary> /// <summary>Called when the engine expects to connect to a server/replay or load the shellmap.</summary>
void StartGame(Arguments args); void StartGame(Arguments args);
} }
public interface IFileSystemLoader
{
void Mount(FS fileSystem, ObjectCreator objectCreator);
}
} }

View File

@@ -260,15 +260,15 @@ namespace OpenRA.Network
try try
{ {
var ms = new MemoryStream(); var ms = new MemoryStream();
ms.Write(packet.Length); ms.WriteArray(BitConverter.GetBytes(packet.Length));
ms.Write(packet); ms.WriteArray(packet);
foreach (var s in queuedSyncPackets) foreach (var s in queuedSyncPackets)
{ {
var q = OrderIO.SerializeSync(s); var q = OrderIO.SerializeSync(s);
ms.Write(q.Length); ms.WriteArray(BitConverter.GetBytes(q.Length));
ms.Write(q); ms.WriteArray(q);
sentSync.Enqueue(s); sentSync.Enqueue(s);
} }

View File

@@ -122,10 +122,10 @@ namespace OpenRA.Network
LastSyncFrame = rs.ReadInt32(); LastSyncFrame = rs.ReadInt32();
lastSyncPacket = rs.ReadBytes(Order.SyncHashOrderLength); lastSyncPacket = rs.ReadBytes(Order.SyncHashOrderLength);
var globalSettings = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:globalSettings"); var globalSettings = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
GlobalSettings = Session.Global.Deserialize(globalSettings[0].Value); GlobalSettings = Session.Global.Deserialize(globalSettings[0].Value);
var slots = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:slots"); var slots = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
Slots = new Dictionary<string, Session.Slot>(); Slots = new Dictionary<string, Session.Slot>();
foreach (var s in slots) foreach (var s in slots)
{ {
@@ -133,7 +133,7 @@ namespace OpenRA.Network
Slots.Add(slot.PlayerReference, slot); Slots.Add(slot.PlayerReference, slot);
} }
var slotClients = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:slotClients"); var slotClients = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
SlotClients = new Dictionary<string, SlotClient>(); SlotClients = new Dictionary<string, SlotClient>();
foreach (var s in slotClients) foreach (var s in slotClients)
{ {
@@ -144,9 +144,9 @@ namespace OpenRA.Network
if (rs.Position != traitDataOffset || rs.ReadInt32() != TraitDataMarker) if (rs.Position != traitDataOffset || rs.ReadInt32() != TraitDataMarker)
throw new InvalidDataException("Invalid orasav file"); throw new InvalidDataException("Invalid orasav file");
var traitData = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:traitData"); var traitData = MiniYaml.FromString(rs.ReadString(Encoding.UTF8, Connection.MaxOrderLength));
foreach (var td in traitData) foreach (var td in traitData)
TraitData.Add(Exts.ParseInt32Invariant(td.Key), td.Value); TraitData.Add(int.Parse(td.Key), td.Value);
rs.Seek(0, SeekOrigin.Begin); rs.Seek(0, SeekOrigin.Begin);
ordersStream.Write(rs.ReadBytes(metadataOffset), 0, metadataOffset); ordersStream.Write(rs.ReadBytes(metadataOffset), 0, metadataOffset);
@@ -226,10 +226,10 @@ namespace OpenRA.Network
clientSlot = firstBotSlotIndex; clientSlot = firstBotSlotIndex;
} }
ordersStream.Write(data.Length + 8); ordersStream.WriteArray(BitConverter.GetBytes(data.Length + 8));
ordersStream.Write(frame); ordersStream.WriteArray(BitConverter.GetBytes(frame));
ordersStream.Write(clientSlot); ordersStream.WriteArray(BitConverter.GetBytes(clientSlot));
ordersStream.Write(data); ordersStream.WriteArray(data);
LastOrdersFrame = frame; LastOrdersFrame = frame;
} }
@@ -238,7 +238,7 @@ namespace OpenRA.Network
// Send the trait data first to guarantee that it is available when needed // Send the trait data first to guarantee that it is available when needed
foreach (var kv in TraitData) foreach (var kv in TraitData)
{ {
var data = new List<MiniYamlNode>() { new(kv.Key.ToStringInvariant(), kv.Value) }.WriteToString(); var data = new List<MiniYamlNode>() { new MiniYamlNode(kv.Key.ToString(), kv.Value) }.WriteToString();
packetFn(0, 0, Order.FromTargetString("SaveTraitData", data, true).Serialize()); packetFn(0, 0, Order.FromTargetString("SaveTraitData", data, true).Serialize());
} }
@@ -288,35 +288,35 @@ namespace OpenRA.Network
{ {
ordersStream.Seek(0, SeekOrigin.Begin); ordersStream.Seek(0, SeekOrigin.Begin);
ordersStream.CopyTo(file); ordersStream.CopyTo(file);
file.Write(MetadataMarker); file.Write(BitConverter.GetBytes(MetadataMarker), 0, 4);
file.Write(LastOrdersFrame); file.Write(BitConverter.GetBytes(LastOrdersFrame), 0, 4);
file.Write(LastSyncFrame); file.Write(BitConverter.GetBytes(LastSyncFrame), 0, 4);
file.Write(lastSyncPacket, 0, Order.SyncHashOrderLength); file.Write(lastSyncPacket, 0, Order.SyncHashOrderLength);
var globalSettingsNodes = new List<MiniYamlNode>() { GlobalSettings.Serialize() }; var globalSettingsNodes = new List<MiniYamlNode>() { GlobalSettings.Serialize() };
file.WriteLengthPrefixedString(Encoding.UTF8, globalSettingsNodes.WriteToString()); file.WriteString(Encoding.UTF8, globalSettingsNodes.WriteToString());
var slotNodes = Slots var slotNodes = Slots
.Select(s => s.Value.Serialize()) .Select(s => s.Value.Serialize())
.ToList(); .ToList();
file.WriteLengthPrefixedString(Encoding.UTF8, slotNodes.WriteToString()); file.WriteString(Encoding.UTF8, slotNodes.WriteToString());
var slotClientNodes = SlotClients var slotClientNodes = SlotClients
.Select(s => s.Value.Serialize(s.Key)) .Select(s => s.Value.Serialize(s.Key))
.ToList(); .ToList();
file.WriteLengthPrefixedString(Encoding.UTF8, slotClientNodes.WriteToString()); file.WriteString(Encoding.UTF8, slotClientNodes.WriteToString());
var traitDataOffset = file.Length; var traitDataOffset = file.Length;
file.Write(TraitDataMarker); file.Write(BitConverter.GetBytes(TraitDataMarker), 0, 4);
var traitDataNodes = TraitData var traitDataNodes = TraitData
.Select(kv => new MiniYamlNode(kv.Key.ToStringInvariant(), kv.Value)) .Select(kv => new MiniYamlNode(kv.Key.ToString(), kv.Value))
.ToList(); .ToList();
file.WriteLengthPrefixedString(Encoding.UTF8, traitDataNodes.WriteToString()); file.WriteString(Encoding.UTF8, traitDataNodes.WriteToString());
file.Write((int)ordersStream.Length); file.Write(BitConverter.GetBytes(ordersStream.Length), 0, 4);
file.Write((int)traitDataOffset); file.Write(BitConverter.GetBytes(traitDataOffset), 0, 4);
file.Write(EOFMarker); file.Write(BitConverter.GetBytes(EOFMarker), 0, 4);
} }
} }
} }

View File

@@ -140,7 +140,7 @@ namespace OpenRA.Network
static object LoadClients(MiniYaml yaml) static object LoadClients(MiniYaml yaml)
{ {
var clients = new List<GameClient>(); var clients = new List<GameClient>();
var clientsNode = yaml.NodeWithKeyOrDefault("Clients"); var clientsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Clients");
if (clientsNode != null) if (clientsNode != null)
{ {
var regex = new Regex(@"Client@\d+"); var regex = new Regex(@"Client@\d+");
@@ -159,7 +159,7 @@ namespace OpenRA.Network
// Games advertised using the old API used a single Mods field // Games advertised using the old API used a single Mods field
if (Mod == null || Version == null) if (Mod == null || Version == null)
{ {
var modsNode = yaml.NodeWithKeyOrDefault("Mods"); var modsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Mods");
if (modsNode != null) if (modsNode != null)
{ {
var modVersion = modsNode.Value.Value.Split('@'); var modVersion = modsNode.Value.Value.Split('@');
@@ -169,7 +169,8 @@ namespace OpenRA.Network
} }
// Games advertised using the old API calculated the play time locally // Games advertised using the old API calculated the play time locally
if (State == 2 && PlayTime < 0 && DateTime.TryParse(Started, out var startTime)) if (State == 2 && PlayTime < 0)
if (DateTime.TryParse(Started, out var startTime))
PlayTime = (int)(DateTime.UtcNow - startTime).TotalSeconds; PlayTime = (int)(DateTime.UtcNow - startTime).TotalSeconds;
var externalKey = ExternalMod.MakeKey(Mod, Version); var externalKey = ExternalMod.MakeKey(Mod, Version);
@@ -182,13 +183,13 @@ namespace OpenRA.Network
if (external != null && external.Version == Version) if (external != null && external.Version == Version)
{ {
// Use external mod registration to populate the section header // Use external mod registration to populate the section header
ModTitle = external.Id; ModTitle = external.Title;
} }
else if (Game.Mods.TryGetValue(Mod, out var mod)) else if (Game.Mods.TryGetValue(Mod, out var mod))
{ {
// Use internal mod data to populate the section header, but // Use internal mod data to populate the section header, but
// on-connect switching must use the external mod plumbing. // on-connect switching must use the external mod plumbing.
ModTitle = mod.Metadata.TitleTranslated; ModTitle = mod.Metadata.Title;
} }
else else
{ {
@@ -199,7 +200,7 @@ namespace OpenRA.Network
.FirstOrDefault(m => m.Id == Mod); .FirstOrDefault(m => m.Id == Mod);
if (guessMod != null) if (guessMod != null)
ModTitle = guessMod.Id; ModTitle = guessMod.Title;
else else
ModTitle = $"Unknown mod: {Mod}"; ModTitle = $"Unknown mod: {Mod}";
} }
@@ -216,13 +217,13 @@ namespace OpenRA.Network
Name = server.Settings.Name; Name = server.Settings.Name;
// IP address will be replaced with a real value by the master server / receiving LAN client // IP address will be replaced with a real value by the master server / receiving LAN client
Address = "0.0.0.0:" + server.Settings.ListenPort.ToStringInvariant(); Address = "0.0.0.0:" + server.Settings.ListenPort.ToString();
State = (int)server.State; State = (int)server.State;
MaxPlayers = server.LobbyInfo.Slots.Count(s => !s.Value.Closed) - server.LobbyInfo.Clients.Count(c1 => c1.Bot != null); MaxPlayers = server.LobbyInfo.Slots.Count(s => !s.Value.Closed) - server.LobbyInfo.Clients.Count(c1 => c1.Bot != null);
Map = server.Map.Uid; Map = server.Map.Uid;
Mod = manifest.Id; Mod = manifest.Id;
Version = manifest.Metadata.Version; Version = manifest.Metadata.Version;
ModTitle = manifest.Metadata.TitleTranslated; ModTitle = manifest.Metadata.Title;
ModWebsite = manifest.Metadata.Website; ModWebsite = manifest.Metadata.Website;
ModIcon32 = manifest.Metadata.WebIcon32; ModIcon32 = manifest.Metadata.WebIcon32;
Protected = !string.IsNullOrEmpty(server.Settings.Password); Protected = !string.IsNullOrEmpty(server.Settings.Password);
@@ -233,7 +234,7 @@ namespace OpenRA.Network
public string ToPOSTData(bool lanGame) public string ToPOSTData(bool lanGame)
{ {
var root = new List<MiniYamlNode>() { new("Protocol", ProtocolVersion.ToStringInvariant()) }; var root = new List<MiniYamlNode>() { new MiniYamlNode("Protocol", ProtocolVersion.ToString()) };
foreach (var field in SerializeFields) foreach (var field in SerializeFields)
root.Add(FieldSaver.SaveField(this, field)); root.Add(FieldSaver.SaveField(this, field));
@@ -242,16 +243,18 @@ namespace OpenRA.Network
// Add fields that are normally generated by the master server // 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 // 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("Id", Platform.SessionGUID.ToString()));
root.Add(new MiniYamlNode("Players", Clients.Count(c => !c.IsBot && !c.IsSpectator).ToStringInvariant())); root.Add(new MiniYamlNode("Players", Clients.Count(c => !c.IsBot && !c.IsSpectator).ToString()));
root.Add(new MiniYamlNode("Spectators", Clients.Count(c => c.IsSpectator).ToStringInvariant())); root.Add(new MiniYamlNode("Spectators", Clients.Count(c => c.IsSpectator).ToString()));
root.Add(new MiniYamlNode("Bots", Clients.Count(c => c.IsBot).ToStringInvariant())); root.Add(new MiniYamlNode("Bots", Clients.Count(c => c.IsBot).ToString()));
// Included for backwards compatibility with older clients that don't support separated Mod/Version. // Included for backwards compatibility with older clients that don't support separated Mod/Version.
root.Add(new MiniYamlNode("Mods", Mod + "@" + Version)); root.Add(new MiniYamlNode("Mods", Mod + "@" + Version));
} }
var clientsNode = new MiniYaml("", Clients.Select((c, i) => var clientsNode = new MiniYaml("");
new MiniYamlNode("Client@" + i, FieldSaver.Save(c)))); var i = 0;
foreach (var c in Clients)
clientsNode.Nodes.Add(new MiniYamlNode("Client@" + i++.ToString(), FieldSaver.Save(c)));
root.Add(new MiniYamlNode("Clients", clientsNode)); root.Add(new MiniYamlNode("Clients", clientsNode));
return new MiniYaml("", root) return new MiniYaml("", root)

View File

@@ -20,16 +20,16 @@ namespace OpenRA.Network
public string Version; public string Version;
public string AuthToken; public string AuthToken;
public static HandshakeRequest Deserialize(string data, string name) public static HandshakeRequest Deserialize(string data)
{ {
var handshake = new HandshakeRequest(); var handshake = new HandshakeRequest();
FieldLoader.Load(handshake, MiniYaml.FromString(data, name).First().Value); FieldLoader.Load(handshake, MiniYaml.FromString(data).First().Value);
return handshake; return handshake;
} }
public string Serialize() public string Serialize()
{ {
var data = new List<MiniYamlNode> { new("Handshake", FieldSaver.Save(this)) }; var data = new List<MiniYamlNode> { new MiniYamlNode("Handshake", FieldSaver.Save(this)) };
return data.WriteToString(); return data.WriteToString();
} }
} }
@@ -51,14 +51,14 @@ namespace OpenRA.Network
[FieldLoader.Ignore] [FieldLoader.Ignore]
public Session.Client Client; public Session.Client Client;
public static HandshakeResponse Deserialize(string data, string name) public static HandshakeResponse Deserialize(string data)
{ {
var handshake = new HandshakeResponse var handshake = new HandshakeResponse
{ {
Client = new Session.Client() Client = new Session.Client()
}; };
var ys = MiniYaml.FromString(data, name); var ys = MiniYaml.FromString(data);
foreach (var y in ys) foreach (var y in ys)
{ {
switch (y.Key) switch (y.Key)
@@ -79,9 +79,9 @@ namespace OpenRA.Network
{ {
var data = new List<MiniYamlNode> var data = new List<MiniYamlNode>
{ {
new("Handshake", null, new MiniYamlNode("Handshake", null,
new[] { "Mod", "Version", "Password", "Fingerprint", "AuthSignature", "OrdersProtocol" }.Select(p => FieldSaver.SaveField(this, p)).ToList()), new[] { "Mod", "Version", "Password", "Fingerprint", "AuthSignature", "OrdersProtocol" }.Select(p => FieldSaver.SaveField(this, p)).ToList()),
new("Client", FieldSaver.Save(Client)) new MiniYamlNode("Client", FieldSaver.Save(Client))
}; };
return data.WriteToString(); return data.WriteToString();

View File

@@ -11,6 +11,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Linguini.Shared.Types.Bundle; using Linguini.Shared.Types.Bundle;
namespace OpenRA.Network namespace OpenRA.Network
@@ -48,75 +50,74 @@ namespace OpenRA.Network
} }
} }
public class FluentMessage public class LocalizedMessage
{ {
public const int ProtocolVersion = 1; public const int ProtocolVersion = 1;
public readonly string Key = string.Empty; public readonly string Key = string.Empty;
[FieldLoader.LoadUsing(nameof(LoadArguments))] [FieldLoader.LoadUsing(nameof(LoadArguments))]
public readonly object[] Arguments; public readonly FluentArgument[] Arguments = Array.Empty<FluentArgument>();
public string TranslatedText { get; }
static object LoadArguments(MiniYaml yaml) static object LoadArguments(MiniYaml yaml)
{ {
var arguments = new List<object>(); var arguments = new List<FluentArgument>();
var argumentsNode = yaml.NodeWithKeyOrDefault("Arguments"); var argumentsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Arguments");
if (argumentsNode != null) if (argumentsNode != null)
{ {
foreach (var argumentNode in argumentsNode.Value.Nodes) var regex = new Regex(@"Argument@\d+");
{ foreach (var argument in argumentsNode.Value.Nodes)
var argument = FieldLoader.Load<FluentArgument>(argumentNode.Value); if (regex.IsMatch(argument.Key))
arguments.Add(argument.Key); arguments.Add(FieldLoader.Load<FluentArgument>(argument.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(number);
}
else
arguments.Add(argument.Value);
}
} }
return arguments.ToArray(); return arguments.ToArray();
} }
public FluentMessage(MiniYaml yaml) public LocalizedMessage(MiniYaml yaml)
{ {
// Let the FieldLoader do the dirty work of loading the public fields. // Let the FieldLoader do the dirty work of loading the public fields.
FieldLoader.Load(this, yaml); 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);
} }
public static string Serialize(string key, object[] args) TranslatedText = TranslationProvider.GetString(Key, argumentDictionary);
}
public static string Serialize(string key, Dictionary<string, object> arguments = null)
{ {
var root = new List<MiniYamlNode> var root = new List<MiniYamlNode>
{ {
new("Protocol", ProtocolVersion.ToStringInvariant()), new MiniYamlNode("Protocol", ProtocolVersion.ToString()),
new("Key", key), new MiniYamlNode("Key", key)
}; };
if (args != null) if (arguments != null)
{ {
var nodes = new List<MiniYamlNode>(); var argumentsNode = new MiniYaml("");
for (var i = 0; i < args.Length; i += 2) var i = 0;
{ foreach (var argument in arguments.Select(a => new FluentArgument(a.Key, a.Value)))
var argKey = args[i] as string; argumentsNode.Nodes.Add(new MiniYamlNode("Argument@" + i++, FieldSaver.Save(argument)));
if (string.IsNullOrEmpty(argKey))
throw new ArgumentException($"Expected the argument at index {i} to be a non-empty string", nameof(args));
var argValue = args[i + 1]; root.Add(new MiniYamlNode("Arguments", argumentsNode));
if (argValue == null)
throw new ArgumentNullException(nameof(args), $"Expected the argument at index {i + 1} to be a non-null value");
nodes.Add(new MiniYamlNode($"Argument@{i / 2}", FieldSaver.Save(new FluentArgument(argKey, argValue))));
}
root.Add(new MiniYamlNode("Arguments", new MiniYaml("", nodes)));
} }
return new MiniYaml("", root) return new MiniYaml("", root)
.ToLines("FluentMessage") .ToLines("LocalizedMessage")
.JoinWith("\n"); .JoinWith("\n");
} }
} }

View File

@@ -71,8 +71,8 @@ namespace OpenRA.Network
} }
catch (Exception e) catch (Exception e)
{ {
Log.Write("nat", "Port forwarding failed."); Console.WriteLine("Port forwarding failed: {0}", e.Message);
Log.Write("nat", e); Log.Write("nat", e.StackTrace);
return false; return false;
} }
@@ -90,8 +90,8 @@ namespace OpenRA.Network
} }
catch (Exception e) catch (Exception e)
{ {
Log.Write("nat", "Port removal failed."); Console.WriteLine("Port removal failed: {0}", e.Message);
Log.Write("nat", e); Log.Write("nat", e.StackTrace);
return false; return false;
} }

View File

@@ -78,8 +78,7 @@ namespace OpenRA
readonly Target target; readonly Target target;
readonly Target visualFeedbackTarget; readonly Target visualFeedbackTarget;
Order(string orderString, Actor subject, in Target target, string targetString, bool queued, Order(string orderString, Actor subject, in Target target, string targetString, bool queued, Actor[] extraActors, CPos extraLocation, uint extraData, Actor[] groupedActors = null)
Actor[] extraActors, CPos extraLocation, uint extraData, Actor[] groupedActors = null)
{ {
OrderString = orderString ?? ""; OrderString = orderString ?? "";
Subject = subject; Subject = subject;
@@ -157,18 +156,7 @@ namespace OpenRA
else else
{ {
var pos = new WPos(r.ReadInt32(), r.ReadInt32(), r.ReadInt32()); var pos = new WPos(r.ReadInt32(), r.ReadInt32(), r.ReadInt32());
var numberOfTerrainPositions = r.ReadInt16();
if (numberOfTerrainPositions == -1)
target = Target.FromPos(pos); 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; break;
@@ -271,15 +259,7 @@ namespace OpenRA
public static Order FromGroupedOrder(Order grouped, Actor subject) public static Order FromGroupedOrder(Order grouped, Actor subject)
{ {
return new Order( return new Order(grouped.OrderString, subject, grouped.Target, grouped.TargetString, grouped.Queued, grouped.ExtraActors, grouped.ExtraLocation, grouped.ExtraData);
grouped.OrderString,
subject,
grouped.Target,
grouped.TargetString,
grouped.Queued,
grouped.ExtraActors,
grouped.ExtraLocation,
grouped.ExtraData);
} }
public static Order Command(string text) public static Order Command(string text)
@@ -408,21 +388,6 @@ namespace OpenRA
w.Write(targetState.Pos.X); w.Write(targetState.Pos.X);
w.Write(targetState.Pos.Y); w.Write(targetState.Pos.Y);
w.Write(targetState.Pos.Z); 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; break;
@@ -465,7 +430,7 @@ namespace OpenRA
public override string ToString() public override string ToString()
{ {
return $"OrderString: \"{OrderString}\" \n\t Type: \"{Type}\". \n\t Subject: \"{Subject}\". \n\t Target: \"{Target}\"." + return $"OrderString: \"{OrderString}\" \n\t Type: \"{Type}\". \n\t Subject: \"{Subject}\". \n\t Target: \"{Target}\"." +
$"\n\t TargetString: \"{TargetString}\".\n\t IsImmediate: {IsImmediate}.\n\t Player(PlayerName): {Player?.ResolvedPlayerName}\n"; $"\n\t TargetString: \"{TargetString}\".\n\t IsImmediate: {IsImmediate}.\n\t Player(PlayerName): {Player?.PlayerName}\n";
} }
} }
} }

View File

@@ -27,7 +27,7 @@ namespace OpenRA.Network
// the Order objects directly on the local client. // the Order objects directly on the local client.
data = new MemoryStream(); data = new MemoryStream();
foreach (var o in orders) foreach (var o in orders)
data.Write(o.Serialize()); data.WriteArray(o.Serialize());
} }
public OrderPacket(MemoryStream data) public OrderPacket(MemoryStream data)
@@ -55,7 +55,7 @@ namespace OpenRA.Network
public byte[] Serialize(int frame) public byte[] Serialize(int frame)
{ {
var ms = new MemoryStream((int)data.Length + 4); var ms = new MemoryStream((int)data.Length + 4);
ms.Write(frame); ms.WriteArray(BitConverter.GetBytes(frame));
data.Position = 0; data.Position = 0;
data.CopyTo(ms); data.CopyTo(ms);
@@ -83,19 +83,19 @@ namespace OpenRA.Network
public static byte[] SerializeSync((int Frame, int SyncHash, ulong DefeatState) data) public static byte[] SerializeSync((int Frame, int SyncHash, ulong DefeatState) data)
{ {
var ms = new MemoryStream(4 + Order.SyncHashOrderLength); var ms = new MemoryStream(4 + Order.SyncHashOrderLength);
ms.Write(data.Frame); ms.WriteArray(BitConverter.GetBytes(data.Frame));
ms.WriteByte((byte)OrderType.SyncHash); ms.WriteByte((byte)OrderType.SyncHash);
ms.Write(data.SyncHash); ms.WriteArray(BitConverter.GetBytes(data.SyncHash));
ms.Write(data.DefeatState); ms.WriteArray(BitConverter.GetBytes(data.DefeatState));
return ms.GetBuffer(); return ms.GetBuffer();
} }
public static byte[] SerializePingResponse(long timestamp, byte queueLength) public static byte[] SerializePingResponse(long timestamp, byte queueLength)
{ {
var ms = new MemoryStream(14); var ms = new MemoryStream(14);
ms.Write(0); ms.WriteArray(BitConverter.GetBytes(0));
ms.WriteByte((byte)OrderType.Ping); ms.WriteByte((byte)OrderType.Ping);
ms.Write(timestamp); ms.WriteArray(BitConverter.GetBytes(timestamp));
ms.WriteByte(queueLength); ms.WriteByte(queueLength);
return ms.GetBuffer(); return ms.GetBuffer();
} }

View File

@@ -22,9 +22,6 @@ namespace OpenRA.Network
{ {
const OrderPacket ClientDisconnected = null; const OrderPacket ClientDisconnected = null;
[FluentReference("frame")]
const string DesyncCompareLogs = "notification-desync-compare-logs";
readonly SyncReport syncReport; readonly SyncReport syncReport;
readonly Dictionary<int, Queue<(int Frame, OrderPacket Orders)>> pendingOrders = new(); readonly Dictionary<int, Queue<(int Frame, OrderPacket Orders)>> pendingOrders = new();
readonly Dictionary<int, (int SyncHash, ulong DefeatState)> syncForFrame = new(); readonly Dictionary<int, (int SyncHash, ulong DefeatState)> syncForFrame = new();
@@ -39,9 +36,6 @@ namespace OpenRA.Network
public string ServerError = null; public string ServerError = null;
public bool AuthenticationFailed = false; 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 NetFrameNumber { get; private set; }
public int LocalFrameNumber; public int LocalFrameNumber;
@@ -76,7 +70,7 @@ namespace OpenRA.Network
public int Client; public int Client;
public Order Order; public Order Order;
public override readonly string ToString() public override string ToString()
{ {
return $"ClientId: {Client} {Order}"; return $"ClientId: {Client} {Order}";
} }
@@ -91,7 +85,7 @@ namespace OpenRA.Network
World.OutOfSync(); World.OutOfSync();
IsOutOfSync = true; IsOutOfSync = true;
TextNotificationsManager.AddSystemLine(DesyncCompareLogs, "frame", frame); TextNotificationsManager.AddSystemLine($"Out of sync in frame {frame}.\nCompare syncreport.log with other players.");
} }
public void StartGame() public void StartGame()

View File

@@ -69,7 +69,7 @@ namespace OpenRA.Network
if (o.OrderString == "StartGame") if (o.OrderString == "StartGame")
IsValid = true; IsValid = true;
else if (o.OrderString == "SyncInfo" && !IsValid) else if (o.OrderString == "SyncInfo" && !IsValid)
LobbyInfo = Session.Deserialize(o.TargetString, o.OrderString); LobbyInfo = Session.Deserialize(o.TargetString);
} }
} }
} }

View File

@@ -67,7 +67,7 @@ namespace OpenRA.Network
} }
} }
file.Write(initialContent); file.WriteArray(initialContent);
writer = new BinaryWriter(file); writer = new BinaryWriter(file);
} }
@@ -92,8 +92,8 @@ namespace OpenRA.Network
public void ReceiveFrame(int clientID, int frame, byte[] data) public void ReceiveFrame(int clientID, int frame, byte[] data)
{ {
var ms = new MemoryStream(4 + data.Length); var ms = new MemoryStream(4 + data.Length);
ms.Write(frame); ms.WriteArray(BitConverter.GetBytes(frame));
ms.Write(data); ms.WriteArray(data);
Receive(clientID, ms.GetBuffer()); Receive(clientID, ms.GetBuffer());
} }

View File

@@ -41,13 +41,13 @@ namespace OpenRA.Network
return null; return null;
} }
public static Session Deserialize(string data, string name) public static Session Deserialize(string data)
{ {
try try
{ {
var session = new Session(); var session = new Session();
var nodes = MiniYaml.FromString(data, name); var nodes = MiniYaml.FromString(data);
foreach (var node in nodes) foreach (var node in nodes)
{ {
var strings = node.Key.Split('@'); var strings = node.Key.Split('@');
@@ -227,7 +227,7 @@ namespace OpenRA.Network
{ {
var gs = FieldLoader.Load<Global>(data); var gs = FieldLoader.Load<Global>(data);
var optionsNode = data.NodeWithKeyOrDefault("Options"); var optionsNode = data.Nodes.FirstOrDefault(n => n.Key == "Options");
if (optionsNode != null) if (optionsNode != null)
foreach (var n in optionsNode.Value.Nodes) foreach (var n in optionsNode.Value.Nodes)
gs.LobbyOptions[n.Key] = FieldLoader.Load<LobbyOptionState>(n.Value); gs.LobbyOptions[n.Key] = FieldLoader.Load<LobbyOptionState>(n.Value);
@@ -238,9 +238,8 @@ namespace OpenRA.Network
public MiniYamlNode Serialize() public MiniYamlNode Serialize()
{ {
var data = new MiniYamlNode("GlobalSettings", FieldSaver.Save(this)); var data = new MiniYamlNode("GlobalSettings", FieldSaver.Save(this));
var options = LobbyOptions.Select(kv => new MiniYamlNode(kv.Key, FieldSaver.Save(kv.Value))); var options = LobbyOptions.Select(kv => new MiniYamlNode(kv.Key, FieldSaver.Save(kv.Value))).ToList();
data = data.WithValue(data.Value.WithNodesAppended( data.Value.Nodes.Add(new MiniYamlNode("Options", new MiniYaml(null, options)));
new[] { new MiniYamlNode("Options", new MiniYaml(null, options)) }));
return data; return data;
} }
@@ -265,7 +264,7 @@ namespace OpenRA.Network
{ {
var sessionData = new List<MiniYamlNode>() var sessionData = new List<MiniYamlNode>()
{ {
new("DisabledSpawnPoints", FieldSaver.FormatValue(DisabledSpawnPoints)) new MiniYamlNode("DisabledSpawnPoints", FieldSaver.FormatValue(DisabledSpawnPoints))
}; };
foreach (var client in Clients) foreach (var client in Clients)

View File

@@ -122,7 +122,7 @@ namespace OpenRA.Network
Log.Write("sync", $"Player: {Game.Settings.Player.Name} ({Platform.CurrentPlatform} {Environment.OSVersion} {Platform.RuntimeVersion})"); Log.Write("sync", $"Player: {Game.Settings.Player.Name} ({Platform.CurrentPlatform} {Environment.OSVersion} {Platform.RuntimeVersion})");
if (Game.IsHost) if (Game.IsHost)
Log.Write("sync", "Player is host."); Log.Write("sync", "Player is host.");
Log.Write("sync", $"Game ID: {orderManager.LobbyInfo.GlobalSettings.GameUid} (Mod: {mod.TitleTranslated} at Version {mod.Version})"); Log.Write("sync", $"Game ID: {orderManager.LobbyInfo.GlobalSettings.GameUid} (Mod: {mod.Title} at Version {mod.Version})");
Log.Write("sync", $"Sync for net frame {r.Frame} -------------"); Log.Write("sync", $"Sync for net frame {r.Frame} -------------");
Log.Write("sync", $"SharedRandom: {r.SyncedRandom} (#{r.TotalCount})"); Log.Write("sync", $"SharedRandom: {r.SyncedRandom} (#{r.TotalCount})");
Log.Write("sync", "Synced Traits:"); Log.Write("sync", "Synced Traits:");
@@ -201,12 +201,8 @@ namespace OpenRA.Network
public TypeInfo(Type type) public TypeInfo(Type type)
{ {
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
var fields = type.GetFields(Flags) var fields = type.GetFields(Flags).Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>());
.Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>()) var properties = type.GetProperties(Flags).Where(pi => pi.HasAttribute<SyncAttribute>());
.ToList();
var properties = type.GetProperties(Flags)
.Where(pi => pi.HasAttribute<SyncAttribute>())
.ToList();
foreach (var prop in properties) foreach (var prop in properties)
if (!prop.CanRead || prop.GetIndexParameters().Length > 0) if (!prop.CanRead || prop.GetIndexParameters().Length > 0)
@@ -304,7 +300,7 @@ namespace OpenRA.Network
public object this[int index] public object this[int index]
{ {
readonly get get
{ {
if (item2OrSentinel == Sentinel) if (item2OrSentinel == Sentinel)
return ((object[])item1OrArray)[index]; return ((object[])item1OrArray)[index];

View File

@@ -20,24 +20,6 @@ namespace OpenRA.Network
{ {
public const int ChatMessageMaxLength = 2500; public const int ChatMessageMaxLength = 2500;
[FluentReference("player")]
const string Joined = "notification-joined";
[FluentReference("player")]
const string Left = "notification-lobby-disconnected";
[FluentReference]
const string GameStarted = "notification-game-has-started";
[FluentReference]
const string GameSaved = "notification-game-saved";
[FluentReference("player")]
const string GamePaused = "notification-game-paused";
[FluentReference("player")]
const string GameUnpaused = "notification-game-unpaused";
public static int? KickVoteTarget { get; internal set; } public static int? KickVoteTarget { get; internal set; }
static Player FindPlayerByClient(this World world, Session.Client c) static Player FindPlayerByClient(this World world, Session.Client c)
@@ -56,22 +38,17 @@ namespace OpenRA.Network
TextNotificationsManager.AddSystemLine(order.TargetString); TextNotificationsManager.AddSystemLine(order.TargetString);
break; break;
// Client side resolved server message // Client side translated server message
case "FluentMessage": case "LocalizedMessage":
{ {
if (string.IsNullOrEmpty(order.TargetString)) if (string.IsNullOrEmpty(order.TargetString))
break; break;
var yaml = MiniYaml.FromString(order.TargetString, order.OrderString); var yaml = MiniYaml.FromString(order.TargetString);
foreach (var node in yaml) foreach (var node in yaml)
{ {
var message = new FluentMessage(node.Value); var localizedMessage = new LocalizedMessage(node.Value);
if (message.Key == Joined) TextNotificationsManager.AddSystemLine(localizedMessage.TranslatedText);
TextNotificationsManager.AddPlayerJoinedLine(message.Key, message.Arguments);
else if (message.Key == Left)
TextNotificationsManager.AddPlayerLeftLine(message.Key, message.Arguments);
else
TextNotificationsManager.AddSystemLine(message.Key, message.Arguments);
} }
break; break;
@@ -183,7 +160,7 @@ namespace OpenRA.Network
if (!string.IsNullOrEmpty(order.TargetString)) if (!string.IsNullOrEmpty(order.TargetString))
{ {
var data = MiniYaml.FromString(order.TargetString, order.OrderString); var data = MiniYaml.FromString(order.TargetString);
var saveLastOrdersFrame = data.FirstOrDefault(n => n.Key == "SaveLastOrdersFrame"); var saveLastOrdersFrame = data.FirstOrDefault(n => n.Key == "SaveLastOrdersFrame");
if (saveLastOrdersFrame != null) if (saveLastOrdersFrame != null)
orderManager.GameSaveLastFrame = orderManager.GameSaveLastFrame =
@@ -195,7 +172,7 @@ namespace OpenRA.Network
FieldLoader.GetValue<int>("SaveSyncFrame", saveSyncFrame.Value.Value); FieldLoader.GetValue<int>("SaveSyncFrame", saveSyncFrame.Value.Value);
} }
else else
TextNotificationsManager.AddSystemLine(GameStarted); TextNotificationsManager.AddSystemLine("The game has started.");
Game.StartGame(orderManager.LobbyInfo.GlobalSettings.Map, WorldType.Regular); Game.StartGame(orderManager.LobbyInfo.GlobalSettings.Map, WorldType.Regular);
break; break;
@@ -203,8 +180,8 @@ namespace OpenRA.Network
case "SaveTraitData": case "SaveTraitData":
{ {
var data = MiniYaml.FromString(order.TargetString, order.OrderString)[0]; var data = MiniYaml.FromString(order.TargetString)[0];
var traitIndex = Exts.ParseInt32Invariant(data.Key); var traitIndex = int.Parse(data.Key);
world?.AddGameSaveTraitData(traitIndex, data.Value); world?.AddGameSaveTraitData(traitIndex, data.Value);
@@ -213,7 +190,7 @@ namespace OpenRA.Network
case "GameSaved": case "GameSaved":
if (!orderManager.World.IsReplay) if (!orderManager.World.IsReplay)
TextNotificationsManager.AddSystemLine(GameSaved); TextNotificationsManager.AddSystemLine("Game saved");
foreach (var nsr in orderManager.World.WorldActor.TraitsImplementing<INotifyGameSaved>()) foreach (var nsr in orderManager.World.WorldActor.TraitsImplementing<INotifyGameSaved>())
nsr.GameSaved(orderManager.World); nsr.GameSaved(orderManager.World);
@@ -231,7 +208,10 @@ namespace OpenRA.Network
break; break;
if (orderManager.World.Paused != pause && world != null && world.LobbyInfo.NonBotClients.Count() > 1) if (orderManager.World.Paused != pause && world != null && world.LobbyInfo.NonBotClients.Count() > 1)
TextNotificationsManager.AddSystemLine(pause ? GamePaused : GameUnpaused, "player", client.Name); {
var pausetext = $"The game is {(pause ? "paused" : "un-paused")} by {client.Name}";
TextNotificationsManager.AddSystemLine(pausetext);
}
orderManager.World.Paused = pause; orderManager.World.Paused = pause;
orderManager.World.PredictedPaused = pause; orderManager.World.PredictedPaused = pause;
@@ -244,7 +224,7 @@ namespace OpenRA.Network
{ {
// Switch to the server's mod if we need and are able to // Switch to the server's mod if we need and are able to
var mod = Game.ModData.Manifest; var mod = Game.ModData.Manifest;
var request = HandshakeRequest.Deserialize(order.TargetString, order.OrderString); var request = HandshakeRequest.Deserialize(order.TargetString);
var externalKey = ExternalMod.MakeKey(request.Mod, request.Version); var externalKey = ExternalMod.MakeKey(request.Mod, request.Version);
if ((request.Mod != mod.Id || request.Version != mod.Metadata.Version) && if ((request.Mod != mod.Id || request.Version != mod.Metadata.Version) &&
@@ -312,7 +292,7 @@ namespace OpenRA.Network
case "SyncInfo": case "SyncInfo":
{ {
orderManager.LobbyInfo = Session.Deserialize(order.TargetString, order.OrderString); orderManager.LobbyInfo = Session.Deserialize(order.TargetString);
Game.SyncLobbyInfo(); Game.SyncLobbyInfo();
break; break;
} }
@@ -320,7 +300,7 @@ namespace OpenRA.Network
case "SyncLobbyClients": case "SyncLobbyClients":
{ {
var clients = new List<Session.Client>(); var clients = new List<Session.Client>();
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString); var nodes = MiniYaml.FromString(order.TargetString);
foreach (var node in nodes) foreach (var node in nodes)
{ {
var strings = node.Key.Split('@'); var strings = node.Key.Split('@');
@@ -336,7 +316,7 @@ namespace OpenRA.Network
case "SyncLobbySlots": case "SyncLobbySlots":
{ {
var slots = new Dictionary<string, Session.Slot>(); var slots = new Dictionary<string, Session.Slot>();
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString); var nodes = MiniYaml.FromString(order.TargetString);
foreach (var node in nodes) foreach (var node in nodes)
{ {
var strings = node.Key.Split('@'); var strings = node.Key.Split('@');
@@ -354,7 +334,7 @@ namespace OpenRA.Network
case "SyncLobbyGlobalSettings": case "SyncLobbyGlobalSettings":
{ {
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString); var nodes = MiniYaml.FromString(order.TargetString);
foreach (var node in nodes) foreach (var node in nodes)
{ {
var strings = node.Key.Split('@'); var strings = node.Key.Split('@');
@@ -368,13 +348,13 @@ namespace OpenRA.Network
case "SyncConnectionQuality": case "SyncConnectionQuality":
{ {
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString); var nodes = MiniYaml.FromString(order.TargetString);
foreach (var node in nodes) foreach (var node in nodes)
{ {
var strings = node.Key.Split('@'); var strings = node.Key.Split('@');
if (strings[0] == "ConnectionQuality") if (strings[0] == "ConnectionQuality")
{ {
var client = orderManager.LobbyInfo.Clients.FirstOrDefault(c => c.Index == Exts.ParseInt32Invariant(strings[1])); var client = orderManager.LobbyInfo.Clients.FirstOrDefault(c => c.Index == int.Parse(strings[1]));
if (client != null) if (client != null)
client.ConnectionQuality = FieldLoader.GetValue<Session.ConnectionQuality>("ConnectionQuality", node.Value.Value); client.ConnectionQuality = FieldLoader.GetValue<Session.ConnectionQuality>("ConnectionQuality", node.Value.Value);
} }
@@ -383,12 +363,6 @@ namespace OpenRA.Network
break; break;
} }
case "SyncMapPool":
{
orderManager.ServerMapPool = FieldLoader.GetValue<HashSet<string>>("SyncMapPool", order.TargetString);
break;
}
default: default:
{ {
if (world == null) if (world == null)

Some files were not shown because too many files have changed in this diff Show More