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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -40,10 +40,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Install .NET 6.0
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'
@@ -53,25 +53,27 @@ jobs:
- name: Package AppImages
run: |
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"
- name: Upload Packages
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
gh release upload ${{ github.ref_name }} build/linux/*
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
overwrite: true
file_glob: true
file: build/linux/*
macos:
name: macOS Disk Image
runs-on: macos-13
runs-on: macos-11
steps:
- name: Clone Repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Install .NET 6.0
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'
@@ -90,21 +92,23 @@ jobs:
./packaging/macos/buildpackage.sh "${GIT_TAG}" "${PWD}/build/macos"
- name: Upload Package
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
gh release upload ${{ github.ref_name }} build/macos/*
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
overwrite: true
file_glob: true
file: build/macos/*
windows:
name: Windows Installers
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Install .NET 6.0
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v3
with:
dotnet-version: '6.0.x'
@@ -120,8 +124,10 @@ jobs:
./packaging/windows/buildpackage.sh "${GIT_TAG}" "${PWD}/build/windows"
- name: Upload Packages
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
gh release upload ${{ github.ref_name }} build/windows/*
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
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:
* Gustas Kažukauskas (PunkPun)
@@ -97,7 +98,6 @@ Also thanks to:
* Kanar
* Kenny Hoxworth (hoxworth)
* Kevin Azzam (ChaoticMind)
* Kevin Streser
* Krishnakanth Mallik
* Kyle Smith (Smitty)
* Kyrre Soerensen (zypres)
@@ -150,7 +150,6 @@ Also thanks to:
* Teemu Nieminen (Temeez)
* Thomas Christlieb (ThomasChr)
* Tim Mylemans (gecko)
* Tinix
* Tirili
* Tomas Einarsson (Mesacer)
* Tom van Leth (tovl)
@@ -162,42 +161,61 @@ Also thanks to:
* Wojciech Walaszek (Voidwalker)
* Wuschel
Using GNU FreeFont distributed under the GNU GPL terms.
Using GNU FreeFont distributed under the GNU GPL
terms.
Using Simple DirectMedia Layer distributed under the terms of the zlib license.
Using Simple DirectMedia Layer distributed under
the terms of the zlib license.
Using FreeType distributed under the terms of the FreeType License.
Using FreeType distributed under the terms of the
FreeType License.
Using OpenAL Soft distributed under the GNU LGPL.
Using SDL2-CS and OpenAL-CS created by Ethan Lee and released under the zlib license.
Using SDL2-CS and OpenAL-CS created by Ethan
Lee and released under the zlib license.
Using Eluant created by Chris Howie and released under the MIT license.
Using Eluant created by Chris Howie and released
under the MIT license.
Using FuzzyLogicLibrary (fuzzynet) by Dmitry Kaluzhny and released under the GNU GPL terms.
Using FuzzyLogicLibrary (fuzzynet) by Dmitry
Kaluzhny and released under the GNU GPL terms.
Using Mono.Nat by Alan McGovern, Ben Motmans, Nicholas Terry distributed under the MIT license.
Using Mono.Nat by Alan McGovern, Ben Motmans,
Nicholas Terry distributed under the MIT license.
Using MP3Sharp by Robert Bruke and Zane Wagner licensed under the GNU LGPL Version 3.
Using MP3Sharp by Robert Bruke and Zane Wagner
licensed under the GNU LGPL Version 3.
Using TagLib# by Stephen Shaw licensed under the GNU LGPL Version 2.1.
Using TagLib# by Stephen Shaw licensed under the
GNU LGPL Version 2.1.
Using NVorbis by Andrew Ward distributed under the MIT license.
Using NVorbis by Andrew Ward distributed under
the MIT license.
Using ICSharpCode.SharpZipLib initially by Mike Krueger and distributed under the GNU GPL terms.
Using ICSharpCode.SharpZipLib initially by Mike
Krueger and distributed under the GNU GPL terms.
Using rix0rrr.BeaconLib developed by Rico Huijbers distributed under MIT License.
Using rix0rrr.BeaconLib developed by Rico Huijbers
distributed under MIT License.
Using DiscordRichPresence developed by Lachee distributed under MIT License.
Using DiscordRichPresence developed by Lachee
distributed under MIT License.
Using Json.NET developed by James Newton-King distributed under MIT License.
Using Json.NET developed by James Newton-King
distributed under MIT License.
Using ANGLE distributed under the BS3 3-Clause license.
Using Pfim developed by Nick Babcock distributed under the MIT license.
Using Pfim developed by Nick Babcock
distributed under the MIT license.
Using Linguini by the Space Station 14 team licensed under Apache and MIT terms.
Using Linguini by the Space Station 14 team
licensed under Apache and MIT terms.
This site or product includes IP2Location LITE data available from 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
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
[version]: https://contributor-covenant.org/version/1/4/
[homepage]: http://contributor-covenant.org
[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)
* [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.

View File

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

View File

@@ -33,7 +33,7 @@
<Optimize>false</Optimize>
<!-- Enable only for Debug builds to improve compile-time performance for Release builds -->
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<!-- Enabling GenerateDocumentationFile is required for IDE0005 (Remove unnecessary import)
<!-- Enabling GenerateDocumentationFile is required for IDE0005 (Remove unnecessary import)
rule to run in command line builds. https://github.com/dotnet/roslyn/issues/41640
Enable only for Debug builds to improve compile-time performance for Release builds -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -51,11 +51,8 @@
</ItemGroup>
</Target>
<!-- StyleCop/Roslynator -->
<!-- StyleCop -->
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
<!-- Roslynator analyzers fail to run under Mono (AD0001) -->
<PackageReference Include="Roslynator.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
</ItemGroup>
</Project>

View File

@@ -7,7 +7,7 @@ Windows
=======
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)
To compile OpenRA, open the `OpenRA.sln` solution in the main folder, build it from the command-line with `dotnet` or use the Makefile analogue command `make all` scripted in PowerShell syntax.
@@ -25,7 +25,7 @@ To compile OpenRA, run `make` from the command line (or `make RUNTIME=mono` if u
The default behaviour on the x86_64 architecture is to download several pre-compiled native libraries using the Nuget packaging manager. If you prefer to use system libraries, compile instead using `make TARGETPLATFORM=unix-generic`.
If you choose to use system libraries, or your system is not x86_64, you will need to install [SDL 2](https://www.libsdl.org/download-2.0.php), [FreeType](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:

View File

@@ -153,12 +153,12 @@ tests:
############# 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),)
$(error Unable to determine new version (requires git or override of variable VERSION))
endif
@sh -c '. ./packaging/functions.sh; set_engine_version "$(VERSION)" .'
@sh -c '. ./packaging/functions.sh; set_mod_version "$(VERSION)" mods/*/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:
@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>
/// <para>
/// Called every tick to run activity logic. Returns false if the activity should
/// remain active, or true if it is complete. Cancelled activities must ensure they
/// return the actor to a consistent state before returning true.
/// </para>
/// <para>
///
/// Child activities can be queued using QueueChild, and these will be ticked
/// instead of the parent while they are active. Activities that need to run logic
/// in parallel with child activities should set ChildHasPriority to false and
/// manually call TickChildren.
/// </para>
/// <para>
///
/// Queuing one or more child activities and returning true is valid, and causes
/// the activity to be completed immediately (without ticking again) once the
/// children have completed.
/// </para>
/// </summary>
public virtual bool Tick(Actor self)
{
@@ -226,11 +222,10 @@ namespace OpenRA.Activities
}
/// <summary>
/// <para>Prints the activity tree, starting from the top or optionally from a given origin.</para>
/// <para>
/// Prints the activity tree, starting from the top or optionally from a given origin.
///
/// Call this method from any place that's called during a tick, such as the Tick() method itself or
/// the Before(First|Last)Run() methods. The origin activity will be marked in the output.
/// </para>
/// </summary>
/// <param name="self">The actor performing this activity.</param>
/// <param name="origin">Activity from which to start traversing, and which to mark. If null, mark the calling activity, and start traversal from the top.</param>

View File

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

View File

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

View File

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

View File

@@ -22,9 +22,6 @@ namespace OpenRA
// Fixed byte pattern for the OID header
static readonly byte[] OIDHeader = { 0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0 };
static readonly char[] HexUpperAlphabet = "0123456789ABCDEF".ToArray();
static readonly char[] HexLowerAlphabet = "0123456789abcdef".ToArray();
public static string PublicKeyFingerprint(RSAParameters parameters)
{
// Public key fingerprint is defined as the SHA1 of the modulus + exponent bytes
@@ -56,33 +53,33 @@ namespace OpenRA
using (var s = new MemoryStream(data))
{
// SEQUENCE
s.ReadUInt8();
s.ReadByte();
ReadTLVLength(s);
// SEQUENCE -> fixed header junk
s.ReadUInt8();
s.ReadByte();
var headerLength = ReadTLVLength(s);
s.Position += headerLength;
// SEQUENCE -> BIT_STRING
s.ReadUInt8();
s.ReadByte();
ReadTLVLength(s);
s.ReadUInt8();
s.ReadByte();
// SEQUENCE -> BIT_STRING -> SEQUENCE
s.ReadUInt8();
s.ReadByte();
ReadTLVLength(s);
// SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (modulus)
s.ReadUInt8();
s.ReadByte();
var modulusLength = ReadTLVLength(s);
s.ReadUInt8();
s.ReadByte();
var modulus = s.ReadBytes(modulusLength - 1);
// SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (exponent)
s.ReadUInt8();
s.ReadByte();
var exponentLength = ReadTLVLength(s);
s.ReadUInt8();
s.ReadByte();
var exponent = s.ReadBytes(exponentLength - 1);
return new RSAParameters
@@ -161,13 +158,13 @@ namespace OpenRA
static int ReadTLVLength(Stream s)
{
var length = s.ReadUInt8();
var length = s.ReadByte();
if (length < 0x80)
return length;
Span<byte> data = stackalloc byte[4];
s.ReadBytes(data[..Math.Min(length & 0x7F, 4)]);
return BitConverter.ToInt32(data);
var data = new byte[4];
s.ReadBytes(data, 0, Math.Min(length & 0x7F, 4));
return BitConverter.ToInt32(data.ToArray(), 0);
}
static int TripletFullLength(int dataLength)
@@ -252,44 +249,19 @@ namespace OpenRA
public static string SHA1Hash(Stream data)
{
using var csp = SHA1.Create();
return ToHex(csp.ComputeHash(data), true);
using (var csp = SHA1.Create())
return new string(csp.ComputeHash(data).SelectMany(a => a.ToString("x2")).ToArray());
}
public static string SHA1Hash(byte[] data)
{
using var csp = SHA1.Create();
return ToHex(csp.ComputeHash(data), true);
using (var csp = SHA1.Create())
return new string(csp.ComputeHash(data).SelectMany(a => a.ToString("x2")).ToArray());
}
public static string SHA1Hash(string data)
{
return SHA1Hash(Encoding.UTF8.GetBytes(data));
}
public static string ToHex(ReadOnlySpan<byte> source, bool lowerCase = false)
{
if (source.Length == 0)
return string.Empty;
// excessively avoid stack overflow if source is too large (considering that we're allocating a new string)
var buffer = source.Length <= 256 ? stackalloc char[source.Length * 2] : new char[source.Length * 2];
return ToHexInternal(source, buffer, lowerCase);
}
static string ToHexInternal(ReadOnlySpan<byte> source, Span<char> buffer, bool lowerCase)
{
var sourceIndex = 0;
var alphabet = lowerCase ? HexLowerAlphabet : HexUpperAlphabet;
for (var i = 0; i < buffer.Length; i += 2)
{
var b = source[sourceIndex++];
buffer[i] = alphabet[b >> 4];
buffer[i + 1] = alphabet[b & 0xF];
}
return new string(buffer);
}
}
}

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,6 @@ using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using OpenRA.Primitives;
namespace OpenRA
@@ -59,7 +58,7 @@ namespace OpenRA
return new MiniYaml(
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)
@@ -85,7 +84,7 @@ namespace OpenRA
// This is only for documentation generation
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
var result = new StringBuilder();
var result = "";
var dict = (System.Collections.IDictionary)v;
foreach (var kvp in dict)
{
@@ -95,10 +94,10 @@ namespace OpenRA
var formattedKey = FormatValue(key);
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)

View File

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

View File

@@ -47,8 +47,8 @@ namespace OpenRA.FileFormats
throw new NotSupportedException($"Metadata version {version} is not supported");
// Read game info (max 100K limit as a safeguard against corrupted files)
var data = fs.ReadLengthPrefixedString(Encoding.UTF8, 1024 * 100);
GameInfo = GameInformation.Deserialize(data, path);
var data = fs.ReadString(Encoding.UTF8, 1024 * 100);
GameInfo = GameInformation.Deserialize(data);
}
public void Write(BinaryWriter writer)
@@ -62,7 +62,7 @@ namespace OpenRA.FileFormats
{
// Write lobby info data
writer.Flush();
dataLength += writer.BaseStream.WriteLengthPrefixedString(Encoding.UTF8, GameInfo.Serialize());
dataLength += writer.BaseStream.WriteString(Encoding.UTF8, GameInfo.Serialize());
}
// 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 TryOpen(string filename, out Stream s);
bool Exists(string filename);
bool IsExternalFile(string filename);
bool IsExternalModFile(string filename);
}
public class FileSystem : IReadOnlyFileSystem
@@ -83,14 +83,14 @@ namespace OpenRA.FileSystem
public void Mount(string name, string explicitName = null)
{
var optional = name.StartsWith('~');
var optional = name.StartsWith("~", StringComparison.Ordinal);
if (optional)
name = name[1..];
try
{
IReadOnlyPackage package;
if (name.StartsWith('$'))
if (name.StartsWith("$", StringComparison.Ordinal))
{
name = name[1..];
@@ -109,8 +109,10 @@ namespace OpenRA.FileSystem
Mount(package, explicitName);
}
catch when (optional)
catch
{
if (!optional)
throw;
}
}
@@ -159,7 +161,9 @@ namespace OpenRA.FileSystem
explicitMounts.Remove(key);
// 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();
}
else
@@ -181,13 +185,11 @@ namespace OpenRA.FileSystem
fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
}
public void TrimExcess()
public void LoadFromManifest(Manifest manifest)
{
mountedPackages.TrimExcess();
explicitMounts.TrimExcess();
modPackages.TrimExcess();
foreach (var packages in fileIndex.Values)
packages.TrimExcess();
UnmountAll();
foreach (var kv in manifest.Packages)
Mount(kv.Key, kv.Value);
}
Stream GetFromCache(string filename)
@@ -224,11 +226,14 @@ namespace OpenRA.FileSystem
public bool TryOpen(string filename, out Stream s)
{
var explicitSplit = filename.IndexOf('|');
if (explicitSplit > 0 && explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
if (explicitSplit > 0)
{
s = explicitPackage.GetStream(filename[(explicitSplit + 1)..]);
if (s != null)
return true;
if (explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
{
s = explicitPackage.GetStream(filename[(explicitSplit + 1)..]);
if (s != null)
return true;
}
}
s = GetFromCache(filename);
@@ -257,20 +262,64 @@ namespace OpenRA.FileSystem
public bool Exists(string filename)
{
var explicitSplit = filename.IndexOf('|');
if (explicitSplit > 0 &&
explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage) &&
explicitPackage.Contains(filename[(explicitSplit + 1)..]))
return true;
if (explicitSplit > 0)
if (explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
if (explicitPackage.Contains(filename[(explicitSplit + 1)..]))
return true;
return fileIndex.ContainsKey(filename);
}
/// <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>
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)
@@ -286,8 +335,7 @@ namespace OpenRA.FileSystem
if (name == ".")
continue;
resolved = Directory.GetFileSystemEntries(resolved)
.FirstOrDefault(e => e.Equals(Path.Combine(resolved, name), StringComparison.InvariantCultureIgnoreCase));
resolved = Directory.GetFileSystemEntries(resolved).FirstOrDefault(e => e.Equals(Path.Combine(resolved, name), StringComparison.InvariantCultureIgnoreCase));
if (resolved == null)
return null;

View File

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

View File

@@ -21,7 +21,7 @@ namespace OpenRA.FileSystem
{
const uint ZipSignature = 0x04034b50;
public class ReadOnlyZipFile : IReadOnlyPackage
class ReadOnlyZipFile : IReadOnlyPackage
{
public string Name { get; protected set; }
protected ZipFile pkg;
@@ -68,7 +68,6 @@ namespace OpenRA.FileSystem
public void Dispose()
{
pkg?.Close();
GC.SuppressFinalize(this);
}
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();
@@ -114,15 +113,14 @@ namespace OpenRA.FileSystem
pkgStream.Position = 0;
pkg = new ZipFile(pkgStream);
Name = filename;
// Remove subfields that can break ZIP updating.
foreach (ZipEntry entry in pkg)
entry.ExtraData = null;
}
void Commit()
{
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)
@@ -149,7 +147,7 @@ namespace OpenRA.FileSystem
public ZipFolder(ReadOnlyZipFile parent, string path)
{
if (path.EndsWith('/'))
if (path.EndsWith("/", StringComparison.Ordinal))
path = path[..^1];
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
{
[FluentReference("filename")]
[TranslationReference("filename")]
const string SavedScreenshot = "notification-saved-screenshot";
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 AfterGameStart = () => { };
internal static void StartGame(string mapUID, WorldType type)
{
// 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.
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
// PostLoadComplete is designed for anything that should trigger at the very end of loading.
// e.g. audio notifications that the game is starting.
OrderManager.World.PostLoadComplete(worldRenderer);
AfterGameStart();
}
public static void RestartGame()
@@ -368,7 +361,22 @@ namespace OpenRA
Settings.Game.Platform = p;
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);
Sound = new Sound(platform, Settings.Sound);
@@ -395,7 +403,7 @@ namespace OpenRA
Mods = new InstalledMods(modSearchPaths, explicitModPaths);
Console.WriteLine("Internal mods:");
foreach (var mod in Mods)
Console.WriteLine($"\t{mod.Key} ({mod.Value.Metadata.Version})");
Console.WriteLine($"\t{mod.Key}: {mod.Value.Metadata.Title} ({mod.Value.Metadata.Version})");
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
@@ -408,7 +416,7 @@ namespace OpenRA
// Sanitize input from platform-specific launchers
// Process.Start requires paths to not be quoted, even if they contain spaces
if (launchPath != null && launchPath[0] == '"' && launchPath.Last() == '"')
if (launchPath != null && launchPath.First() == '"' && launchPath.Last() == '"')
launchPath = launchPath[1..^1];
// Metadata registration requires an explicit launch path
@@ -420,31 +428,11 @@ namespace OpenRA
Console.WriteLine("External mods:");
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);
}
public static IPlatform CreatePlatform(string platformName)
{
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + platformName + ".dll");
#if NET5_0_OR_GREATER
var loader = new AssemblyLoader(rendererPath);
var platformType = loader.LoadDefaultAssembly().GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
#else
// NOTE: This is currently the only use of System.Reflection in this file, so would give an unused using error if we import it above
var assembly = System.Reflection.Assembly.LoadFile(rendererPath);
var platformType = assembly.GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
#endif
if (platformType == null)
throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation.");
return (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null);
}
public static void InitializeMod(string mod, Arguments args)
{
// Clear static state if we have switched mods
@@ -496,11 +484,11 @@ namespace OpenRA
Renderer.InitializeDepthBuffer(grid);
Cursor?.Dispose();
Cursor = new CursorManager(ModData.CursorProvider, ModData.Manifest.CursorSheetSize);
Cursor = new CursorManager(ModData.CursorProvider);
var metadata = ModData.Manifest.Metadata;
if (!string.IsNullOrEmpty(metadata.WindowTitleTranslated))
Renderer.Window.SetWindowTitle(metadata.WindowTitleTranslated);
if (!string.IsNullOrEmpty(metadata.WindowTitle))
Renderer.Window.SetWindowTitle(metadata.WindowTitle);
PerfHistory.Items["render"].HasNormalTick = false;
PerfHistory.Items["batches"].HasNormalTick = false;
@@ -536,11 +524,10 @@ namespace OpenRA
.Where(m => m.Status == MapStatus.Available && m.Visibility.HasFlag(MapVisibility.Shellmap))
.Select(m => m.Uid);
var shellmap = shellmaps.RandomOrDefault(CosmeticRandom);
if (shellmap == null)
if (!shellmaps.Any())
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)
@@ -595,7 +582,7 @@ namespace OpenRA
Log.Write("debug", "Taking screenshot " + 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.GameStarted && orderManager.LocalFrameNumber == 0)
PerfHistory.Reset(); // Remove history that occurred whilst the new game was loading.
using (var sample = new PerfSample("tick_time"))
using (new PerfSample("tick_time"))
{
orderManager.LastTickTime.AdvanceTickTime(tick);
@@ -626,11 +610,7 @@ namespace OpenRA
Sync.RunUnsynced(world, orderManager.TickImmediate);
if (world == null)
{
if (orderManager.GameStarted)
PerfHistory.Reset(); // Remove old history when a new game starts.
return;
}
if (orderManager.TryTick())
{
@@ -684,7 +664,7 @@ namespace OpenRA
// Prepare renderables (i.e. render voxels) before calling BeginFrame
using (new PerfSample("render_prepare"))
{
worldRenderer?.BeginFrame();
Renderer.WorldModelRenderer.BeginFrame();
// World rendering is disabled while the loading screen is displayed
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
@@ -694,7 +674,7 @@ namespace OpenRA
}
Ui.PrepareRenderables();
worldRenderer?.EndFrame();
Renderer.WorldModelRenderer.EndFrame();
}
// worldRenderer is null during the initial install/download screen
@@ -798,7 +778,7 @@ namespace OpenRA
var logicWorld = worldRenderer?.World;
// ReplayTimestep = 0 means the replay is paused: we need to keep logicInterval as UI.Timestep to avoid breakage
if (logicWorld != null && (!logicWorld.IsReplay || logicWorld.ReplayTimestep != 0))
if (logicWorld != null && !(logicWorld.IsReplay && logicWorld.ReplayTimestep == 0))
logicInterval = logicWorld == OrderManager.World ? OrderManager.SuggestedTimestep : logicWorld.Timestep;
// Ideal time between screen updates
@@ -933,15 +913,15 @@ namespace OpenRA
{
var endpoints = new List<IPEndPoint>
{
new(IPAddress.IPv6Any, settings.ListenPort),
new(IPAddress.Any, settings.ListenPort)
new IPEndPoint(IPAddress.IPv6Any, settings.ListenPort),
new IPEndPoint(IPAddress.Any, settings.ListenPort)
};
server = new Server.Server(endpoints, settings, ModData, ServerType.Multiplayer);
return server.GetEndpointForLocalConnection();
}
public static ConnectionTarget CreateLocalServer(string map, bool isSkirmish = false)
public static ConnectionTarget CreateLocalServer(string map)
{
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
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();
}
@@ -985,7 +965,7 @@ namespace OpenRA
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)
throw new ArgumentException($"Could not find map '{launchMap}'.");

View File

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

View File

@@ -22,7 +22,7 @@ namespace OpenRA
/// </summary>
public class ActorInfo
{
public const char AbstractActorPrefix = '^';
public const string AbstractActorPrefix = "^";
public const char TraitInstanceSeparator = '@';
/// <summary>
@@ -33,7 +33,7 @@ namespace OpenRA
/// </summary>
public readonly string Name;
readonly TypeDictionary traits = new();
TraitInfo[] constructOrderCache = null;
List<TraitInfo> constructOrderCache = null;
public ActorInfo(ObjectCreator creator, string name, MiniYaml node)
{
@@ -130,7 +130,6 @@ namespace OpenRA
// Continue resolving traits as long as possible.
// Each time we resolve some traits, this means dependencies for other traits may then be possible to satisfy in the next pass.
#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection
var readyToResolve = more.ToList();
while (readyToResolve.Count != 0)
{
@@ -139,7 +138,6 @@ namespace OpenRA
readyToResolve.Clear();
readyToResolve.AddRange(more);
}
#pragma warning restore CA1851
if (unresolved.Count != 0)
{
@@ -162,7 +160,7 @@ namespace OpenRA
throw new YamlException(exceptionString);
}
constructOrderCache = resolved.Select(r => r.Trait).ToArray();
constructOrderCache = resolved.Select(r => r.Trait).ToList();
return constructOrderCache;
}
@@ -187,7 +185,7 @@ namespace OpenRA
public bool HasTraitInfo<T>() where T : ITraitInfoInterface { return traits.Contains<T>(); }
public T TraitInfo<T>() where T : ITraitInfoInterface { return traits.Get<T>(); }
public T TraitInfoOrDefault<T>() where T : ITraitInfoInterface { return traits.GetOrDefault<T>(); }
public 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()
{

View File

@@ -124,7 +124,7 @@ namespace OpenRA
{
var actors = MergeOrDefault("Manifest,Rules", fs, m.Rules, null, null,
k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value),
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix));
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal));
var weapons = MergeOrDefault("Manifest,Weapons", fs, m.Weapons, null, null,
k => new WeaponInfo(k.Value));
@@ -182,7 +182,7 @@ namespace OpenRA
{
var actors = MergeOrDefault("Rules", fileSystem, m.Rules, mapRules, dr.Actors,
k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value),
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix));
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal));
var weapons = MergeOrDefault("Weapons", fileSystem, m.Weapons, mapWeapons, dr.Weapons,
k => new WeaponInfo(k.Value));
@@ -226,10 +226,10 @@ namespace OpenRA
static bool AnyCustomYaml(MiniYaml yaml)
{
return yaml != null && (yaml.Value != null || yaml.Nodes.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)
{
@@ -260,18 +260,18 @@ namespace OpenRA
return true;
// Any trait overrides that aren't explicitly whitelisted are flagged
if (mapRules == null)
return false;
if (AnyFlaggedTraits(modData, mapRules.Nodes))
return true;
if (mapRules.Value != null)
if (mapRules != null)
{
var mapFiles = FieldLoader.GetValue<string[]>("value", mapRules.Value);
foreach (var f in mapFiles)
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
return true;
if (AnyFlaggedTraits(modData, mapRules.Nodes))
return true;
if (mapRules.Value != null)
{
var mapFiles = FieldLoader.GetValue<string[]>("value", mapRules.Value);
foreach (var f in mapFiles)
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
return true;
}
}
return false;

View File

@@ -40,16 +40,16 @@ namespace OpenRA.GameRules
static Dictionary<string, SoundPool> ParseSoundPool(MiniYaml y, string key)
{
var ret = new Dictionary<string, SoundPool>();
var classifiction = y.NodeWithKey(key);
var classifiction = y.Nodes.First(x => x.Key == key);
foreach (var t in classifiction.Value.Nodes)
{
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)
volumeModifier = FieldLoader.GetValue<float>(volumeModifierNode.Key, volumeModifierNode.Value.Value);
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)
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
// 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);
}
static object LoadProjectile(MiniYaml yaml)
{
var proj = yaml.NodeWithKeyOrDefault("Projectile")?.Value;
if (proj == null)
if (!yaml.ToDictionary().TryGetValue("Projectile", out var proj))
return null;
var ret = Game.CreateObject<IProjectileInfo>(proj.Value + "Info");
@@ -160,7 +159,7 @@ namespace OpenRA.GameRules
static object LoadWarheads(MiniYaml yaml)
{
var retList = new List<IWarhead>();
foreach (var node in yaml.Nodes.Where(n => n.Key.StartsWith("Warhead", StringComparison.Ordinal)))
foreach (var node in yaml.Nodes.Where(n => n.Key.StartsWith("Warhead")))
{
var ret = Game.CreateObject<IWarhead>(node.Value.Value + "Warhead");
if (ret == null)

View File

@@ -10,12 +10,13 @@
#endregion
using System.Collections.Generic;
using System.Linq;
namespace OpenRA
{
public class GameSpeed
{
[FluentReference]
[TranslationReference]
[FieldLoader.Require]
public readonly string Name;
@@ -37,7 +38,7 @@ namespace OpenRA
static object LoadSpeeds(MiniYaml y)
{
var ret = new Dictionary<string, GameSpeed>();
var speedsNode = y.NodeWithKeyOrDefault("Speeds");
var speedsNode = y.Nodes.FirstOrDefault(n => n.Key == "Speeds");
if (speedsNode == null)
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 bool IsDecoration { get; set; }
readonly Map map;
readonly SequenceSet sequences;
readonly Func<WAngle> facingFunc;
readonly Func<bool> paused;
@@ -44,7 +43,6 @@ namespace OpenRA.Graphics
public Animation(World world, string name, Func<WAngle> facingFunc, Func<bool> paused)
{
map = world.Map;
sequences = world.Map.Sequences;
Name = name.ToLowerInvariant();
this.facingFunc = facingFunc;
@@ -60,18 +58,13 @@ namespace OpenRA.Graphics
var tintModifiers = CurrentSequence.IgnoreWorldTint ? TintModifiers.IgnoreWorldTint : TintModifiers.None;
var alpha = CurrentSequence.GetAlpha(CurrentFrame);
var (image, rotation) = CurrentSequence.GetSpriteWithRotation(CurrentFrame, facingFunc());
var imageRenderable = new SpriteRenderable(
image, pos, offset, CurrentSequence.ZOffset + zOffset, palette,
CurrentSequence.Scale, alpha, float3.Ones, tintModifiers, IsDecoration, rotation);
var imageRenderable = new SpriteRenderable(image, pos, offset, CurrentSequence.ZOffset + zOffset, palette, CurrentSequence.Scale, alpha, float3.Ones, tintModifiers, IsDecoration,
rotation);
var shadow = CurrentSequence.GetShadow(CurrentFrame, facingFunc());
if (shadow != null)
{
var height = map.DistanceAboveTerrain(pos).Length;
var shadowRenderable = new SpriteRenderable(
shadow, pos, offset - new WVec(0, 0, height), CurrentSequence.ShadowZOffset + zOffset + height, palette,
CurrentSequence.Scale, 1f, float3.Ones, tintModifiers,
var shadowRenderable = new SpriteRenderable(shadow, pos, offset, CurrentSequence.ShadowZOffset + zOffset, palette, CurrentSequence.Scale, 1f, float3.Ones, tintModifiers,
true, rotation);
return new IRenderable[] { shadowRenderable, imageRenderable };
}

View File

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

View File

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

View File

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

View File

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

View File

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

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
using System;
using System.Collections.Generic;
using OpenRA.FileSystem;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Graphics
{
@@ -29,14 +30,6 @@ namespace OpenRA.Graphics
Rectangle AggregateBounds { get; }
}
public interface IModelWidget
{
public string Palette { get; }
public float Scale { get; }
public void Setup(Func<bool> isVisible, Func<string> getPalette, Func<string> getPlayerPalette,
Func<float> getScale, Func<IModel> getVoxel, Func<WRot> getRotation);
}
public readonly struct ModelRenderData
{
public readonly int Start;
@@ -51,13 +44,51 @@ namespace OpenRA.Graphics
}
}
public interface IModelCacheInfo : ITraitInfoInterface { }
public interface IModelCache
public interface IModelCache : IDisposable
{
IModel GetModel(string model);
IModel GetModelSequence(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.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Cnc.Traits
namespace OpenRA.Graphics
{
public class ModelRenderProxy
{
@@ -34,14 +32,7 @@ namespace OpenRA.Mods.Cnc.Traits
}
}
[TraitLocation(SystemActors.World | SystemActors.EditorWorld)]
[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
public sealed class ModelRenderer : IDisposable
{
// Static constants
static readonly float[] ShadowDiffuse = new float[] { 0, 0, 0 };
@@ -55,32 +46,28 @@ namespace OpenRA.Mods.Cnc.Traits
readonly Renderer renderer;
readonly IShader shader;
public readonly IModelCache ModelCache;
readonly Dictionary<Sheet, IFrameBuffer> mappedBuffers = new();
readonly Stack<KeyValuePair<Sheet, IFrameBuffer>> unmappedBuffers = new();
readonly List<(Sheet Sheet, Action Func)> doRender = new();
readonly int sheetSize;
SheetBuilder sheetBuilderForFrame;
bool isInFrame;
public void SetPalette(HardwarePalette palette)
public ModelRenderer(Renderer renderer, IShader shader)
{
shader.SetTexture("Palette", palette.Texture);
shader.SetVec("PaletteRows", palette.Height);
this.renderer = renderer;
this.shader = shader;
}
public ModelRenderer(ModelRendererInfo info, Actor self)
public void SetPalette(ITexture palette)
{
renderer = Game.Renderer;
shader = renderer.CreateShader(new ModelShaderBindings());
renderer.WorldRenderers = renderer.WorldRenderers.Append(this).ToArray();
shader.SetTexture("Palette", palette);
}
ModelCache = self.Trait<IModelCache>();
sheetSize = Game.Settings.Graphics.SheetSize;
var a = 2f / sheetSize;
public void SetViewportParams()
{
var a = 2f / renderer.SheetSize;
var view = new[]
{
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 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 shadowTranslateMtx = Util.TranslationMatrix(shadowCenter.X - shadowSpriteOffset.X, sheetSize - (shadowCenter.Y - shadowSpriteOffset.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, renderer.SheetSize - (shadowCenter.Y - shadowSpriteOffset.Y), 0);
var correctionTransform = Util.MatrixMultiply(translateMtx, FlipMtx);
var shadowCorrectionTransform = Util.MatrixMultiply(shadowTranslateMtx, ShadowScaleFlipMtx);
void RenderFunc()
doRender.Add((sprite.Sheet, () =>
{
foreach (var m in models)
{
@@ -226,18 +213,16 @@ namespace OpenRA.Mods.Cnc.Traits
// Transform light vector from shadow -> world -> limb coords
var lightDirection = ExtractRotationVector(Util.MatrixMultiply(it, lightTransform));
Render(rd, ModelCache, Util.MatrixMultiply(transform, t), lightDirection,
lightAmbientColor, lightDiffuseColor, color.TextureIndex, normals.TextureIndex);
Render(rd, wr.World.ModelCache, Util.MatrixMultiply(transform, t), lightDirection,
lightAmbientColor, lightDiffuseColor, color.TextureMidIndex, normals.TextureMidIndex);
// Disable shadow normals by forcing zero diffuse and identity ambient light
if (m.ShowShadow)
Render(rd, ModelCache, Util.MatrixMultiply(shadow, t), lightDirection,
ShadowAmbient, ShadowDiffuse, shadowPalette.TextureIndex, normals.TextureIndex);
Render(rd, wr.World.ModelCache, Util.MatrixMultiply(shadow, t), lightDirection,
ShadowAmbient, ShadowDiffuse, shadowPalette.TextureMidIndex, normals.TextureMidIndex);
}
}
}
doRender.Add((sprite.Sheet, RenderFunc));
}));
var screenLightVector = Util.MatrixVectorMultiply(invShadowTransform, ZVector);
screenLightVector = Util.MatrixVectorMultiply(cameraTransform, screenLightVector);
@@ -252,9 +237,9 @@ namespace OpenRA.Mods.Cnc.Traits
// Width and height must be even to avoid rendering glitches
if ((width & 1) == 1)
width++;
width += 1;
if ((height & 1) == 1)
height++;
height += 1;
size = new Size(width, height);
}
@@ -282,17 +267,17 @@ namespace OpenRA.Mods.Cnc.Traits
IModelCache cache,
float[] t, float[] lightDirection,
float[] ambientLight, float[] diffuseLight,
float colorPaletteTextureIndex, float normalsPaletteTextureIndex)
float colorPaletteTextureMidIndex, float normalsPaletteTextureMidIndex)
{
shader.SetTexture("DiffuseTexture", renderData.Sheet.GetTexture());
shader.SetVec("Palettes", colorPaletteTextureIndex, normalsPaletteTextureIndex);
shader.SetVec("PaletteRows", colorPaletteTextureMidIndex, normalsPaletteTextureMidIndex);
shader.SetMatrix("TransformMatrix", t);
shader.SetVec("LightDirection", lightDirection, 4);
shader.SetVec("AmbientLight", ambientLight, 3);
shader.SetVec("DiffuseLight", diffuseLight, 3);
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()
@@ -313,14 +298,14 @@ namespace OpenRA.Mods.Cnc.Traits
Game.Renderer.Flush();
fbo.Bind();
Game.Renderer.EnableDepthBuffer();
Game.Renderer.Context.EnableDepthBuffer();
return fbo;
}
static void DisableFrameBuffer(IFrameBuffer fbo)
{
Game.Renderer.Flush();
Game.Renderer.DisableDepthBuffer();
Game.Renderer.Context.DisableDepthBuffer();
fbo.Unbind();
}
@@ -368,7 +353,8 @@ namespace OpenRA.Mods.Cnc.Traits
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);
mappedBuffers.Add(sheet, framebuffer);
@@ -385,12 +371,6 @@ namespace OpenRA.Mods.Cnc.Traits
mappedBuffers.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)
{
return Color.FromArgb(palette[index]);
return Color.FromArgb((int)palette[index]);
}
public static IPalette AsReadOnly(this IPalette palette)
@@ -103,7 +103,7 @@ namespace OpenRA.Graphics
: this(p)
{
for (var i = 0; i < Palette.Size; i++)
colors[i] = r.GetRemappedColor(this.GetColor(i), i).ToArgb();
colors[i] = (uint)r.GetRemappedColor(this.GetColor(i), i).ToArgb();
}
public ImmutablePalette(IPalette p)
@@ -142,7 +142,7 @@ namespace OpenRA.Graphics
public void SetColor(int index, Color color)
{
colors[index] = color.ToArgb();
colors[index] = (uint)color.ToArgb();
}
public void SetFromPalette(IPalette p)
@@ -153,7 +153,7 @@ namespace OpenRA.Graphics
public void ApplyRemap(IPaletteRemap r)
{
for (var i = 0; i < Palette.Size; i++)
colors[i] = 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
{
readonly float index;
readonly HardwarePalette hardwarePalette;
public readonly string Name;
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)
{
Name = name;
Palette = palette;
TextureIndex = index;
this.index = index;
this.hardwarePalette = hardwarePalette;
}

View File

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

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

View File

@@ -94,7 +94,7 @@ namespace OpenRA.Graphics
foreach (var node in nodes)
{
// Nodes starting with ^ are inheritable but never loaded directly
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix))
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal))
continue;
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)
return;
dirty = true;
releaseBufferOnCommit = true;
@@ -141,29 +140,6 @@ namespace OpenRA.Graphics
GetTexture();
}
public bool ReleaseBufferAndTryTransferTo(Sheet destination)
{
if (Size != destination.Size)
throw new ArgumentException("Destination sheet does not have the same size", nameof(destination));
var buffer = data;
ReleaseBuffer();
// We aren't commiting data to the GPU, so let's not delete our data.
if (Game.Renderer == null)
return false;
// Only transfer if the destination has no data that would be lost by overwriting.
if (buffer != null && destination.data == null && destination.texture == null)
{
Array.Clear(buffer, 0, buffer.Length);
destination.data = buffer;
return true;
}
return false;
}
public void Dispose()
{
texture?.Dispose();

View File

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

View File

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

View File

@@ -14,30 +14,28 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Primitives;
namespace OpenRA.Graphics
{
public delegate ISpriteFrame AdjustFrame(ISpriteFrame input, int index, int total);
public sealed class SpriteCache : IDisposable
{
public readonly Dictionary<SheetType, SheetBuilder> SheetBuilders;
readonly ISpriteLoader[] loaders;
readonly IReadOnlyFileSystem fileSystem;
readonly Dictionary<
int,
(int[] Frames, MiniYamlNode.SourceLocation Location, AdjustFrame AdjustFrame, bool Premultiplied)> spriteReservations = new();
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> spriteReservations = new();
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> frameReservations = new();
readonly Dictionary<string, List<int>> reservationsByFilename = new();
readonly Dictionary<int, ISpriteFrame[]> resolvedFrames = new();
readonly Dictionary<int, Sprite[]> resolvedSprites = new();
readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = new();
int nextReservationToken = 1;
public SpriteCache(
IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders, int bgraSheetSize, int indexedSheetSize, int bgraSheetMargin = 1, int indexedSheetMargin = 1)
public SpriteCache(IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders, int bgraSheetSize, int indexedSheetSize, int bgraSheetMargin = 1, int indexedSheetMargin = 1)
{
SheetBuilders = new Dictionary<SheetType, SheetBuilder>
{
@@ -49,110 +47,97 @@ namespace OpenRA.Graphics
this.loaders = loaders;
}
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location,
AdjustFrame adjustFrame = null, bool premultiplied = false)
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
{
var token = nextReservationToken++;
spriteReservations[token] = (frames?.ToArray(), location, adjustFrame, premultiplied);
spriteReservations[token] = (frames?.ToArray(), location);
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(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))
return null;
using (stream)
{
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 null;
}
}
public ISpriteFrame[] LoadFramesUncached(string filename)
{
return GetFrames(fileSystem, filename, loaders);
}
public void LoadReservations(ModData modData)
{
var pendingResolve = new List<(
string Filename,
int FrameIndex,
bool Premultiplied,
AdjustFrame AdjustFrame,
ISpriteFrame Frame,
Sprite[] SpritesForToken)>();
foreach (var sb in SheetBuilders.Values)
sb.Current.CreateBuffer();
var spriteCache = new Dictionary<int, Sprite>();
foreach (var (filename, tokens) in reservationsByFilename)
{
modData.LoadScreen?.Display();
var loadedFrames = GetFrames(fileSystem, filename, loaders);
var loadedFrames = GetFrames(fileSystem, filename, loaders, out _);
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)
{
var resolved = new Sprite[loadedFrames.Length];
resolvedSprites[token] = resolved;
if (rs.Frames != null && rs.Frames.Any(i => i >= loadedFrames.Length))
throw new InvalidOperationException($"{rs.Location}: {filename} does not contain frames: " +
string.Join(',', rs.Frames.Where(f => f >= loadedFrames.Length)));
var frames = rs.Frames ?? Enumerable.Range(0, loadedFrames.Length);
var total = rs.Frames?.Length ?? loadedFrames.Length;
var j = 0;
var frames = r.Frames ?? Enumerable.Range(0, loadedFrames.Length);
foreach (var i in frames)
{
var frame = loadedFrames[i];
if (rs.AdjustFrame != null)
frame = rs.AdjustFrame(frame, j++, total);
pendingResolve.Add((filename, i, rs.Premultiplied, rs.AdjustFrame, frame, resolved));
}
resolved[i] = spriteCache.GetOrAdd(i,
f => SheetBuilders[SheetBuilder.FrameTypeToSheetType(loadedFrames[f].Type)].Add(loadedFrames[f]));
resolvedSprites[token] = resolved;
}
else
{
resolvedSprites[token] = null;
missingFiles[token] = (filename, rs.Location);
missingFiles[token] = (filename, r.Location);
}
}
}
spriteCache.Clear();
}
spriteReservations.Clear();
spriteReservations.TrimExcess();
frameReservations.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)
sb.Current.ReleaseBuffer();
@@ -160,11 +145,18 @@ namespace OpenRA.Graphics
public Sprite[] ResolveSprites(int token)
{
if (!resolvedSprites.Remove(token, out var resolved))
throw new InvalidOperationException($"{nameof(token)} {token} has either already been resolved, or was never reserved via {nameof(ReserveSprites)}");
var resolved = resolvedSprites[token];
resolvedSprites.Remove(token);
if (missingFiles.TryGetValue(token, out var r))
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);
resolvedSprites.TrimExcess();
return resolved;
}
public ISpriteFrame[] ResolveFrames(int token)
{
var resolved = resolvedFrames[token];
resolvedFrames.Remove(token);
if (missingFiles.TryGetValue(token, out var r))
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);

View File

@@ -27,7 +27,7 @@ namespace OpenRA.Graphics
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)
throw new ArgumentException("The sheet builder must create BGRA sheets.", nameof(builder));
@@ -36,7 +36,7 @@ namespace OpenRA.Graphics
this.size = size;
this.builder = builder;
font = platform.CreateFont(data);
font = Game.Renderer.CreateFont(data);
glyphs = new Cache<char, GlyphInfo>(CreateGlyph);
contrastGlyphs = new Cache<(char, int), Sprite>(CreateContrastGlyph);
dilationElements = new Cache<int, float[]>(CreateCircularWeightMap);

View File

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

View File

@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using OpenRA.Primitives;
namespace OpenRA.Graphics
@@ -20,7 +19,6 @@ namespace OpenRA.Graphics
{
public const int SheetCount = 8;
static readonly string[] SheetIndexToTextureName = Exts.MakeArray(SheetCount, i => $"Texture{i}");
static readonly int UintSize = Marshal.SizeOf<uint>();
readonly Renderer renderer;
readonly IShader shader;
@@ -29,21 +27,21 @@ namespace OpenRA.Graphics
readonly Sheet[] sheets = new Sheet[SheetCount];
BlendMode currentBlend = BlendMode.Alpha;
int vertexCount = 0;
int sheetCount = 0;
int nv = 0;
int ns = 0;
public SpriteRenderer(Renderer renderer, IShader shader)
{
this.renderer = renderer;
this.shader = shader;
vertices = renderer.Context.CreateVertices<Vertex>(renderer.TempVertexBufferSize);
vertices = renderer.Context.CreateVertices(renderer.TempBufferSize);
}
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());
sheets[i] = null;
@@ -52,11 +50,12 @@ namespace OpenRA.Graphics
renderer.Context.SetBlendMode(currentBlend);
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);
vertexCount = 0;
sheetCount = 0;
nv = 0;
ns = 0;
}
}
@@ -64,7 +63,7 @@ namespace OpenRA.Graphics
{
renderer.CurrentBatchRenderer = this;
if (s.BlendMode != currentBlend || vertexCount + 4 > renderer.TempVertexBufferSize)
if (s.BlendMode != currentBlend || nv + 6 > renderer.TempBufferSize)
Flush();
currentBlend = s.BlendMode;
@@ -72,7 +71,7 @@ namespace OpenRA.Graphics
// Check if the sheet (or secondary data sheet) have already been mapped
var sheet = s.Sheet;
var sheetIndex = 0;
for (; sheetIndex < sheetCount; sheetIndex++)
for (; sheetIndex < ns; sheetIndex++)
if (sheets[sheetIndex] == sheet)
break;
@@ -81,7 +80,7 @@ namespace OpenRA.Graphics
if (ss != null)
{
var secondarySheet = ss.SecondarySheet;
for (; secondarySheetIndex < sheetCount; secondarySheetIndex++)
for (; secondarySheetIndex < ns; secondarySheetIndex++)
if (sheets[secondarySheetIndex] == secondarySheet)
break;
@@ -100,22 +99,22 @@ namespace OpenRA.Graphics
secondarySheetIndex = ss != null && ss.SecondarySheet != sheet ? 1 : 0;
}
if (sheetIndex >= sheetCount)
if (sheetIndex >= ns)
{
sheets[sheetIndex] = sheet;
sheetCount++;
ns++;
}
if (secondarySheetIndex >= sheetCount && ss != null)
if (secondarySheetIndex >= ns && ss != null)
{
sheets[secondarySheetIndex] = ss.SecondarySheet;
sheetCount++;
ns++;
}
return new int2(sheetIndex, secondarySheetIndex);
}
static int ResolveTextureIndex(Sprite s, PaletteReference pal)
static float ResolveTextureIndex(Sprite s, PaletteReference pal)
{
if (pal == null)
return 0;
@@ -129,20 +128,20 @@ namespace OpenRA.Graphics
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);
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);
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);
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);
vertexCount += 4;
nv += 6;
}
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);
}
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)
{
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);
vertexCount += 4;
nv += 6;
}
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);
}
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);
Util.FastCreateQuad(vertices, a, b, c, d, s, samplers, paletteTextureIndex, tint, alpha, vertexCount);
vertexCount += 4;
Util.FastCreateQuad(vertices, a, b, c, d, s, samplers, paletteTextureIndex, tint, alpha, nv);
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;
foreach (var s in sheets)
@@ -186,7 +185,7 @@ namespace OpenRA.Graphics
renderer.Context.SetBlendMode(blendMode);
shader.PrepareRender();
renderer.DrawQuadBatch(buffer, indices, shader, length, UintSize * start);
renderer.DrawBatch(buffer, start, length, type);
renderer.Context.SetBlendMode(BlendMode.None);
}
@@ -197,32 +196,29 @@ namespace OpenRA.Graphics
}
// For RGBAColorRenderer
internal void DrawRGBAQuad(Vertex[] v, BlendMode blendMode)
internal void DrawRGBAVertices(Vertex[] v, BlendMode blendMode)
{
renderer.CurrentBatchRenderer = this;
if (currentBlend != blendMode || vertexCount + 4 > renderer.TempVertexBufferSize)
if (currentBlend != blendMode || nv + v.Length > renderer.TempBufferSize)
Flush();
currentBlend = blendMode;
Array.Copy(v, 0, vertices, vertexCount, v.Length);
vertexCount += 4;
Array.Copy(v, 0, vertices, nv, v.Length);
nv += v.Length;
}
public void SetPalette(HardwarePalette palette)
public void SetPalette(ITexture palette, ITexture colorShifts)
{
shader.SetTexture("Palette", palette.Texture);
shader.SetTexture("ColorShifts", palette.ColorShifts);
shader.SetVec("PaletteRows", palette.Height);
shader.SetTexture("Palette", palette);
shader.SetTexture("ColorShifts", colorShifts);
}
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
// using p1 to values [0, 2] and then subtract by 1 using p2, where p stands for projection. It's
// standard practice for shaders to use a projection matrix, but as we project orthographically
// we are able to send less data to the GPU.
// Calculate the scale (r1) and offset (r2) that convert from OpenRA viewport pixels
// to OpenGL normalized device coordinates (NDC). OpenGL expects coordinates to vary from [-1, 1],
// so we rescale viewport pixels to the range [0, 2] using r1 then subtract 1 using r2.
var width = 2f / (downscale * sheetSize.Width);
var height = 2f / (downscale * sheetSize.Height);
@@ -243,8 +239,8 @@ namespace OpenRA.Graphics
var depth = depthMargin != 0f ? 2f / (downscale * (sheetSize.Height + depthMargin)) : 0;
shader.SetVec("DepthTextureScale", 128 * depth);
shader.SetVec("Scroll", scroll.X, scroll.Y, depthMargin != 0f ? scroll.Y : 0);
shader.SetVec("p1", width, height, -depth);
shader.SetVec("p2", -1, -1, depthMargin != 0f ? 1 : 0);
shader.SetVec("r1", width, height, -depth);
shader.SetVec("r2", -1, -1, depthMargin != 0f ? 1 : 0);
}
public void SetDepthPreview(bool enabled, float contrast, float offset)
@@ -253,9 +249,9 @@ namespace OpenRA.Graphics
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.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
namespace OpenRA.Graphics
{
public sealed class TerrainSpriteLayer : IDisposable
{
// PERF: we can reuse the IndexBuffer as all layers have the same size.
static readonly ConditionalWeakTable<World, IndexBufferRc> IndexBuffers = new();
readonly IndexBufferRc indexBufferWrapper;
static readonly int[] CornerVertexMap = { 0, 1, 2, 2, 3, 0 };
public readonly BlendMode BlendMode;
@@ -31,8 +28,7 @@ namespace OpenRA.Graphics
readonly Vertex[] vertices;
readonly bool[] ignoreTint;
readonly HashSet<int> dirtyRows = new();
readonly int indexRowStride;
readonly int vertexRowStride;
readonly int rowStride;
readonly bool restrictToBounds;
readonly WorldRenderer worldRenderer;
@@ -47,25 +43,19 @@ namespace OpenRA.Graphics
this.emptySprite = emptySprite;
sheets = new Sheet[SpriteRenderer.SheetCount];
BlendMode = blendMode;
map = world.Map;
rowStride = 6 * map.MapSize.X;
vertexRowStride = 4 * map.MapSize.X;
vertices = new Vertex[vertexRowStride * map.MapSize.Y];
vertexBuffer = Game.Renderer.Context.CreateVertexBuffer<Vertex>(vertices.Length);
indexRowStride = 6 * map.MapSize.X;
lock (IndexBuffers)
{
indexBufferWrapper = IndexBuffers.GetValue(world, world => new IndexBufferRc(world));
indexBufferWrapper.AddRef();
}
vertices = new Vertex[rowStride * map.MapSize.Y];
palettes = new PaletteReference[map.MapSize.X * map.MapSize.Y];
vertexBuffer = Game.Renderer.Context.CreateVertexBuffer(vertices.Length);
wr.PaletteInvalidated += UpdatePaletteIndices;
if (wr.TerrainLighting != null)
{
ignoreTint = new bool[vertexRowStride * map.MapSize.Y];
ignoreTint = new bool[rowStride * map.MapSize.Y];
wr.TerrainLighting.CellChanged += UpdateTint;
}
}
@@ -75,9 +65,8 @@ namespace OpenRA.Graphics
for (var i = 0; i < vertices.Length; i++)
{
var v = vertices[i];
var p = palettes[i / 4]?.TextureIndex ?? 0;
var c = (uint)((p & 0xFFFF) << 16) | (v.C & 0xFFFF);
vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, c, v.R, v.G, v.B, v.A);
var p = palettes[i / 6]?.TextureIndex ?? 0;
vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, p, v.C, v.R, v.G, v.B, v.A);
}
for (var row = 0; row < map.MapSize.Y; row++)
@@ -108,13 +97,13 @@ namespace OpenRA.Graphics
void UpdateTint(MPos uv)
{
var offset = vertexRowStride * uv.V + 4 * uv.U;
var offset = rowStride * uv.V + 6 * uv.U;
if (ignoreTint[offset])
{
for (var i = 0; i < 4; i++)
for (var i = 0; i < 6; 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;
@@ -136,10 +125,10 @@ namespace OpenRA.Graphics
// Apply tint directly to the underlying vertices
// This saves us from having to re-query the sprite information, which has not changed
for (var i = 0; i < 4; i++)
for (var i = 0; i < 6; 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);
@@ -191,7 +180,7 @@ namespace OpenRA.Graphics
if (!map.Tiles.Contains(uv))
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);
palettes[uv.V * map.MapSize.X + uv.U] = palette;
@@ -220,13 +209,13 @@ namespace OpenRA.Graphics
if (!dirtyRows.Remove(row))
continue;
var rowOffset = vertexRowStride * row;
vertexBuffer.SetData(vertices, rowOffset, rowOffset, vertexRowStride);
var rowOffset = rowStride * row;
vertexBuffer.SetData(vertices, rowOffset, rowOffset, rowStride);
}
Game.Renderer.WorldSpriteRenderer.DrawVertexBuffer(
vertexBuffer, indexBufferWrapper.Buffer, indexRowStride * firstRow,
indexRowStride * (lastRow - firstRow), sheets, BlendMode);
vertexBuffer, rowStride * firstRow, rowStride * (lastRow - firstRow),
PrimitiveType.TriangleList, sheets, BlendMode);
Game.Renderer.Flush();
}
@@ -238,29 +227,6 @@ namespace OpenRA.Graphics
worldRenderer.TerrainLighting.CellChanged -= UpdateTint;
vertexBuffer.Dispose();
lock (IndexBuffers)
indexBufferWrapper.Dispose();
}
sealed class IndexBufferRc : IDisposable
{
public IIndexBuffer Buffer;
int count;
public IndexBufferRc(World world)
{
Buffer = Game.Renderer.Context.CreateIndexBuffer(Util.CreateQuadIndices(world.Map.MapSize.X * world.Map.MapSize.Y));
}
public void AddRef() { count++; }
public void Dispose()
{
count--;
if (count == 0)
Buffer.Dispose();
}
}
}
}

View File

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

View File

@@ -10,7 +10,6 @@
#endregion
using System;
using System.Runtime.InteropServices;
using OpenRA.FileFormats;
using OpenRA.Primitives;
@@ -21,17 +20,7 @@ namespace OpenRA.Graphics
// yes, our channel order is nuts.
static readonly int[] ChannelMasks = { 2, 1, 0, 3 };
public static uint[] CreateQuadIndices(int quads)
{
var indices = new uint[quads * 6];
ReadOnlySpan<uint> cornerVertexMap = stackalloc uint[] { 0, 1, 2, 2, 3, 0 };
for (var i = 0; i < indices.Length; i++)
indices[i] = cornerVertexMap[i % 6] + (uint)(4 * (i / 6));
return indices;
}
public static void FastCreateQuad(Vertex[] vertices, in float3 o, Sprite r, int2 samplers, int paletteTextureIndex, int nv,
public static void FastCreateQuad(Vertex[] vertices, in float3 o, Sprite r, int2 samplers, float paletteTextureIndex, int nv,
in float3 size, in float3 tint, float alpha, float rotation = 0f)
{
float3 a, b, c, d;
@@ -73,7 +62,7 @@ namespace OpenRA.Graphics
public static void FastCreateQuad(Vertex[] vertices,
in float3 a, in float3 b, in float3 c, in float3 d,
Sprite r, int2 samplers, int paletteTextureIndex,
Sprite r, int2 samplers, float paletteTextureIndex,
in float3 tint, float alpha, int nv)
{
float sl = 0;
@@ -95,33 +84,76 @@ namespace OpenRA.Graphics
attribC |= samplers.Y << 9;
}
attribC |= (paletteTextureIndex & 0xFFFF) << 16;
var uAttribC = (uint)attribC;
vertices[nv] = new Vertex(a, r.Left, r.Top, sl, st, uAttribC, tint, alpha);
vertices[nv + 1] = new Vertex(b, r.Right, r.Top, sr, st, uAttribC, tint, alpha);
vertices[nv + 2] = new Vertex(c, r.Right, r.Bottom, sr, sb, uAttribC, tint, alpha);
vertices[nv + 3] = new Vertex(d, r.Left, r.Bottom, sl, sb, uAttribC, tint, alpha);
var fAttribC = (float)attribC;
vertices[nv] = new Vertex(a, r.Left, r.Top, sl, st, paletteTextureIndex, fAttribC, tint, alpha);
vertices[nv + 1] = new Vertex(b, r.Right, r.Top, sr, st, paletteTextureIndex, fAttribC, tint, alpha);
vertices[nv + 2] = new Vertex(c, r.Right, r.Bottom, sr, sb, paletteTextureIndex, fAttribC, tint, alpha);
vertices[nv + 3] = new Vertex(c, r.Right, r.Bottom, sr, sb, paletteTextureIndex, fAttribC, tint, alpha);
vertices[nv + 4] = new Vertex(d, r.Left, r.Bottom, sl, sb, paletteTextureIndex, fAttribC, tint, alpha);
vertices[nv + 5] = new Vertex(a, r.Left, r.Top, sl, st, paletteTextureIndex, fAttribC, tint, alpha);
}
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 stride = dest.Sheet.Size.Width;
var x = dest.Bounds.Left;
var y = dest.Bounds.Top;
var width = dest.Bounds.Width;
var height = dest.Bounds.Height;
if (dest.Channel == TextureChannel.RGBA)
{
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
{
// Copy into single channel of destination.
var destStride = stride * 4;
var destOffset = destStride * y + x * 4 + ChannelMasks[(int)dest.Channel];
var destStride = dest.Sheet.Size.Width * 4;
var destOffset = destStride * dest.Bounds.Top + dest.Bounds.Left * 4 + ChannelMasks[(int)dest.Channel];
var destSkip = destStride - 4 * width;
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)
{
var destData = dest.Sheet.GetData();
var stride = dest.Sheet.Size.Width;
var x = dest.Bounds.Left;
var y = dest.Bounds.Top;
var destStride = dest.Sheet.Size.Width;
var width = dest.Bounds.Width;
var height = dest.Bounds.Height;
var si = 0;
var di = y * stride + x;
var d = MemoryMarshal.Cast<byte, uint>(destData);
for (var h = 0; h < height; h++)
unsafe
{
for (var w = 0; w < width; w++)
// Cast the data to an int array so we can copy the src data directly
fixed (byte* bd = &destData[0])
{
Color c;
switch (src.Type)
var data = (int*)bd;
var x = dest.Bounds.Left;
var y = dest.Bounds.Top;
var k = 0;
for (var j = 0; j < height; j++)
{
case SpriteFrameType.Indexed8:
c = src.Palette[src.Data[si++]];
break;
for (var i = 0; i < width; i++)
{
Color cc;
switch (src.Type)
{
case SpriteFrameType.Indexed8:
{
cc = src.Palette[src.Data[k++]];
break;
}
case SpriteFrameType.Rgba32:
case SpriteFrameType.Rgb24:
var r = src.Data[si++];
var g = src.Data[si++];
var b = src.Data[si++];
var a = src.Type == SpriteFrameType.Rgba32 ? src.Data[si++] : byte.MaxValue;
c = Color.FromArgb(a, r, g, b);
break;
case SpriteFrameType.Rgba32:
case SpriteFrameType.Rgb24:
{
var r = src.Data[k++];
var g = src.Data[k++];
var b = src.Data[k++];
var a = src.Type == SpriteFrameType.Rgba32 ? src.Data[k++] : (byte)255;
cc = Color.FromArgb(a, r, g, b);
break;
}
// PNGs don't support BGR[A], so no need to include them here
default:
throw new InvalidOperationException($"Unknown SpriteFrameType {src.Type}");
// Pngs don't support BGR[A], so no need to include them here
default:
throw new InvalidOperationException($"Unknown SpriteFrameType {src.Type}");
}
data[(y + j) * destStride + x + i] = PremultiplyAlpha(cc).ToArgb();
}
}
d[di++] = PremultiplyAlpha(c).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.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;
// Palette and channel flags
public readonly uint C;
public readonly float P, C;
// Color tint
public readonly float R, G, B, A;
public Vertex(in float3 xyz, float s, float t, float u, float v, uint c)
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, c, float3.Ones, 1f) { }
public Vertex(in float3 xyz, float s, float t, float u, float v, float p, float c)
: 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)
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, c, tint.X, tint.Y, tint.Z, 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, p, c, tint.X, tint.Y, tint.Z, a) { }
public Vertex(float x, float y, float z, float s, float t, float u, float v, uint c, in float3 tint, float a)
: this(x, y, z, s, t, u, v, c, tint.X, tint.Y, tint.Z, a) { }
public Vertex(float x, float y, float z, float s, float t, float u, float v, float p, float c, in float3 tint, float a)
: this(x, y, z, s, t, u, v, p, c, tint.X, tint.Y, tint.Z, a) { }
public Vertex(float x, float y, float z, float s, float t, float u, float v, uint c, 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;
S = s; T = t;
U = u; V = v;
C = c;
P = p; C = c;
R = r; G = g; B = b; A = a;
}
}
public sealed class CombinedShaderBindings : ShaderBindings
{
public CombinedShaderBindings()
: base("combined")
{ }
public override ShaderVertexAttribute[] Attributes { get; } = new[]
{
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
new ShaderVertexAttribute("aVertexAttributes", ShaderVertexAttributeType.UInt, 1, 28),
new ShaderVertexAttribute("aVertexTint", ShaderVertexAttributeType.Float, 4, 32)
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -154,7 +154,7 @@ namespace OpenRA
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)
@@ -175,7 +175,8 @@ namespace OpenRA
protected CompositeActorInit(TraitInfo info)
: base(info.InstanceName) { }
protected CompositeActorInit() { }
protected CompositeActorInit()
: base() { }
public virtual void Initialize(MiniYaml yaml)
{

View File

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

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.Collections;
using System.Collections.Generic;
using System.Linq;
namespace OpenRA
{
@@ -63,9 +64,9 @@ namespace OpenRA
}
/// <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));
var minU = int.MaxValue;
@@ -102,7 +103,6 @@ namespace OpenRA
}
public MapCoordsRegion MapCoords => new(mapTopLeft, mapBottomRight);
public CellCoordsRegion CellCoords => new(TopLeft, BottomRight);
public CellRegionEnumerator GetEnumerator()
{
@@ -136,12 +136,12 @@ namespace OpenRA
public bool MoveNext()
{
u++;
u += 1;
// Check for column overflow
if (u > r.mapBottomRight.U)
{
v++;
v += 1;
u = r.mapTopLeft.U;
// Check for row overflow
@@ -162,8 +162,8 @@ namespace OpenRA
}
public CPos Current { get; private set; }
readonly object IEnumerator.Current => Current;
public readonly void Dispose() { }
object IEnumerator.Current => Current;
public void Dispose() { }
}
}
}

View File

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

View File

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

View File

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

View File

@@ -86,7 +86,7 @@ namespace OpenRA
dirty = false;
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 (mapAction.Value == MapAction.Delete)

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,36 +20,18 @@ namespace OpenRA
{
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());
}
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
return y.ToLines().JoinWith("\n").TrimEnd('\n') + "\n";
}
public static IEnumerable<string> ToLines(this IEnumerable<MiniYamlNode> y)
{
foreach (var kv in y)
foreach (var line in kv.Value.ToLines(kv.Key, kv.Comment))
yield return line;
}
public static void WriteToFile(this IEnumerable<MiniYamlNodeBuilder> y, string filename)
{
File.WriteAllLines(filename, y.ToLines().Select(x => x.TrimEnd()).ToArray());
}
public static string WriteToString(this IEnumerable<MiniYamlNodeBuilder> y)
{
// Remove all trailing newlines and restore the final EOF newline
return y.ToLines().JoinWith("\n").TrimEnd('\n') + "\n";
}
public static IEnumerable<string> ToLines(this IEnumerable<MiniYamlNodeBuilder> y)
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))
@@ -61,29 +43,22 @@ namespace OpenRA
{
public readonly struct SourceLocation
{
public readonly string Name;
public readonly string Filename;
public readonly int Line;
public SourceLocation(string name, int line)
public SourceLocation(string filename, int line)
{
Name = name;
Filename = filename;
Line = line;
}
public override string ToString() { return $"{Name}:{Line}"; }
public override string ToString() { return $"{Filename}:{Line}"; }
}
public readonly SourceLocation Location;
public readonly string Key;
public readonly MiniYaml Value;
public readonly string Comment;
public MiniYamlNode WithValue(MiniYaml value)
{
if (Value == value)
return this;
return new MiniYamlNode(Key, value, Comment, Location);
}
public SourceLocation Location;
public string Key;
public MiniYaml Value;
public string Comment;
public MiniYamlNode(string k, MiniYaml v, string c = null)
{
@@ -99,15 +74,26 @@ namespace OpenRA
}
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) { }
public MiniYamlNode(string k, string v, string c, List<MiniYamlNode> n)
: this(k, new MiniYaml(v, n), c) { }
public MiniYamlNode(string k, string v, string c, List<MiniYamlNode> n, SourceLocation loc)
: this(k, new MiniYaml(v, n), c, loc) { }
public override string ToString()
{
return $"{{YamlNode: {Key} @ {Location}}}";
}
public MiniYamlNode Clone()
{
return new MiniYamlNode(Key, Value.Clone(), Comment, Location);
}
}
public sealed class MiniYaml
@@ -115,59 +101,15 @@ namespace OpenRA
const int SpacesPerLevel = 4;
static readonly Func<string, string> StringIdentity = s => s;
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 readonly ImmutableArray<MiniYamlNode> Nodes;
public MiniYaml WithValue(string value)
public MiniYaml Clone()
{
if (Value == value)
return this;
return new MiniYaml(value, Nodes);
}
public MiniYaml WithNodes(IEnumerable<MiniYamlNode> nodes)
{
if (nodes is ImmutableArray<MiniYamlNode> n && Nodes == n)
return this;
return new MiniYaml(Value, nodes);
}
public MiniYaml WithNodesAppended(IEnumerable<MiniYamlNode> nodes)
{
var newNodes = Nodes.AddRange(nodes);
if (Nodes == newNodes)
return this;
return new MiniYaml(Value, newNodes);
}
public MiniYamlNode NodeWithKey(string key)
{
var result = NodeWithKeyOrDefault(key);
if (result == null)
throw new InvalidDataException($"No node with key '{key}'");
return result;
}
public MiniYamlNode NodeWithKeyOrDefault(string key)
{
// PERF: Avoid LINQ.
var first = true;
MiniYamlNode result = null;
var clonedNodes = new List<MiniYamlNode>(Nodes.Count);
foreach (var node in Nodes)
{
if (node.Key != key)
continue;
if (!first)
throw new InvalidDataException($"Duplicate key '{node.Key}' in {node.Location}");
first = false;
result = node;
}
return result;
clonedNodes.Add(node.Clone());
return new MiniYaml(Value, clonedNodes);
}
public Dictionary<string, MiniYaml> ToDictionary()
@@ -183,7 +125,7 @@ namespace OpenRA
public Dictionary<TKey, TElement> ToDictionary<TKey, TElement>(
Func<string, TKey> keySelector, Func<MiniYaml, TElement> elementSelector)
{
var ret = new Dictionary<TKey, TElement>(Nodes.Length);
var ret = new Dictionary<TKey, TElement>(Nodes.Count);
foreach (var y in Nodes)
{
var key = keySelector(y.Key);
@@ -196,27 +138,28 @@ namespace OpenRA
}
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;
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.
// Pool these strings so we only need one copy of each unique string.
// This saves on long-term memory usage as parsed values can often live a long time.
// A caller can also provide a pool as input, allowing de-duplication across multiple parses.
stringPool ??= new HashSet<string>();
var nd = y.ToDictionary();
return nd.TryGetValue(s, out var v) ? v.Nodes : new List<MiniYamlNode>();
}
var result = new List<List<MiniYamlNode>>
static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string filename, bool discardCommentsAndWhitespace, Dictionary<string, string> stringPool)
{
stringPool ??= new Dictionary<string, string>();
var levels = new List<List<MiniYamlNode>>
{
new()
new List<MiniYamlNode>()
};
var parsedLines = new List<(int Level, string Key, string Value, string Comment, MiniYamlNode.SourceLocation Location)>();
var lineNo = 0;
foreach (var ll in lines)
@@ -232,7 +175,7 @@ namespace OpenRA
ReadOnlySpan<char> key = default;
ReadOnlySpan<char> value = default;
ReadOnlySpan<char> comment = default;
var location = new MiniYamlNode.SourceLocation(name, lineNo);
var location = new MiniYamlNode.SourceLocation(filename, lineNo);
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>`
// The # character is allowed in the value if escaped (\#).
// 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] != '\\'))
{
commentStart = i + 1;
if (i <= keyStart + keyLength)
if (commentStart <= keyLength)
keyLength = i - keyStart;
else
valueLength = i - valueStart;
@@ -322,81 +274,46 @@ namespace OpenRA
if (!key.IsEmpty || !discardCommentsAndWhitespace)
{
if (parsedLines.Count > 0 && parsedLines[^1].Level < level - 1)
throw new YamlException($"Bad indent in miniyaml at {location}");
while (parsedLines.Count > 0 && parsedLines[^1].Level > level)
BuildCompletedSubNode(level);
var keyString = key.IsEmpty ? null : key.ToString();
var valueString = value.IsEmpty ? null : value.ToString();
// Note: We need to support empty comments here to ensure that empty comments
// (i.e. a lone # at the end of a line) can be correctly re-serialized
var commentString = comment == ReadOnlySpan<char>.Empty ? null : comment.ToString();
var commentString = comment == default ? null : comment.ToString();
keyString = keyString == null ? null : stringPool.GetOrAdd(keyString);
valueString = valueString == null ? null : stringPool.GetOrAdd(valueString);
commentString = commentString == null ? null : stringPool.GetOrAdd(commentString);
keyString = keyString == null ? null : stringPool.GetOrAdd(keyString, keyString);
valueString = valueString == null ? null : stringPool.GetOrAdd(valueString, valueString);
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)
BuildCompletedSubNode(0);
foreach (var nodes in levels)
nodes.TrimExcess();
return result[0];
void BuildCompletedSubNode(int level)
{
var lastLevel = parsedLines[^1].Level;
while (lastLevel >= result.Count)
result.Add(new List<MiniYamlNode>());
while (parsedLines.Count > 0 && parsedLines[^1].Level >= level)
{
var parent = parsedLines[^1];
var startOfRange = parsedLines.Count - 1;
while (startOfRange > 0 && parsedLines[startOfRange - 1].Level == parent.Level)
startOfRange--;
for (var i = startOfRange; i < parsedLines.Count - 1; i++)
{
var sibling = parsedLines[i];
result[parent.Level].Add(
new MiniYamlNode(sibling.Key, new MiniYaml(sibling.Value), sibling.Comment, sibling.Location));
}
var childNodes = parent.Level + 1 < result.Count ? result[parent.Level + 1] : null;
result[parent.Level].Add(new MiniYamlNode(
parent.Key,
new MiniYaml(parent.Value, childNodes ?? Enumerable.Empty<MiniYamlNode>()),
parent.Comment,
parent.Location));
childNodes?.Clear();
parsedLines.RemoveRange(startOfRange, parsedLines.Count - startOfRange);
}
}
return levels[0];
}
public static List<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
public static List<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
{
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();
if (sourcesList.Count == 0)
@@ -419,41 +336,28 @@ namespace OpenRA
}
// Resolve any top-level removals (e.g. removing whole actor blocks)
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)));
var result = ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty);
return result as List<MiniYamlNode> ?? result.ToList();
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)).ToList());
return ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty);
}
static void MergeIntoResolved(MiniYamlNode overrideNode, List<MiniYamlNode> existingNodes, HashSet<string> existingNodeKeys,
Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
{
var existingNodeIndex = -1;
MiniYamlNode existingNode = null;
if (!existingNodeKeys.Add(overrideNode.Key))
if (existingNodeKeys.Add(overrideNode.Key))
{
existingNodeIndex = IndexOfKey(existingNodes, overrideNode.Key);
existingNode = existingNodes[existingNodeIndex];
existingNodes.Add(overrideNode.Clone());
return;
}
var value = MergePartial(existingNode?.Value, overrideNode.Value);
var nodes = ResolveInherits(value, tree, inherited);
if (!value.Nodes.SequenceEqual(nodes))
value = value.WithNodes(nodes);
if (existingNode != null)
existingNodes[existingNodeIndex] = existingNode.WithValue(value);
else
existingNodes.Add(overrideNode.WithValue(value));
var existingNode = existingNodes.Find(n => n.Key == overrideNode.Key);
existingNode.Value = MergePartial(existingNode.Value, overrideNode.Value);
existingNode.Value.Nodes = ResolveInherits(existingNode.Value, tree, inherited);
}
static IReadOnlyCollection<MiniYamlNode> ResolveInherits(
MiniYaml node, Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
static List<MiniYamlNode> ResolveInherits(MiniYaml node, Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
{
if (node.Nodes.Length == 0)
return node.Nodes;
var resolved = new List<MiniYamlNode>(node.Nodes.Length);
var resolvedKeys = new HashSet<string>(node.Nodes.Length);
var resolved = new List<MiniYamlNode>(node.Nodes.Count);
var resolvedKeys = new HashSet<string>(node.Nodes.Count);
foreach (var n in node.Nodes)
{
@@ -469,14 +373,13 @@ namespace OpenRA
}
catch (ArgumentException)
{
throw new YamlException(
$"{n.Location}: Parent type `{n.Value.Value}` was already inherited by this yaml tree at {inherited[n.Value.Value]} (note: may be from a derived tree)");
throw new YamlException($"{n.Location}: Parent type `{n.Value.Value}` was already inherited by this yaml tree at {inherited[n.Value.Value]} (note: may be from a derived tree)");
}
foreach (var r in ResolveInherits(parent, tree, inherited))
MergeIntoResolved(r, resolved, resolvedKeys, tree, inherited);
}
else if (n.Key.StartsWith('-'))
else if (n.Key.StartsWith("-", StringComparison.Ordinal))
{
var removed = n.Key[1..];
if (resolved.RemoveAll(r => r.Key == removed) == 0)
@@ -487,6 +390,7 @@ namespace OpenRA
MergeIntoResolved(n, resolved, resolvedKeys, tree, inherited);
}
resolved.TrimExcess();
return resolved;
}
@@ -494,11 +398,8 @@ namespace OpenRA
/// Merges any duplicate keys that are defined within the same set of nodes.
/// Does not resolve inheritance or node removals.
/// </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 ret = new List<MiniYamlNode>(existingNodes.Count);
foreach (var n in existingNodes)
@@ -508,26 +409,19 @@ namespace OpenRA
else
{
// 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[originalIndex];
ret[originalIndex] = original.WithValue(MergePartial(original.Value, n.Value));
var original = ret.First(r => r.Key == n.Key);
original.Value = MergePartial(original.Value, n.Value);
}
}
ret.TrimExcess();
return ret;
}
static MiniYaml MergePartial(MiniYaml existingNodes, MiniYaml overrideNodes)
{
lock (ConflictScratch)
{
// PERF: Reuse ConflictScratch for all conflict checks to avoid allocations.
existingNodes?.Nodes.IntoDictionaryWithConflictLog(
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
overrideNodes?.Nodes.IntoDictionaryWithConflictLog(
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
ConflictScratch.Clear();
}
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})");
if (existingNodes == null)
return overrideNodes;
@@ -538,7 +432,7 @@ namespace OpenRA
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)
return overrideNodes;
@@ -558,7 +452,7 @@ namespace OpenRA
{
// Append Removal nodes to the result.
// Therefore: we know the remainder of the method deals with a plain node.
if (node.Key.StartsWith('-'))
if (node.Key.StartsWith("-", StringComparison.Ordinal))
{
ret.Add(node);
return;
@@ -574,8 +468,9 @@ namespace OpenRA
// A Removal node is closer than the previous node.
// We should not merge the new node, as the data being merged will jump before the Removal.
// Instead, append it so the previous node is applied, then removed, then the new node is applied.
var previousNodeIndex = LastIndexOfKey(ret, node.Key);
var previousRemovalNodeIndex = LastIndexOfKey(ret, $"-{node.Key}");
var removalKey = $"-{node.Key}";
var previousNodeIndex = ret.FindLastIndex(n => n.Key == node.Key);
var previousRemovalNodeIndex = ret.FindLastIndex(n => n.Key == removalKey);
if (previousRemovalNodeIndex != -1 && previousRemovalNodeIndex > previousNodeIndex)
{
ret.Add(node);
@@ -584,30 +479,13 @@ namespace OpenRA
// A previous node is present with no intervening Removal.
// 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;
}
static int IndexOfKey(List<MiniYamlNode> nodes, string key)
{
// PERF: Avoid LINQ.
for (var i = 0; i < nodes.Count; i++)
if (nodes[i].Key == key)
return i;
return -1;
}
static int LastIndexOfKey(List<MiniYamlNode> nodes, string key)
{
// PERF: Avoid LINQ.
for (var i = nodes.Count - 1; i >= 0; i--)
if (nodes[i].Key == key)
return i;
return -1;
}
public IEnumerable<string> ToLines(string key, string comment = null)
{
var hasKey = !string.IsNullOrEmpty(key);
@@ -630,100 +508,14 @@ namespace OpenRA
files = files.Append(mapFiles);
}
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
IEnumerable<IReadOnlyCollection<MiniYamlNode>> yaml = files.Select(s => FromStream(fileSystem.Open(s), s, stringPool: stringPool));
if (mapRules != null && mapRules.Nodes.Length > 0)
var yaml = files.Select(s => FromStream(fileSystem.Open(s), s));
if (mapRules != null && mapRules.Nodes.Count > 0)
yaml = yaml.Append(mapRules.Nodes);
return Merge(yaml);
}
}
public sealed class MiniYamlNodeBuilder
{
public MiniYamlNode.SourceLocation Location;
public string Key;
public MiniYamlBuilder Value;
public string Comment;
public MiniYamlNodeBuilder(MiniYamlNode node)
{
Location = node.Location;
Key = node.Key;
Value = new MiniYamlBuilder(node.Value);
Comment = node.Comment;
}
public MiniYamlNodeBuilder(string k, MiniYamlBuilder v, string c = null)
{
Key = k;
Value = v;
Comment = c;
}
public MiniYamlNodeBuilder(string k, MiniYamlBuilder v, string c, MiniYamlNode.SourceLocation loc)
: this(k, v, c)
{
Location = loc;
}
public MiniYamlNodeBuilder(string k, string v, string c = null)
: this(k, new MiniYamlBuilder(v, null), c) { }
public MiniYamlNodeBuilder(string k, string v, List<MiniYamlNode> n)
: this(k, new MiniYamlBuilder(v, n), null) { }
public MiniYamlNode Build()
{
return new MiniYamlNode(Key, Value.Build(), Comment, Location);
}
}
public sealed class MiniYamlBuilder
{
public string Value;
public List<MiniYamlNodeBuilder> Nodes;
public MiniYamlBuilder(MiniYaml yaml)
{
Value = yaml.Value;
Nodes = yaml.Nodes.Select(n => new MiniYamlNodeBuilder(n)).ToList();
}
public MiniYamlBuilder(string value)
: this(value, null) { }
public MiniYamlBuilder(string value, List<MiniYamlNode> nodes)
{
Value = value;
Nodes = nodes == null ? new List<MiniYamlNodeBuilder>() : nodes.ConvertAll(x => new MiniYamlNodeBuilder(x));
}
public MiniYaml Build()
{
return new MiniYaml(Value, Nodes.Select(n => n.Build()));
}
public IEnumerable<string> ToLines(string key, string comment = null)
{
var hasKey = !string.IsNullOrEmpty(key);
var hasValue = !string.IsNullOrEmpty(Value);
var hasComment = comment != null;
yield return (hasKey ? key + ":" : "")
+ (hasValue ? " " + Value.Replace("#", "\\#") : "")
+ (hasComment ? (hasKey || hasValue ? " " : "") + "#" + comment : "");
if (Nodes != null)
foreach (var line in Nodes.ToLines())
yield return "\t" + line;
}
public MiniYamlNodeBuilder NodeWithKeyOrDefault(string key)
{
return Nodes.SingleOrDefault(n => n.Key == key);
}
}
[Serializable]
public class YamlException : Exception
{

View File

@@ -33,10 +33,9 @@ namespace OpenRA
public readonly ISpriteLoader[] SpriteLoaders;
public readonly ITerrainLoader TerrainLoader;
public readonly ISpriteSequenceLoader SpriteSequenceLoader;
public readonly IModelSequenceLoader ModelSequenceLoader;
public readonly IVideoLoader[] VideoLoaders;
public readonly HotkeyManager Hotkeys;
public readonly IFileSystemLoader FileSystemLoader;
public ILoadScreen LoadScreen { get; }
public CursorProvider CursorProvider { get; private set; }
public FS ModFiles;
@@ -56,17 +55,11 @@ namespace OpenRA
Manifest = new Manifest(mod.Id, mod.Package);
ObjectCreator = new ObjectCreator(Manifest, mods);
PackageLoaders = ObjectCreator.GetLoaders<IPackageLoader>(Manifest.PackageFormats, "package");
ModFiles = new FS(mod.Id, mods, PackageLoaders);
FileSystemLoader = ObjectCreator.GetLoader<IFileSystemLoader>(Manifest.FileSystem.Value, "filesystem");
FieldLoader.Load(FileSystemLoader, Manifest.FileSystem);
FileSystemLoader.Mount(ModFiles, ObjectCreator);
ModFiles.TrimExcess();
ModFiles.LoadFromManifest(Manifest);
Manifest.LoadCustomData(ObjectCreator);
FluentProvider.Initialize(this, DefaultFileSystem);
if (useLoadScreen)
{
LoadScreen = ObjectCreator.CreateObject<ILoadScreen>(Manifest.LoadScreen.Value);
@@ -97,6 +90,15 @@ namespace OpenRA
SpriteSequenceLoader = (ISpriteSequenceLoader)sequenceCtor.Invoke(new[] { this });
var modelFormat = Manifest.Get<ModelSequenceFormat>();
var modelLoader = ObjectCreator.FindType(modelFormat.Type + "Loader");
var modelCtor = modelLoader?.GetConstructor(new[] { typeof(ModData) });
if (modelLoader == null || !modelLoader.GetInterfaces().Contains(typeof(IModelSequenceLoader)) || modelCtor == null)
throw new InvalidOperationException($"Unable to find a model loader for type '{modelFormat.Type}'.");
ModelSequenceLoader = (IModelSequenceLoader)modelCtor.Invoke(new[] { this });
ModelSequenceLoader.OnMissingModelError = s => Log.Write("debug", s);
Hotkeys = new HotkeyManager(ModFiles, Game.Settings.Keys, Manifest);
defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this));
@@ -132,7 +134,7 @@ namespace OpenRA
// horribly when you use ModData in unexpected ways.
ChromeMetrics.Initialize(this);
ChromeProvider.Initialize(this);
FluentProvider.Initialize(this, fileSystem);
TranslationProvider.Initialize(this, fileSystem);
Game.Sound.Initialize(SoundLoaders, fileSystem);
@@ -166,8 +168,7 @@ namespace OpenRA
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, stringPool: stringPool)).ToArray();
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s)).ToArray();
}
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>
void StartGame(Arguments args);
}
public interface IFileSystemLoader
{
void Mount(FS fileSystem, ObjectCreator objectCreator);
}
}

View File

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

View File

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

View File

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

View File

@@ -20,16 +20,16 @@ namespace OpenRA.Network
public string Version;
public string AuthToken;
public static HandshakeRequest Deserialize(string data, string name)
public static HandshakeRequest Deserialize(string data)
{
var handshake = new HandshakeRequest();
FieldLoader.Load(handshake, MiniYaml.FromString(data, name).First().Value);
FieldLoader.Load(handshake, MiniYaml.FromString(data).First().Value);
return handshake;
}
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();
}
}
@@ -51,14 +51,14 @@ namespace OpenRA.Network
[FieldLoader.Ignore]
public Session.Client Client;
public static HandshakeResponse Deserialize(string data, string name)
public static HandshakeResponse Deserialize(string data)
{
var handshake = new HandshakeResponse
{
Client = new Session.Client()
};
var ys = MiniYaml.FromString(data, name);
var ys = MiniYaml.FromString(data);
foreach (var y in ys)
{
switch (y.Key)
@@ -79,9 +79,9 @@ namespace OpenRA.Network
{
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("Client", FieldSaver.Save(Client))
new MiniYamlNode("Client", FieldSaver.Save(Client))
};
return data.WriteToString();

View File

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

View File

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

View File

@@ -78,8 +78,7 @@ namespace OpenRA
readonly Target target;
readonly Target visualFeedbackTarget;
Order(string orderString, Actor subject, in Target target, string targetString, bool queued,
Actor[] extraActors, CPos extraLocation, uint extraData, Actor[] groupedActors = null)
Order(string orderString, Actor subject, in Target target, string targetString, bool queued, Actor[] extraActors, CPos extraLocation, uint extraData, Actor[] groupedActors = null)
{
OrderString = orderString ?? "";
Subject = subject;
@@ -157,18 +156,7 @@ namespace OpenRA
else
{
var pos = new WPos(r.ReadInt32(), r.ReadInt32(), r.ReadInt32());
var numberOfTerrainPositions = r.ReadInt16();
if (numberOfTerrainPositions == -1)
target = Target.FromPos(pos);
else
{
var terrainPositions = new WPos[numberOfTerrainPositions];
for (var i = 0; i < numberOfTerrainPositions; i++)
terrainPositions[i] = new WPos(r.ReadInt32(), r.ReadInt32(), r.ReadInt32());
target = Target.FromSerializedTerrainPosition(pos, terrainPositions);
}
target = Target.FromPos(pos);
}
break;
@@ -271,15 +259,7 @@ namespace OpenRA
public static Order FromGroupedOrder(Order grouped, Actor subject)
{
return new Order(
grouped.OrderString,
subject,
grouped.Target,
grouped.TargetString,
grouped.Queued,
grouped.ExtraActors,
grouped.ExtraLocation,
grouped.ExtraData);
return new Order(grouped.OrderString, subject, grouped.Target, grouped.TargetString, grouped.Queued, grouped.ExtraActors, grouped.ExtraLocation, grouped.ExtraData);
}
public static Order Command(string text)
@@ -408,21 +388,6 @@ namespace OpenRA
w.Write(targetState.Pos.X);
w.Write(targetState.Pos.Y);
w.Write(targetState.Pos.Z);
// Don't send extra data over the network that will be restored by the Target ctor
var terrainPositions = targetState.TerrainPositions.Length;
if (terrainPositions == 1 && targetState.TerrainPositions[0] == targetState.Pos)
w.Write((short)-1);
else
{
w.Write((short)terrainPositions);
foreach (var position in targetState.TerrainPositions)
{
w.Write(position.X);
w.Write(position.Y);
w.Write(position.Z);
}
}
}
break;
@@ -465,7 +430,7 @@ namespace OpenRA
public override string ToString()
{
return $"OrderString: \"{OrderString}\" \n\t Type: \"{Type}\". \n\t Subject: \"{Subject}\". \n\t Target: \"{Target}\"." +
$"\n\t TargetString: \"{TargetString}\".\n\t IsImmediate: {IsImmediate}.\n\t Player(PlayerName): {Player?.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.
data = new MemoryStream();
foreach (var o in orders)
data.Write(o.Serialize());
data.WriteArray(o.Serialize());
}
public OrderPacket(MemoryStream data)
@@ -55,7 +55,7 @@ namespace OpenRA.Network
public byte[] Serialize(int frame)
{
var ms = new MemoryStream((int)data.Length + 4);
ms.Write(frame);
ms.WriteArray(BitConverter.GetBytes(frame));
data.Position = 0;
data.CopyTo(ms);
@@ -83,19 +83,19 @@ namespace OpenRA.Network
public static byte[] SerializeSync((int Frame, int SyncHash, ulong DefeatState) data)
{
var ms = new MemoryStream(4 + Order.SyncHashOrderLength);
ms.Write(data.Frame);
ms.WriteArray(BitConverter.GetBytes(data.Frame));
ms.WriteByte((byte)OrderType.SyncHash);
ms.Write(data.SyncHash);
ms.Write(data.DefeatState);
ms.WriteArray(BitConverter.GetBytes(data.SyncHash));
ms.WriteArray(BitConverter.GetBytes(data.DefeatState));
return ms.GetBuffer();
}
public static byte[] SerializePingResponse(long timestamp, byte queueLength)
{
var ms = new MemoryStream(14);
ms.Write(0);
ms.WriteArray(BitConverter.GetBytes(0));
ms.WriteByte((byte)OrderType.Ping);
ms.Write(timestamp);
ms.WriteArray(BitConverter.GetBytes(timestamp));
ms.WriteByte(queueLength);
return ms.GetBuffer();
}

View File

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

View File

@@ -69,7 +69,7 @@ namespace OpenRA.Network
if (o.OrderString == "StartGame")
IsValid = true;
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);
}
@@ -92,8 +92,8 @@ namespace OpenRA.Network
public void ReceiveFrame(int clientID, int frame, byte[] data)
{
var ms = new MemoryStream(4 + data.Length);
ms.Write(frame);
ms.Write(data);
ms.WriteArray(BitConverter.GetBytes(frame));
ms.WriteArray(data);
Receive(clientID, ms.GetBuffer());
}

View File

@@ -41,13 +41,13 @@ namespace OpenRA.Network
return null;
}
public static Session Deserialize(string data, string name)
public static Session Deserialize(string data)
{
try
{
var session = new Session();
var nodes = MiniYaml.FromString(data, name);
var nodes = MiniYaml.FromString(data);
foreach (var node in nodes)
{
var strings = node.Key.Split('@');
@@ -227,7 +227,7 @@ namespace OpenRA.Network
{
var gs = FieldLoader.Load<Global>(data);
var optionsNode = data.NodeWithKeyOrDefault("Options");
var optionsNode = data.Nodes.FirstOrDefault(n => n.Key == "Options");
if (optionsNode != null)
foreach (var n in optionsNode.Value.Nodes)
gs.LobbyOptions[n.Key] = FieldLoader.Load<LobbyOptionState>(n.Value);
@@ -238,9 +238,8 @@ namespace OpenRA.Network
public MiniYamlNode Serialize()
{
var data = new MiniYamlNode("GlobalSettings", FieldSaver.Save(this));
var options = LobbyOptions.Select(kv => new MiniYamlNode(kv.Key, FieldSaver.Save(kv.Value)));
data = data.WithValue(data.Value.WithNodesAppended(
new[] { new MiniYamlNode("Options", new MiniYaml(null, options)) }));
var options = LobbyOptions.Select(kv => new MiniYamlNode(kv.Key, FieldSaver.Save(kv.Value))).ToList();
data.Value.Nodes.Add(new MiniYamlNode("Options", new MiniYaml(null, options)));
return data;
}
@@ -265,7 +264,7 @@ namespace OpenRA.Network
{
var sessionData = new List<MiniYamlNode>()
{
new("DisabledSpawnPoints", FieldSaver.FormatValue(DisabledSpawnPoints))
new MiniYamlNode("DisabledSpawnPoints", FieldSaver.FormatValue(DisabledSpawnPoints))
};
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})");
if (Game.IsHost)
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", $"SharedRandom: {r.SyncedRandom} (#{r.TotalCount})");
Log.Write("sync", "Synced Traits:");
@@ -201,12 +201,8 @@ namespace OpenRA.Network
public TypeInfo(Type type)
{
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
var fields = type.GetFields(Flags)
.Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>())
.ToList();
var properties = type.GetProperties(Flags)
.Where(pi => pi.HasAttribute<SyncAttribute>())
.ToList();
var fields = type.GetFields(Flags).Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>());
var properties = type.GetProperties(Flags).Where(pi => pi.HasAttribute<SyncAttribute>());
foreach (var prop in properties)
if (!prop.CanRead || prop.GetIndexParameters().Length > 0)
@@ -304,7 +300,7 @@ namespace OpenRA.Network
public object this[int index]
{
readonly get
get
{
if (item2OrSentinel == Sentinel)
return ((object[])item1OrArray)[index];

View File

@@ -20,24 +20,6 @@ namespace OpenRA.Network
{
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; }
static Player FindPlayerByClient(this World world, Session.Client c)
@@ -56,352 +38,344 @@ namespace OpenRA.Network
TextNotificationsManager.AddSystemLine(order.TargetString);
break;
// Client side resolved server message
case "FluentMessage":
{
if (string.IsNullOrEmpty(order.TargetString))
break;
var yaml = MiniYaml.FromString(order.TargetString, order.OrderString);
foreach (var node in yaml)
// Client side translated server message
case "LocalizedMessage":
{
var message = new FluentMessage(node.Value);
if (message.Key == Joined)
TextNotificationsManager.AddPlayerJoinedLine(message.Key, message.Arguments);
else if (message.Key == Left)
TextNotificationsManager.AddPlayerLeftLine(message.Key, message.Arguments);
else
TextNotificationsManager.AddSystemLine(message.Key, message.Arguments);
}
if (string.IsNullOrEmpty(order.TargetString))
break;
break;
}
var yaml = MiniYaml.FromString(order.TargetString);
foreach (var node in yaml)
{
var localizedMessage = new LocalizedMessage(node.Value);
TextNotificationsManager.AddSystemLine(localizedMessage.TranslatedText);
}
break;
}
case "DisableChatEntry":
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
// Server may send MaxValue to indicate that it is disabled until further notice
if (order.ExtraData == uint.MaxValue)
TextNotificationsManager.ChatDisabledUntil = uint.MaxValue;
else
TextNotificationsManager.ChatDisabledUntil = Game.RunTime + order.ExtraData;
break;
// Server may send MaxValue to indicate that it is disabled until further notice
if (order.ExtraData == uint.MaxValue)
TextNotificationsManager.ChatDisabledUntil = uint.MaxValue;
else
TextNotificationsManager.ChatDisabledUntil = Game.RunTime + order.ExtraData;
break;
}
}
case "StartKickVote":
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
KickVoteTarget = (int)order.ExtraData;
break;
}
KickVoteTarget = (int)order.ExtraData;
break;
}
case "EndKickVote":
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
if (KickVoteTarget == (int)order.ExtraData)
KickVoteTarget = null;
break;
if (KickVoteTarget == (int)order.ExtraData)
KickVoteTarget = null;
break;
}
}
case "Chat":
{
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
if (client == null)
break;
// Cut chat messages to the hard limit to avoid exploits
var message = order.TargetString;
if (message.Length > ChatMessageMaxLength)
message = order.TargetString[..ChatMessageMaxLength];
// ExtraData 0 means this is a normal chat order, everything else is team chat
if (order.ExtraData == 0)
{
var p = world?.FindPlayerByClient(client);
var suffix = (p != null && p.WinState == WinState.Lost) ? " (Dead)" : "";
suffix = client.IsObserver ? " (Spectator)" : suffix;
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
if (client == null)
break;
if (orderManager.LocalClient != null && client != orderManager.LocalClient && client.Team > 0 && client.Team == orderManager.LocalClient.Team)
suffix += " (Ally)";
// Cut chat messages to the hard limit to avoid exploits
var message = order.TargetString;
if (message.Length > ChatMessageMaxLength)
message = order.TargetString[..ChatMessageMaxLength];
TextNotificationsManager.AddChatLine(clientId, client.Name + suffix, message, client.Color);
break;
}
// ExtraData 0 means this is a normal chat order, everything else is team chat
if (order.ExtraData == 0)
{
var p = world?.FindPlayerByClient(client);
var suffix = (p != null && p.WinState == WinState.Lost) ? " (Dead)" : "";
suffix = client.IsObserver ? " (Spectator)" : suffix;
// We are still in the lobby
if (world == null)
{
var prefix = order.ExtraData == uint.MaxValue ? "[Spectators] " : "[Team] ";
if (orderManager.LocalClient != null && client.Team == orderManager.LocalClient.Team)
TextNotificationsManager.AddChatLine(clientId, prefix + client.Name, message, client.Color);
if (orderManager.LocalClient != null && client != orderManager.LocalClient && client.Team > 0 && client.Team == orderManager.LocalClient.Team)
suffix += " (Ally)";
TextNotificationsManager.AddChatLine(clientId, client.Name + suffix, message, client.Color);
break;
}
// We are still in the lobby
if (world == null)
{
var prefix = order.ExtraData == uint.MaxValue ? "[Spectators] " : "[Team] ";
if (orderManager.LocalClient != null && client.Team == orderManager.LocalClient.Team)
TextNotificationsManager.AddChatLine(clientId, prefix + client.Name, message, client.Color);
break;
}
var player = world.FindPlayerByClient(client);
var localClientIsObserver = world.IsReplay || (orderManager.LocalClient != null && orderManager.LocalClient.IsObserver)
|| (world.LocalPlayer != null && world.LocalPlayer.WinState != WinState.Undefined);
// ExtraData gives us the team number, uint.MaxValue means Spectators
if (order.ExtraData == uint.MaxValue && localClientIsObserver)
{
// Validate before adding the line
if (client.IsObserver || (player != null && player.WinState != WinState.Undefined))
TextNotificationsManager.AddChatLine(clientId, "[Spectators] " + client.Name, message, client.Color);
break;
}
var valid = client.Team == order.ExtraData && player != null && player.WinState == WinState.Undefined;
var isSameTeam = orderManager.LocalClient != null && order.ExtraData == orderManager.LocalClient.Team
&& world.LocalPlayer != null && world.LocalPlayer.WinState == WinState.Undefined;
if (valid && (isSameTeam || world.IsReplay))
TextNotificationsManager.AddChatLine(clientId, "[Team" + (world.IsReplay ? " " + order.ExtraData : "") + "] " + client.Name, message, client.Color);
break;
}
var player = world.FindPlayerByClient(client);
var localClientIsObserver = world.IsReplay || (orderManager.LocalClient != null && orderManager.LocalClient.IsObserver)
|| (world.LocalPlayer != null && world.LocalPlayer.WinState != WinState.Undefined);
// ExtraData gives us the team number, uint.MaxValue means Spectators
if (order.ExtraData == uint.MaxValue && localClientIsObserver)
{
// Validate before adding the line
if (client.IsObserver || (player != null && player.WinState != WinState.Undefined))
TextNotificationsManager.AddChatLine(clientId, "[Spectators] " + client.Name, message, client.Color);
break;
}
var valid = client.Team == order.ExtraData && player != null && player.WinState == WinState.Undefined;
var isSameTeam = orderManager.LocalClient != null && order.ExtraData == orderManager.LocalClient.Team
&& world.LocalPlayer != null && world.LocalPlayer.WinState == WinState.Undefined;
if (valid && (isSameTeam || world.IsReplay))
TextNotificationsManager.AddChatLine(clientId, "[Team" + (world.IsReplay ? " " + order.ExtraData : "") + "] " + client.Name, message, client.Color);
break;
}
case "StartGame":
{
if (Game.ModData.MapCache[orderManager.LobbyInfo.GlobalSettings.Map].Status != MapStatus.Available)
{
Game.Disconnect();
Game.LoadShellMap();
if (Game.ModData.MapCache[orderManager.LobbyInfo.GlobalSettings.Map].Status != MapStatus.Available)
{
Game.Disconnect();
Game.LoadShellMap();
// TODO: After adding a startup error dialog, notify the replay load failure.
// TODO: After adding a startup error dialog, notify the replay load failure.
break;
}
if (!string.IsNullOrEmpty(order.TargetString))
{
var data = MiniYaml.FromString(order.TargetString);
var saveLastOrdersFrame = data.FirstOrDefault(n => n.Key == "SaveLastOrdersFrame");
if (saveLastOrdersFrame != null)
orderManager.GameSaveLastFrame =
FieldLoader.GetValue<int>("saveLastOrdersFrame", saveLastOrdersFrame.Value.Value);
var saveSyncFrame = data.FirstOrDefault(n => n.Key == "SaveSyncFrame");
if (saveSyncFrame != null)
orderManager.GameSaveLastSyncFrame =
FieldLoader.GetValue<int>("SaveSyncFrame", saveSyncFrame.Value.Value);
}
else
TextNotificationsManager.AddSystemLine("The game has started.");
Game.StartGame(orderManager.LobbyInfo.GlobalSettings.Map, WorldType.Regular);
break;
}
if (!string.IsNullOrEmpty(order.TargetString))
{
var data = MiniYaml.FromString(order.TargetString, order.OrderString);
var saveLastOrdersFrame = data.FirstOrDefault(n => n.Key == "SaveLastOrdersFrame");
if (saveLastOrdersFrame != null)
orderManager.GameSaveLastFrame =
FieldLoader.GetValue<int>("saveLastOrdersFrame", saveLastOrdersFrame.Value.Value);
var saveSyncFrame = data.FirstOrDefault(n => n.Key == "SaveSyncFrame");
if (saveSyncFrame != null)
orderManager.GameSaveLastSyncFrame =
FieldLoader.GetValue<int>("SaveSyncFrame", saveSyncFrame.Value.Value);
}
else
TextNotificationsManager.AddSystemLine(GameStarted);
Game.StartGame(orderManager.LobbyInfo.GlobalSettings.Map, WorldType.Regular);
break;
}
case "SaveTraitData":
{
var data = MiniYaml.FromString(order.TargetString, order.OrderString)[0];
var traitIndex = Exts.ParseInt32Invariant(data.Key);
{
var data = MiniYaml.FromString(order.TargetString)[0];
var traitIndex = int.Parse(data.Key);
world?.AddGameSaveTraitData(traitIndex, data.Value);
world?.AddGameSaveTraitData(traitIndex, data.Value);
break;
}
break;
}
case "GameSaved":
if (!orderManager.World.IsReplay)
TextNotificationsManager.AddSystemLine(GameSaved);
TextNotificationsManager.AddSystemLine("Game saved");
foreach (var nsr in orderManager.World.WorldActor.TraitsImplementing<INotifyGameSaved>())
nsr.GameSaved(orderManager.World);
break;
case "PauseGame":
{
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
if (client != null)
{
var pause = order.TargetString == "Pause";
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
if (client != null)
{
var pause = order.TargetString == "Pause";
// Prevent injected unpause orders from restarting a finished game
if (orderManager.World.IsGameOver && !pause)
break;
// Prevent injected unpause orders from restarting a finished game
if (orderManager.World.IsGameOver && !pause)
break;
if (orderManager.World.Paused != pause && world != null && world.LobbyInfo.NonBotClients.Count() > 1)
TextNotificationsManager.AddSystemLine(pause ? GamePaused : GameUnpaused, "player", client.Name);
if (orderManager.World.Paused != pause && world != null && world.LobbyInfo.NonBotClients.Count() > 1)
{
var pausetext = $"The game is {(pause ? "paused" : "un-paused")} by {client.Name}";
TextNotificationsManager.AddSystemLine(pausetext);
}
orderManager.World.Paused = pause;
orderManager.World.PredictedPaused = pause;
orderManager.World.Paused = pause;
orderManager.World.PredictedPaused = pause;
}
break;
}
break;
}
case "HandshakeRequest":
{
// Switch to the server's mod if we need and are able to
var mod = Game.ModData.Manifest;
var request = HandshakeRequest.Deserialize(order.TargetString, order.OrderString);
var externalKey = ExternalMod.MakeKey(request.Mod, request.Version);
if ((request.Mod != mod.Id || request.Version != mod.Metadata.Version) &&
Game.ExternalMods.TryGetValue(externalKey, out var external))
{
// The ConnectionFailedLogic will prompt the user to switch mods
CurrentServerSettings.ServerExternalMod = external;
orderManager.Connection.Dispose();
// Switch to the server's mod if we need and are able to
var mod = Game.ModData.Manifest;
var request = HandshakeRequest.Deserialize(order.TargetString);
var externalKey = ExternalMod.MakeKey(request.Mod, request.Version);
if ((request.Mod != mod.Id || request.Version != mod.Metadata.Version) &&
Game.ExternalMods.TryGetValue(externalKey, out var external))
{
// The ConnectionFailedLogic will prompt the user to switch mods
CurrentServerSettings.ServerExternalMod = external;
orderManager.Connection.Dispose();
break;
}
Game.Settings.Player.Name = Settings.SanitizedPlayerName(Game.Settings.Player.Name);
Game.Settings.Save();
// Otherwise send the handshake with our current settings and let the server reject us
var info = new Session.Client()
{
Name = Game.Settings.Player.Name,
PreferredColor = Game.Settings.Player.Color,
Color = Game.Settings.Player.Color,
Faction = "Random",
SpawnPoint = 0,
Team = 0,
State = Session.ClientState.Invalid
};
var localProfile = Game.LocalPlayerProfile;
var response = new HandshakeResponse()
{
Client = info,
Mod = mod.Id,
Version = mod.Metadata.Version,
Password = CurrentServerSettings.Password,
Fingerprint = localProfile.Fingerprint,
OrdersProtocol = ProtocolVersion.Orders
};
if (request.AuthToken != null && response.Fingerprint != null)
response.AuthSignature = localProfile.Sign(request.AuthToken);
orderManager.IssueOrder(new Order("HandshakeResponse", null, false)
{
Type = OrderType.Handshake,
IsImmediate = true,
TargetString = response.Serialize()
});
break;
}
Game.Settings.Player.Name = Settings.SanitizedPlayerName(Game.Settings.Player.Name);
Game.Settings.Save();
// Otherwise send the handshake with our current settings and let the server reject us
var info = new Session.Client()
{
Name = Game.Settings.Player.Name,
PreferredColor = Game.Settings.Player.Color,
Color = Game.Settings.Player.Color,
Faction = "Random",
SpawnPoint = 0,
Team = 0,
State = Session.ClientState.Invalid
};
var localProfile = Game.LocalPlayerProfile;
var response = new HandshakeResponse()
{
Client = info,
Mod = mod.Id,
Version = mod.Metadata.Version,
Password = CurrentServerSettings.Password,
Fingerprint = localProfile.Fingerprint,
OrdersProtocol = ProtocolVersion.Orders
};
if (request.AuthToken != null && response.Fingerprint != null)
response.AuthSignature = localProfile.Sign(request.AuthToken);
orderManager.IssueOrder(new Order("HandshakeResponse", null, false)
{
Type = OrderType.Handshake,
IsImmediate = true,
TargetString = response.Serialize()
});
break;
}
case "ServerError":
{
orderManager.ServerError = order.TargetString;
orderManager.AuthenticationFailed = false;
break;
}
{
orderManager.ServerError = order.TargetString;
orderManager.AuthenticationFailed = false;
break;
}
case "AuthenticationError":
{
// The ConnectionFailedLogic will prompt the user for the password
orderManager.ServerError = order.TargetString;
orderManager.AuthenticationFailed = true;
break;
}
{
// The ConnectionFailedLogic will prompt the user for the password
orderManager.ServerError = order.TargetString;
orderManager.AuthenticationFailed = true;
break;
}
case "SyncInfo":
{
orderManager.LobbyInfo = Session.Deserialize(order.TargetString, order.OrderString);
Game.SyncLobbyInfo();
break;
}
{
orderManager.LobbyInfo = Session.Deserialize(order.TargetString);
Game.SyncLobbyInfo();
break;
}
case "SyncLobbyClients":
{
var clients = new List<Session.Client>();
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString);
foreach (var node in nodes)
{
var strings = node.Key.Split('@');
if (strings[0] == "Client")
clients.Add(Session.Client.Deserialize(node.Value));
}
var clients = new List<Session.Client>();
var nodes = MiniYaml.FromString(order.TargetString);
foreach (var node in nodes)
{
var strings = node.Key.Split('@');
if (strings[0] == "Client")
clients.Add(Session.Client.Deserialize(node.Value));
}
orderManager.LobbyInfo.Clients = clients;
Game.SyncLobbyInfo();
break;
}
orderManager.LobbyInfo.Clients = clients;
Game.SyncLobbyInfo();
break;
}
case "SyncLobbySlots":
{
var slots = new Dictionary<string, Session.Slot>();
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString);
foreach (var node in nodes)
{
var strings = node.Key.Split('@');
if (strings[0] == "Slot")
var slots = new Dictionary<string, Session.Slot>();
var nodes = MiniYaml.FromString(order.TargetString);
foreach (var node in nodes)
{
var slot = Session.Slot.Deserialize(node.Value);
slots.Add(slot.PlayerReference, slot);
var strings = node.Key.Split('@');
if (strings[0] == "Slot")
{
var slot = Session.Slot.Deserialize(node.Value);
slots.Add(slot.PlayerReference, slot);
}
}
}
orderManager.LobbyInfo.Slots = slots;
Game.SyncLobbyInfo();
break;
}
orderManager.LobbyInfo.Slots = slots;
Game.SyncLobbyInfo();
break;
}
case "SyncLobbyGlobalSettings":
{
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString);
foreach (var node in nodes)
{
var strings = node.Key.Split('@');
if (strings[0] == "GlobalSettings")
orderManager.LobbyInfo.GlobalSettings = Session.Global.Deserialize(node.Value);
}
var nodes = MiniYaml.FromString(order.TargetString);
foreach (var node in nodes)
{
var strings = node.Key.Split('@');
if (strings[0] == "GlobalSettings")
orderManager.LobbyInfo.GlobalSettings = Session.Global.Deserialize(node.Value);
}
Game.SyncLobbyInfo();
break;
}
Game.SyncLobbyInfo();
break;
}
case "SyncConnectionQuality":
{
var nodes = MiniYaml.FromString(order.TargetString, order.OrderString);
foreach (var node in nodes)
{
var strings = node.Key.Split('@');
if (strings[0] == "ConnectionQuality")
var nodes = MiniYaml.FromString(order.TargetString);
foreach (var node in nodes)
{
var client = orderManager.LobbyInfo.Clients.FirstOrDefault(c => c.Index == Exts.ParseInt32Invariant(strings[1]));
if (client != null)
client.ConnectionQuality = FieldLoader.GetValue<Session.ConnectionQuality>("ConnectionQuality", node.Value.Value);
var strings = node.Key.Split('@');
if (strings[0] == "ConnectionQuality")
{
var client = orderManager.LobbyInfo.Clients.FirstOrDefault(c => c.Index == int.Parse(strings[1]));
if (client != null)
client.ConnectionQuality = FieldLoader.GetValue<Session.ConnectionQuality>("ConnectionQuality", node.Value.Value);
}
}
break;
}
break;
}
case "SyncMapPool":
{
orderManager.ServerMapPool = FieldLoader.GetValue<HashSet<string>>("SyncMapPool", order.TargetString);
break;
}
default:
{
if (world == null)
{
if (world == null)
break;
if (order.GroupedActors == null)
ResolveOrder(order, world, orderManager, clientId);
else
foreach (var subject in order.GroupedActors)
ResolveOrder(Order.FromGroupedOrder(order, subject), world, orderManager, clientId);
break;
if (order.GroupedActors == null)
ResolveOrder(order, world, orderManager, clientId);
else
foreach (var subject in order.GroupedActors)
ResolveOrder(Order.FromGroupedOrder(order, subject), world, orderManager, clientId);
break;
}
}
}
}

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