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
3080 changed files with 39958 additions and 115922 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

@@ -16,6 +16,7 @@ assignees: ''
- OpenRA Version: ENGINE VERSION
- OpenRA Mod: GAME MOD
- Source: Official download package OR self-compiled OR third-party package
- For self-compiled or third-party packages: Mono version
## Exception log

View File

@@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily

View File

@@ -2,30 +2,25 @@ name: Continuous Integration
on:
push:
paths-ignore:
- '*.md'
pull_request:
branches: [ bleed, 'prep-*' ]
paths-ignore:
- '*.md'
- '*.py'
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
linux:
name: Linux (.NET 8.0)
name: Linux (.NET 6.0)
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v6
uses: actions/checkout@v3
- name: Install .NET 8.0
uses: actions/setup-dotnet@v5
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
dotnet-version: '6.0.x'
- name: Check Code
run: |
@@ -38,18 +33,36 @@ jobs:
make check-scripts
make TREAT_WARNINGS_AS_ERRORS=true test
windows:
name: Windows (.NET 8.0)
runs-on: windows-2022
linux-mono:
name: Linux (mono)
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v6
uses: actions/checkout@v3
- name: Install .NET 8.0
uses: actions/setup-dotnet@v5
- name: Check Code
run: |
mono --version
make RUNTIME=mono check
- name: Check Mods
run: |
# check-scripts does not depend on .net/mono, so is not needed here
make RUNTIME=mono TREAT_WARNINGS_AS_ERRORS=true test
windows:
name: Windows (.NET 6.0)
runs-on: windows-2019
steps:
- name: Clone Repository
uses: actions/checkout@v3
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
dotnet-version: '6.0.x'
- name: Check Code
shell: powershell
@@ -61,7 +74,7 @@ jobs:
- name: Check Mods
run: |
choco install lua --version 5.1.5.52 --no-progress
choco install lua --version 5.1.5.52
$ENV:Path = $ENV:Path + ";C:\Program Files (x86)\Lua\5.1\"
$ENV:TREAT_WARNINGS_AS_ERRORS = "true"
.\make.ps1 check-scripts

View File

@@ -1,9 +1,6 @@
name: Deploy Documentation
on:
push:
branches: [ bleed ]
tags: [ 'release-*', 'playtest-*' ]
workflow_dispatch:
inputs:
tag:
@@ -15,136 +12,132 @@ 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@v6
uses: actions/checkout@v3
with:
ref: ${{ needs.prepare.outputs.git_tag }}
ref: ${{ github.event.inputs.tag }}
- name: Install .NET 8
uses: actions/setup-dotnet@v5
- name: Install .NET 6
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
dotnet-version: '6.0.x'
- name: Prepare Environment
run: |
make all
- name: Clone Wiki
uses: actions/checkout@v6
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@v6
uses: actions/checkout@v3
with:
ref: ${{ needs.prepare.outputs.git_tag }}
ref: ${{ github.event.inputs.tag }}
- name: Install .NET 8
uses: actions/setup-dotnet@v5
- name: Install .NET 6
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
dotnet-version: '6.0.x'
- name: Prepare Environment
run: |
make all
# version_type is release/playtest/bleed - the name of the target branch.
- name: Clone docs.openra.net
uses: actions/checkout@v6
- 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@v6
uses: actions/checkout@v3
- name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -40,12 +40,12 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v6
uses: actions/checkout@v3
- name: Install .NET 8.0
uses: actions/setup-dotnet@v5
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
dotnet-version: '6.0.x'
- name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -53,27 +53,29 @@ 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-14
runs-on: macos-11
steps:
- name: Clone Repository
uses: actions/checkout@v6
uses: actions/checkout@v3
- name: Install .NET 8.0
uses: actions/setup-dotnet@v5
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
dotnet-version: '6.0.x'
- name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -90,23 +92,25 @@ 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@v6
uses: actions/checkout@v3
- name: Install .NET 8.0
uses: actions/setup-dotnet@v5
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
dotnet-version: '6.0.x'
- name: Prepare Environment
run: |
@@ -119,37 +123,11 @@ jobs:
mkdir -p build/windows
./packaging/windows/buildpackage.sh "${GIT_TAG}" "${PWD}/build/windows"
- name: Upload Installer
id: unsigned-artifact
if: github.repository == 'openra/openra'
uses: actions/upload-artifact@v6
with:
path: build/windows/*.exe
- name: Microsoft Authenticode
id: signpath
if: github.repository == 'openra/openra'
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '${{ secrets.SIGNPATH_ORGANISATION_ID }}'
project-slug: 'OpenRA'
signing-policy-slug: 'release-signing'
github-artifact-id: '${{ steps.unsigned-artifact.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'build/windows/signed/'
parameters: |
version: ${{ toJSON(github.ref_name) }}
- name: Upload Packages
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
gh release upload "${{ github.ref_name }}" build/windows/*.zip
if [ "${GITHUB_REPOSITORY}" = "openra/openra" ]; then
gh release upload "${{ github.ref_name }}" build/windows/signed/*.exe
else
gh release upload "${{ github.ref_name }}" build/windows/*.exe
fi
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
overwrite: true
file_glob: true
file: build/windows/*

4
.gitignore vendored
View File

@@ -25,6 +25,10 @@ IP2LOCATION-LITE-DB1.IPV6.BIN.ZIP
\#*
.*.sw?
# Monodevelop
*.pidb
*.userprefs
# Mac OS X
.DS_Store

63
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)
@@ -36,7 +37,6 @@ Also thanks to:
* Andreas Beck (baxtor)
* Ang Soon Li (asl97)
* Arik Lirette (Angusm3)
* Ashley Newson
* Barnaby Smith (mvi)
* Bellator
* Bernd Stellwag (burned42)
@@ -55,7 +55,6 @@ Also thanks to:
* Constantin Helmig (CH4Code)
* D2k Sardaukar
* D'Arcy Rush (r34ch)
* Dan Stoian (stoiandan)
* Daniel Derejvanik (Harisson)
* Danny Keary (Dan9550)
* David Jiménez (Rydra)
@@ -99,7 +98,6 @@ Also thanks to:
* Kanar
* Kenny Hoxworth (hoxworth)
* Kevin Azzam (ChaoticMind)
* Kevin Streser
* Krishnakanth Mallik
* Kyle Smith (Smitty)
* Kyrre Soerensen (zypres)
@@ -143,7 +141,6 @@ Also thanks to:
* Riderr3
* riiga
* Rikhardur Bjarni Einarsson (WolfGaming)
* Robert Nordan (robpvn)
* Sascha Biedermann (bidifx)
* Sean Hunt (coppro)
* Shawn Collins (UberWaffe)
@@ -153,7 +150,6 @@ Also thanks to:
* Teemu Nieminen (Temeez)
* Thomas Christlieb (ThomasChr)
* Tim Mylemans (gecko)
* Tinix
* Tirili
* Tomas Einarsson (Mesacer)
* Tom van Leth (tovl)
@@ -165,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.
@@ -24,6 +24,6 @@ Don't forget to add yourself to [AUTHORS](https://github.com/OpenRA/OpenRA/blob/
Please propose a [CHANGELOG](https://github.com/OpenRA/OpenRA/wiki/CHANGELOG) entry in the pull-request comments.
While your pull-request is in review it will be helpful if you join [Discord](https://discord.openra.net) or [IRC](ircs://irc.libera.chat:6697/openra) to discuss the changes.
While your pull-request is in review it will be helpful if you join [IRC](irc://chat.freenode.net/openra) to discuss the changes.
See also the in-depth guide on [contributing](https://github.com/OpenRA/OpenRA/wiki/Contributing) to the OpenRA project.

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

@@ -5,7 +5,7 @@
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Optimize>true</Optimize>
<LangVersion>12</LangVersion>
<LangVersion>9</LangVersion>
<DebugSymbols>true</DebugSymbols>
<EngineRootPath Condition="'$(EngineRootPath)' == ''">..</EngineRootPath>
<OutputPath>$(EngineRootPath)/bin</OutputPath>
@@ -18,7 +18,8 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework Condition="'$(MSBuildRuntimeType)'!='Mono'">net6.0</TargetFramework>
<TargetFramework Condition="'$(MSBuildRuntimeType)'=='Mono'">netstandard2.1</TargetFramework>
</PropertyGroup>
<PropertyGroup>
@@ -32,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>
@@ -50,10 +51,8 @@
</ItemGroup>
</Target>
<!-- StyleCop/Roslynator -->
<!-- StyleCop -->
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="All" />
<PackageReference Include="Roslynator.Analyzers" Version="4.13.0" PrivateAssets="All" />
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.13.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0
FROM mcr.microsoft.com/dotnet/sdk:6.0
RUN \
apt-get update; \

View File

@@ -7,8 +7,8 @@ Windows
=======
Compiling OpenRA requires the following dependencies:
* [Windows PowerShell >= 4.0](https://microsoft.com/powershell) (included by default in recent Windows 10 versions)
* [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) (or via Visual Studio)
* [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.
@@ -17,13 +17,15 @@ Run the game with `launch-game.cmd`. It can be handed arguments that specify the
Linux
=====
.NET 8 is required to compile OpenRA. The [.NET 8 download page](https://dotnet.microsoft.com/download/dotnet/8.0) provides repositories for various package managers and binary releases for several architectures.
.NET 6 or Mono (version 6.12 or later) is required to compile OpenRA. We recommend using .NET 6 when possible, as Mono is poorly packaged by most Linux distributions (e.g. missing the required `msbuild` toolchain), and has been deprecated as a standalone project.
To compile OpenRA, run `make` from the command line. After this one can run the game with `./launch-game.sh`. It is also possible to specify the mod you wish to run from the command line, e.g. with `./launch-game.sh Game.Mod=ts` if you wish to try the experimental Tiberian Sun mod.
The [.NET 6 download page](https://dotnet.microsoft.com/download/dotnet/6.0) provides repositories for various package managers and binary releases for several architectures. If you prefer to use Mono, we suggest adding the [upstream repository](https://www.mono-project.com/download/stable/#download-lin) for your distro to obtain the latest version and the `msbuild` toolchain.
To compile OpenRA, run `make` from the command line (or `make RUNTIME=mono` if using Mono). After this one can run the game with `./launch-game.sh`. It is also possible to specify the mod you wish to run from the command line, e.g. with `./launch-game.sh Game.Mod=ts` if you wish to try the experimental Tiberian Sun mod.
The default behaviour on the x86_64 architecture is to download several pre-compiled native libraries using the Nuget packaging manager. If you prefer to use system libraries, compile instead using `make TARGETPLATFORM=unix-generic`.
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:
@@ -76,6 +78,6 @@ Type `sudo make install` for system-wide installation. Run `sudo make install-li
macOS
=====
[.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) is required to compile OpenRA.
[.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0) or [Mono](https://www.mono-project.com/download/stable/#download-mac) (version 6.12 or later) is required to compile OpenRA. We recommend using .NET 6 unless you are running a very old version of macOS (10.9 through 10.14).
To compile OpenRA, run `make` from the command line. Run with `./launch-game.sh`.
To compile OpenRA, run `make` from the command line (or `make RUNTIME=mono` if using Mono). Run with `./launch-game.sh`.

View File

@@ -3,17 +3,20 @@
# to compile, run:
# make
#
# to compile using Mono (version 6.12 or greater) instead of .NET 6, run:
# make RUNTIME=mono
#
# to compile using system libraries for native dependencies, run:
# make TARGETPLATFORM=unix-generic
# make [RUNTIME=net6] TARGETPLATFORM=unix-generic
#
# to check the official mods for erroneous yaml files, run:
# make test
# make [RUNTIME=net6] test
#
# to check the engine and official mod dlls for code style violations, run:
# make check
# make [RUNTIME=net6] check
#
# to compile and install Red Alert, Tiberian Dawn, and Dune 2000, run:
# make [prefix=/foo] [bindir=/bar/bin] install
# make [RUNTIME=net6] [prefix=/foo] [bindir=/bar/bin] install
#
# to compile and install Red Alert, Tiberian Dawn, and Dune 2000
# using system libraries for native dependencies, run:
@@ -45,12 +48,15 @@ gameinstalldir ?= $(libdir)/openra
# Toolchain
CWD = $(shell pwd)
MSBUILD = msbuild -verbosity:m -nologo
DOTNET = dotnet
MONO = mono
RM = rm
RM_R = $(RM) -r
RM_F = $(RM) -f
RM_RF = $(RM) -rf
RUNTIME ?= net6
CONFIGURATION ?= Release
DOTNET_RID = $(shell ${DOTNET} --info | grep RID: | cut -w -f3)
ARCH_X64 = $(shell echo ${DOTNET_RID} | grep x64)
@@ -85,12 +91,18 @@ endif
#
all:
@echo "Compiling in ${CONFIGURATION} mode..."
ifeq ($(RUNTIME), mono)
@command -v $(firstword $(MSBUILD)) >/dev/null || (echo "OpenRA requires the '$(MSBUILD)' tool provided by Mono >= 6.12."; exit 1)
@$(MSBUILD) -t:Build -restore -p:Configuration=${CONFIGURATION} -p:TargetPlatform=$(TARGETPLATFORM)
else
@$(DOTNET) build -c ${CONFIGURATION} -nologo -p:TargetPlatform=$(TARGETPLATFORM)
endif
ifeq ($(TARGETPLATFORM), unix-generic)
@./configure-system-libraries.sh
endif
@./fetch-geoip.sh
# dotnet clean and msbuild -t:Clean leave files that cause problems when switching between mono/dotnet
# Deleting the intermediate / output directories ensures the build directory is actually clean
clean:
@-$(RM_RF) ./bin ./*/obj
@@ -99,8 +111,12 @@ clean:
check:
@echo
@echo "Compiling in Debug mode..."
ifeq ($(RUNTIME), mono)
@$(MSBUILD) -t:clean\;build -restore -p:Configuration=Debug -warnaserror -p:TargetPlatform=$(TARGETPLATFORM)
else
@$(DOTNET) clean -c Debug --nologo --verbosity minimal
@$(DOTNET) build -c Debug -nologo -warnaserror -p:TargetPlatform=$(TARGETPLATFORM)
endif
ifeq ($(TARGETPLATFORM), unix-generic)
@./configure-system-libraries.sh
endif
@@ -119,19 +135,15 @@ check-scripts:
test: all
@echo
@echo "Testing Tiberian Sun mod MiniYAML..."
@./utility.sh ts-content --check-yaml
@./utility.sh ts --check-yaml
@echo
@echo "Testing Dune 2000 mod MiniYAML..."
@./utility.sh d2k-content --check-yaml
@./utility.sh d2k --check-yaml
@echo
@echo "Testing Tiberian Dawn mod MiniYAML..."
@./utility.sh cnc-content --check-yaml
@./utility.sh cnc --check-yaml
@echo
@echo "Testing Red Alert mod MiniYAML..."
@./utility.sh ra-content --check-yaml
@./utility.sh ra --check-yaml
tests:
@@ -141,15 +153,15 @@ 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) True True True'
@sh -c '. ./packaging/functions.sh; install_assemblies $(CWD) $(DESTDIR)$(gameinstalldir) $(TARGETPLATFORM) $(RUNTIME) True True True'
@sh -c '. ./packaging/functions.sh; install_data $(CWD) $(DESTDIR)$(gameinstalldir) cnc d2k ra'
install-linux-shortcuts:
@@ -166,21 +178,24 @@ help:
@echo 'to compile, run:'
@echo ' make'
@echo
@echo 'to compile using Mono (version 6.12 or greater) instead of .NET 6, run:'
@echo ' make RUNTIME=mono'
@echo
@echo 'to compile using system libraries for native dependencies, run:'
@echo ' make TARGETPLATFORM=unix-generic'
@echo ' make [RUNTIME=net6] TARGETPLATFORM=unix-generic'
@echo
@echo 'to check the official mods for erroneous yaml files, run:'
@echo ' make [TREAT_WARNINGS_AS_ERRORS=false] test'
@echo ' make [RUNTIME=net6] [TREAT_WARNINGS_AS_ERRORS=false] test'
@echo
@echo 'to check the engine and official mod dlls for code style violations, run:'
@echo ' make check'
@echo ' make [RUNTIME=net6] check'
@echo
@echo 'to compile and install Red Alert, Tiberian Dawn, and Dune 2000 run:'
@echo ' make [prefix=/foo] [TARGETPLATFORM=unix-generic] install'
@echo ' make [RUNTIME=net6] [prefix=/foo] [TARGETPLATFORM=unix-generic] install'
@echo
@echo 'to compile and install Red Alert, Tiberian Dawn, and Dune 2000'
@echo 'using system libraries for native dependencies, run:'
@echo ' make [prefix=/foo] [bindir=/bar/bin] TARGETPLATFORM=unix-generic install'
@echo ' make [RUNTIME=net6] [prefix=/foo] [bindir=/bar/bin] TARGETPLATFORM=unix-generic install'
@echo
@echo 'to install FreeDesktop startup scripts, desktop files, icons, and MIME metadata'
@echo ' make install-linux-shortcuts'

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);
@@ -89,21 +84,21 @@ namespace OpenRA
sealed class ConditionState
{
/// <summary>Delegates that have registered to be notified when this condition changes.</summary>
public readonly List<VariableObserverNotifier> Notifiers = [];
public readonly List<VariableObserverNotifier> Notifiers = new();
/// <summary>Unique integers identifying granted instances of the condition.</summary>
public readonly HashSet<int> Tokens = [];
public readonly HashSet<int> Tokens = new();
}
readonly Dictionary<string, ConditionState> conditionStates = [];
readonly Dictionary<string, ConditionState> conditionStates = new();
/// <summary>Each granted condition receives a unique token that is used when revoking.</summary>
readonly Dictionary<int, string> conditionTokens = [];
readonly Dictionary<int, string> conditionTokens = new();
int nextConditionToken = 1;
/// <summary>Cache of condition -> enabled state for quick evaluation of token counter conditions.</summary>
readonly Dictionary<string, int> conditionCache = [];
readonly Dictionary<string, int> conditionCache = new();
/// <summary>Read-only version of conditionCache that is passed to IConditionConsumers.</summary>
readonly IReadOnlyDictionary<string, int> readOnlyConditionCache;
@@ -123,9 +118,6 @@ namespace OpenRA
readonly IEnumerable<WPos> enabledTargetableWorldPositions;
bool created;
IEnumerable<IRenderable> renderables;
WorldRenderer lastWorldRenderer;
internal Actor(World world, string name, TypeDictionary initDict)
{
var duplicateInit = initDict.WithInterface<ISingleInstanceInit>().GroupBy(i => i.GetType())
@@ -163,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())
{
@@ -190,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();
@@ -205,7 +195,6 @@ namespace OpenRA
EnabledTargetablePositions = targetablePositions.Where(Exts.IsTraitEnabled);
enabledTargetableWorldPositions = EnabledTargetablePositions.SelectMany(tp => tp.TargetablePositions(this));
SyncHashes = syncHashesList.ToArray();
crushables = crushablesList.ToArray();
}
}
@@ -290,18 +279,11 @@ namespace OpenRA
public IEnumerable<IRenderable> Render(WorldRenderer wr)
{
if (lastWorldRenderer != wr)
{
// PERF: Cache the enumerable to reduce allocations.
lastWorldRenderer = wr;
renderables = Renderables(wr);
}
// PERF: Avoid LINQ.
var modifiedRenderables = renderables;
var renderables = Renderables(wr);
foreach (var modifier in renderModifiers)
modifiedRenderables = modifier.ModifyRender(this, wr, modifiedRenderables);
return modifiedRenderables;
renderables = modifier.ModifyRender(this, wr, renderables);
return renderables;
}
IEnumerable<IRenderable> Renderables(WorldRenderer wr)
@@ -514,7 +496,7 @@ namespace OpenRA
if (!visibilityModifier.IsVisible(this, player))
return false;
return defaultVisibility?.IsVisible(this, player) ?? true;
return defaultVisibility.IsVisible(this, player);
}
public BitSet<TargetableType> GetAllTargetTypes()
@@ -551,7 +533,7 @@ namespace OpenRA
if (EnabledTargetablePositions.Any())
return enabledTargetableWorldPositions;
return [CenterPosition];
return new[] { CenterPosition };
}
#region Conditions

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
@@ -78,9 +77,17 @@ namespace OpenRA
return new MPos(X, Y);
// Convert from RectangularIsometric cell (x, y) position to rectangular map position (u, v)
// The staggered rows make this fiddly.
// - The staggered rows make this fiddly (hint: draw a diagram!)
// (a) Consider the relationships:
// - +1x (even -> odd) adds (0, 1) to (u, v)
// - +1x (odd -> even) adds (1, 1) to (u, v)
// - +1y (even -> odd) adds (-1, 1) to (u, v)
// - +1y (odd -> even) adds (0, 1) to (u, v)
// (b) Therefore:
// - ax + by adds (a - b)/2 to u (only even increments count)
// - ax + by adds a + b to v
var u = (X - Y) / 2;
var v = X + Y;
var u = (v - (v & 1)) / 2 - Y;
return new MPos(u, v);
}
@@ -89,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);
}
@@ -99,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))
{
@@ -113,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)
@@ -141,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;
@@ -62,24 +60,23 @@ namespace OpenRA
public override string ToString() { return X + "," + Y; }
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
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

@@ -20,10 +20,7 @@ namespace OpenRA
public static class CryptoUtil
{
// 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();
static readonly byte[] OIDHeader = { 0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0 };
public static string PublicKeyFingerprint(RSAParameters parameters)
{
@@ -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)
@@ -210,7 +207,8 @@ namespace OpenRA
using (var rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(parameters);
return Convert.ToBase64String(rsa.SignHash(SHA1.HashData(data), CryptoConfig.MapNameToOID("SHA1")));
using (var csp = SHA1.Create())
return Convert.ToBase64String(rsa.SignHash(csp.ComputeHash(data), CryptoConfig.MapNameToOID("SHA1")));
}
}
catch (Exception e)
@@ -235,7 +233,8 @@ namespace OpenRA
using (var rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(parameters);
return rsa.VerifyHash(SHA1.HashData(data), CryptoConfig.MapNameToOID("SHA1"), Convert.FromBase64String(signature));
using (var csp = SHA1.Create())
return rsa.VerifyHash(csp.ComputeHash(data), CryptoConfig.MapNameToOID("SHA1"), Convert.FromBase64String(signature));
}
}
catch (Exception e)
@@ -250,42 +249,19 @@ namespace OpenRA
public static string SHA1Hash(Stream data)
{
return ToHex(SHA1.HashData(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)
{
return ToHex(SHA1.HashData(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

@@ -1,22 +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;
namespace OpenRA
{
// Mirrors DescriptionAttribute from System.ComponentModel but we don't want to have to use that everywhere.
[AttributeUsage(AttributeTargets.All)]
public sealed class DescAttribute(params string[] lines) : Attribute
{
public readonly string[] Lines = lines;
}
}

View File

@@ -12,7 +12,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using OpenRA.FileFormats;
@@ -28,8 +27,9 @@ namespace OpenRA
{
public readonly string Id;
public readonly string Version;
public readonly string Title;
public readonly string LaunchPath;
public readonly ImmutableArray<string> LaunchArgs;
public readonly string[] LaunchArgs;
public Sprite Icon { get; internal set; }
public Sprite Icon2x { get; internal set; }
public Sprite Icon3x { get; internal set; }
@@ -41,7 +41,7 @@ namespace OpenRA
public class ExternalMods : IReadOnlyDictionary<string, ExternalMod>
{
readonly Dictionary<string, ExternalMod> mods = [];
readonly Dictionary<string, ExternalMod> mods = new();
readonly SheetBuilder sheetBuilder;
Sheet CreateSheet()
@@ -66,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");
@@ -77,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)
@@ -95,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);
@@ -123,29 +122,26 @@ namespace OpenRA
return;
var key = ExternalMod.MakeKey(mod);
var yaml = new MiniYamlNode("Registration", new MiniYaml("",
[
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))
@@ -205,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

@@ -11,12 +11,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using OpenRA.Primitives;
using OpenRA.Support;
using OpenRA.Traits;
@@ -25,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); }
@@ -79,7 +67,7 @@ namespace OpenRA
return Math.Sign((v1.X - v0.X) * (p.Y - v0.Y) - (p.X - v0.X) * (v1.Y - v0.Y));
}
public static bool PolygonContains(this ImmutableArray<int2> polygon, int2 p)
public static bool PolygonContains(this int2[] polygon, int2 p)
{
var windingNumber = 0;
@@ -121,11 +109,17 @@ namespace OpenRA
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, V v)
{
#if NET5_0_OR_GREATER
// SAFETY: Dictionary cannot be modified whilst the ref is alive.
ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(d, k, out var exists);
ref var value = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(d, k, out var exists);
if (!exists)
value = v;
return value;
#else
if (!d.TryGetValue(k, out var ret))
d.Add(k, ret = v);
return ret;
#endif
}
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, Func<K, V> createFn)
@@ -137,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);
@@ -178,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)
{
@@ -333,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;
}
@@ -374,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;
}
@@ -386,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);
@@ -414,6 +379,11 @@ namespace OpenRA
return ts.Except(exclusions);
}
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source)
{
return new HashSet<T>(source);
}
public static Dictionary<TKey, TSource> ToDictionaryWithConflictLog<TSource, TKey>(
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
string debugName, Func<TKey, string> logKey, Func<TSource, string> logValue)
@@ -424,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);
@@ -454,16 +413,15 @@ namespace OpenRA
continue;
// Check for a key conflict:
if (!output.TryAdd(key, element))
if (!d.TryAdd(key, element))
{
dupKeys ??= [];
if (!dupKeys.TryGetValue(key, out var dupKeyMessages))
{
// Log the initial conflicting value already inserted:
dupKeyMessages =
[
logValue(output[key])
];
dupKeyMessages = new List<string>
{
logValue(d[key])
};
dupKeys.Add(key, dupKeyMessages);
}
@@ -473,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(CultureInfo.InvariantCulture, $"{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)
@@ -501,43 +460,50 @@ namespace OpenRA
return result;
}
public static byte ParseByteInvariant(string s)
public static T[,] ResizeArray<T>(T[,] ts, T t, int width, int height)
{
return byte.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
var result = new T[width, height];
for (var i = 0; i < width; i++)
{
for (var j = 0; j < height; j++)
{
// Workaround for broken ternary operators in certain versions of mono
// (3.10 and certain versions of the 3.8 series): https://bugzilla.xamarin.com/show_bug.cgi?id=23319
if (i <= ts.GetUpperBound(0) && j <= ts.GetUpperBound(1))
result[i, j] = ts[i, j];
else
result[i, j] = t;
}
}
return result;
}
public static ushort ParseUshortInvariant(string s)
public static int ToBits(this IEnumerable<bool> bits)
{
return ushort.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
var i = 0;
var result = 0;
foreach (var b in bits)
if (b)
result |= 1 << i++;
else
i++;
if (i > 33)
throw new InvalidOperationException("ToBits only accepts up to 32 values.");
return result;
}
public static 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 float ParseFloatOrPercentInvariant(string s)
public static byte ParseByte(string s)
{
var f = float.Parse(s.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo);
return f * (s.Contains('%') ? 0.01f : 1f);
return byte.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static bool TryParseByteInvariant(string s, out byte i)
{
return byte.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
public static bool TryParseUshortInvariant(string s, out ushort i)
{
return ushort.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
public static bool TryParseInt32Invariant(string s, out int i)
public static bool TryParseIntegerInvariant(string s, out int i)
{
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
@@ -547,47 +513,6 @@ namespace OpenRA
return long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
public static bool TryParseFloatOrPercentInvariant(string s, out float f)
{
if (float.TryParse(s?.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out f))
{
f *= s.Contains('%') ? 0.01f : 1f;
return true;
}
return false;
}
public static string ToStringInvariant(this byte i)
{
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 uint i)
{
return i.ToString(NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this float f)
{
return f.ToString(NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this int i, string format)
{
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()
{
@@ -665,7 +595,7 @@ namespace OpenRA
if (index == -1)
{
// The remaining string is an empty string
str = [];
str = ReadOnlySpan<char>.Empty;
Current = span;
return true;
}
@@ -677,4 +607,27 @@ namespace OpenRA
public ReadOnlySpan<char> Current { get; private set; }
}
public static class Enum<T>
{
public static T Parse(string s) { return (T)Enum.Parse(typeof(T), s); }
public static T[] GetValues() { return (T[])Enum.GetValues(typeof(T)); }
public static bool TryParse(string s, bool ignoreCase, out T value)
{
// The string may be a comma delimited list of values
var names = ignoreCase ? Enum.GetNames(typeof(T)).Select(x => x.ToLowerInvariant()) : Enum.GetNames(typeof(T));
var values = ignoreCase ? s.Split(',').Select(x => x.Trim().ToLowerInvariant()) : s.Split(',').Select(x => x.Trim());
if (values.Any(x => !names.Contains(x)))
{
value = default;
return false;
}
value = (T)Enum.Parse(typeof(T), s, ignoreCase);
return true;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,57 +10,55 @@
#endregion
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using OpenRA.Primitives;
namespace OpenRA
{
public static class FieldSaver
{
public static MiniYaml Save(object o)
public static MiniYaml Save(object o, bool includePrivateByDefault = false)
{
var nodes = new List<MiniYamlNode>();
string root = null;
foreach (var fieldInfo in FieldLoader.GetTypeLoadInfo(o.GetType()))
foreach (var info in FieldLoader.GetTypeLoadInfo(o.GetType(), includePrivateByDefault))
{
if (fieldInfo.Field.FieldType.IsGenericType && fieldInfo.Field.FieldType.IsAssignableTo(typeof(System.Collections.IDictionary)))
if (info.Attribute.DictionaryFromYamlKey)
{
var dict = (System.Collections.IDictionary)fieldInfo.Field.GetValue(o);
var dictNodes = new List<MiniYamlNode>();
var dict = (System.Collections.IDictionary)info.Field.GetValue(o);
foreach (var kvp in dict)
{
var key = ((System.Collections.DictionaryEntry)kvp).Key;
var value = ((System.Collections.DictionaryEntry)kvp).Value;
dictNodes.Add(new MiniYamlNode(FormatValue(key), FormatValue(value)));
}
nodes.Add(new MiniYamlNode(fieldInfo.YamlName, "", dictNodes));
nodes.Add(new MiniYamlNode(FormatValue(key), FormatValue(value)));
}
}
else if (info.Attribute.FromYamlKey)
root = FormatValue(o, info.Field);
else
nodes.Add(new MiniYamlNode(fieldInfo.YamlName, FormatValue(o, fieldInfo.Field)));
nodes.Add(new MiniYamlNode(info.YamlName, FormatValue(o, info.Field)));
}
return new MiniYaml(null, nodes);
return new MiniYaml(root, nodes);
}
public static MiniYaml SaveDifferences(object o, object from)
public static MiniYaml SaveDifferences(object o, object from, bool includePrivateByDefault = false)
{
if (o.GetType() != from.GetType())
throw new InvalidOperationException("FieldSaver: can't diff objects of different types");
throw new InvalidOperationException("FieldLoader: can't diff objects of different types");
var fields = FieldLoader.GetTypeLoadInfo(o.GetType())
var fields = FieldLoader.GetTypeLoadInfo(o.GetType(), includePrivateByDefault)
.Where(info => FormatValue(o, info.Field) != FormatValue(from, info.Field));
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)
@@ -80,37 +78,13 @@ namespace OpenRA
if (t.IsArray && t.GetArrayRank() == 1)
return ((Array)v).Cast<object>().Select(FormatValue).JoinWith(", ");
if (t.IsGenericType &&
t.GetGenericTypeDefinition() == typeof(ImmutableArray<>))
{
try
{
return ((System.Collections.IEnumerable)v).Cast<object>().Select(FormatValue).JoinWith(", ");
}
catch (InvalidOperationException)
{
return "";
}
}
if (t.IsGenericType &&
(t.GetGenericTypeDefinition() == typeof(List<>) ||
t.GetGenericTypeDefinition() == typeof(HashSet<>) ||
t.GetGenericTypeDefinition()
.BaseTypes()
.Select(bt => bt.IsGenericType ? bt.GetGenericTypeDefinition() : null)
.Any(bt => bt == typeof(FrozenSet<>))))
if (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(HashSet<>) || t.GetGenericTypeDefinition() == typeof(List<>)))
return ((System.Collections.IEnumerable)v).Cast<object>().Select(FormatValue).JoinWith(", ");
// This is only for documentation generation
if (t.IsGenericType &&
(t.GetGenericTypeDefinition() == typeof(Dictionary<,>) ||
t.GetGenericTypeDefinition()
.BaseTypes()
.Select(bt => bt.IsGenericType ? bt.GetGenericTypeDefinition() : null)
.Any(bt => bt == typeof(FrozenDictionary<,>))))
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
var result = new StringBuilder();
var result = "";
var dict = (System.Collections.IDictionary)v;
foreach (var kvp in dict)
{
@@ -120,10 +94,10 @@ namespace OpenRA
var formattedKey = FormatValue(key);
var formattedValue = FormatValue(value);
result.Append(CultureInfo.InvariantCulture, $"{formattedKey}: {formattedValue}{Environment.NewLine}");
result += $"{formattedKey}: {formattedValue}{Environment.NewLine}";
}
return result.ToString();
return result;
}
if (v is DateTime d)

View File

@@ -10,105 +10,28 @@
#endregion
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;
using ICSharpCode.SharpZipLib.Checksum;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using OpenRA.Graphics;
using OpenRA.Primitives;
namespace OpenRA.FileFormats
{
/// <summary>
/// Used to connect several IDAT chunks into a continuous stream.
/// </summary>
sealed class PngIdatStream : Stream
{
readonly Stream baseStream;
int remainingInChunk;
bool eof;
public PngIdatStream(Stream baseStream, int initialLen)
{
this.baseStream = baseStream;
remainingInChunk = initialLen;
}
public override int Read(byte[] buffer, int offset, int count) => Read(buffer.AsSpan(offset, count));
public override int Read(Span<byte> buffer)
{
if (eof || buffer.Length == 0)
return 0;
var totalRead = 0;
Span<byte> header = stackalloc byte[8];
while (buffer.Length > 0)
{
if (remainingInChunk == 0)
{
// Skip CRC and read next chunk header.
var r = baseStream.Seek(4, SeekOrigin.Current) + baseStream.Read(header);
if (r < 12)
throw new EndOfStreamException("Invalid PNG file - no end chunk found.");
// The PNG spec states that IDAT chunks must be chained together.
if (BinaryPrimitives.ReadUInt32BigEndian(header[4..]) == Png.ChunkIDAT)
remainingInChunk = BinaryPrimitives.ReadInt32BigEndian(header[..4]);
else
{
// This is not an IDAT chunk. Discontinue reading.
baseStream.Seek(-8, SeekOrigin.Current);
eof = true;
return totalRead;
}
}
var toRead = Math.Min(buffer.Length, remainingInChunk);
var read = baseStream.Read(buffer[..toRead]);
if (read == 0)
throw new EndOfStreamException("Unexpected end of stream in IDAT chunk.");
remainingInChunk -= read;
totalRead += read;
buffer = buffer[read..];
}
return totalRead;
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}
public class Png
{
public const uint ChunkIHDR = 0x49484452;
public const uint ChunkPLTE = 0x504C5445;
public const uint ChunkIDAT = 0x49444154;
public const uint ChunkIEND = 0x49454E44;
public const uint ChunkTRNS = 0x74524E53;
public const uint ChunkTEXT = 0x74455874;
static readonly byte[] Signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
static readonly byte[] Signature = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
public int Width { get; }
public int Height { get; }
public Color[] Palette { get; }
public byte[] Data { get; }
public SpriteFrameType Type { get; }
public Dictionary<string, string> EmbeddedData = [];
public Dictionary<string, string> EmbeddedData = new();
public int PixelStride => Type == SpriteFrameType.Indexed8 ? 1 : Type == SpriteFrameType.Rgb24 ? 3 : 4;
@@ -119,273 +42,164 @@ namespace OpenRA.FileFormats
s.Position += 8;
var headerParsed = false;
var dataParsed = false;
byte bitDepth = 8;
var data = new List<byte>();
Type = SpriteFrameType.Rgba32;
// Use a reusable 8-byte buffer for Length + Type.
Span<byte> chunkHeader = stackalloc byte[8];
while (true)
{
if (s.Read(chunkHeader) < 8)
throw new EndOfStreamException("Invalid PNG file - no end chunk found.");
var length = IPAddress.NetworkToHostOrder(s.ReadInt32());
var type = Encoding.UTF8.GetString(s.ReadBytes(4));
var content = s.ReadBytes(length);
/*var crc = */s.ReadInt32();
var length = BinaryPrimitives.ReadInt32BigEndian(chunkHeader[..4]);
var type = BinaryPrimitives.ReadUInt32BigEndian(chunkHeader[4..]);
if (!headerParsed && type != ChunkIHDR)
if (!headerParsed && type != "IHDR")
throw new InvalidDataException("Invalid PNG file - header does not appear first.");
switch (type)
using (var ms = new MemoryStream(content))
{
case ChunkIHDR:
switch (type)
{
if (headerParsed)
throw new InvalidDataException("Invalid PNG file - duplicate header.");
#pragma warning disable CA2014 // Do not use stackalloc in loops
Span<byte> buffer = stackalloc byte[13];
#pragma warning restore CA2014 // Do not use stackalloc in loops
s.ReadBytes(buffer);
Width = BinaryPrimitives.ReadInt32BigEndian(buffer[..4]);
Height = BinaryPrimitives.ReadInt32BigEndian(buffer[4..8]);
bitDepth = buffer[8];
var colorType = (PngColorType)buffer[9];
if (IsPaletted(bitDepth, colorType))
Type = SpriteFrameType.Indexed8;
else if (colorType == PngColorType.Color)
Type = SpriteFrameType.Rgb24;
var compression = buffer[10];
// filter = buffer[11]
var interlace = buffer[12];
if (compression != 0)
throw new InvalidDataException("Compression method not supported");
if (interlace != 0)
throw new InvalidDataException("Interlacing not supported");
Data = new byte[Width * Height * PixelStride];
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
headerParsed = true;
break;
}
case ChunkPLTE:
{
if (length % 3 != 0)
throw new InvalidDataException("Invalid PLTE chunk length.");
var count = length / 3;
Palette = new Color[count];
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
case "IHDR":
{
s.ReadBytes(buffer.AsSpan(0, length));
for (var i = 0; i < count; i++)
if (headerParsed)
throw new InvalidDataException("Invalid PNG file - duplicate header.");
Width = IPAddress.NetworkToHostOrder(ms.ReadInt32());
Height = IPAddress.NetworkToHostOrder(ms.ReadInt32());
var bitDepth = ms.ReadUInt8();
var colorType = (PngColorType)ms.ReadUInt8();
if (IsPaletted(bitDepth, colorType))
Type = SpriteFrameType.Indexed8;
else if (colorType == PngColorType.Color)
Type = SpriteFrameType.Rgb24;
Data = new byte[Width * Height * PixelStride];
var compression = ms.ReadUInt8();
/*var filter = */ms.ReadUInt8();
var interlace = ms.ReadUInt8();
if (compression != 0)
throw new InvalidDataException("Compression method not supported");
if (interlace != 0)
throw new InvalidDataException("Interlacing not supported");
headerParsed = true;
break;
}
case "PLTE":
{
Palette = new Color[256];
for (var i = 0; i < length / 3; i++)
{
var offset = i * 3;
Palette[i] = Color.FromArgb(buffer[offset], buffer[offset + 1], buffer[offset + 2]);
var r = ms.ReadUInt8(); var g = ms.ReadUInt8(); var b = ms.ReadUInt8();
Palette[i] = Color.FromArgb(r, g, b);
}
break;
}
finally
case "tRNS":
{
ArrayPool<byte>.Shared.Return(buffer);
if (Palette == null)
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
for (var i = 0; i < length; i++)
Palette[i] = Color.FromArgb(ms.ReadUInt8(), Palette[i]);
break;
}
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
break;
}
case ChunkTRNS:
{
if (Palette == null)
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
case "IDAT":
{
s.ReadBytes(buffer.AsSpan(0, length));
for (var i = 0; i < length && i < Palette.Length; i++)
Palette[i] = Color.FromArgb(buffer[i], Palette[i]);
data.AddRange(content);
break;
}
finally
case "tEXt":
{
ArrayPool<byte>.Shared.Return(buffer);
var key = ms.ReadASCIIZ();
EmbeddedData.Add(key, ms.ReadASCII(length - key.Length - 1));
break;
}
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
break;
}
case ChunkIDAT:
if (dataParsed)
throw new InvalidDataException("Invalid PNG file - discontinuous IDAT chunks.");
dataParsed = true;
ProcessIDAT(s, length, bitDepth);
break;
case ChunkTEXT:
{
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
case "IEND":
{
var span = buffer.AsSpan(0, length);
s.ReadBytes(span);
using (var ns = new MemoryStream(data.ToArray()))
{
using (var ds = new InflaterInputStream(ns))
{
var pxStride = PixelStride;
var rowStride = Width * pxStride;
// Find Null Terminator for ASCIIZ (Keyword).
var nullIndex = span.IndexOf((byte)0);
if (nullIndex == -1)
return;
Span<byte> prevLine = new byte[rowStride];
for (var y = 0; y < Height; y++)
{
var filter = (PngFilter)ds.ReadUInt8();
ds.ReadBytes(Data, y * rowStride, rowStride);
var line = Data.AsSpan(y * rowStride, rowStride);
var key = Encoding.ASCII.GetString(span[..nullIndex]);
var value = Encoding.ASCII.GetString(span[(nullIndex + 1)..]);
switch (filter)
{
case PngFilter.None:
break;
case PngFilter.Sub:
for (var i = pxStride; i < rowStride; i++)
line[i] += line[i - pxStride];
break;
case PngFilter.Up:
for (var i = 0; i < rowStride; i++)
line[i] += prevLine[i];
break;
case PngFilter.Average:
for (var i = 0; i < pxStride; i++)
line[i] += Average(0, prevLine[i]);
for (var i = pxStride; i < rowStride; i++)
line[i] += Average(line[i - pxStride], prevLine[i]);
break;
case PngFilter.Paeth:
for (var i = 0; i < pxStride; i++)
line[i] += Paeth(0, prevLine[i], 0);
for (var i = pxStride; i < rowStride; i++)
line[i] += Paeth(line[i - pxStride], prevLine[i], prevLine[i - pxStride]);
break;
default:
throw new InvalidOperationException("Unsupported Filter");
}
EmbeddedData[key] = value;
prevLine = line;
}
static byte Average(byte a, byte b) => (byte)((a + b) / 2);
static byte Paeth(byte a, byte b, byte c)
{
var p = a + b - c;
var pa = Math.Abs(p - a);
var pb = Math.Abs(p - b);
var pc = Math.Abs(p - c);
return (pa <= pb && pa <= pc) ? a :
(pb <= pc) ? b : c;
}
}
}
if (Type == SpriteFrameType.Indexed8 && Palette == null)
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
return;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
break;
}
case ChunkIEND:
{
if (Type == SpriteFrameType.Indexed8 && Palette == null)
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
return;
}
default:
{
// Skip unknown chunk + CRC.
s.Seek(length + 4, SeekOrigin.Current);
break;
}
}
}
}
void ProcessIDAT(Stream s, int firstChunkLength, byte bitDepth)
{
var pxStride = PixelStride;
var rowStride = Width * pxStride;
var pixelsPerByte = 8 / bitDepth;
var sourceRowStride = Exts.IntegerDivisionRoundingAwayFromZero(rowStride, pixelsPerByte);
// Custom stream that stitches IDAT chunks together without copying.
using var idatStream = new PngIdatStream(s, firstChunkLength);
using var ds = new ZLibStream(idatStream, CompressionMode.Decompress);
// Rent buffer from pool to avoid allocation churn.
var prevLineBuffer = ArrayPool<byte>.Shared.Rent(rowStride);
var prevLine = prevLineBuffer.AsSpan(0, rowStride);
prevLine.Clear();
try
{
for (var y = 0; y < Height; y++)
{
var filterByte = ds.ReadByte();
if (filterByte == -1)
break;
var filter = (PngFilter)filterByte;
var line = Data.AsSpan(y * rowStride, rowStride);
ds.ReadBytes(line[..sourceRowStride]);
// If the source has a bit depth of 1, 2 or 4 it packs multiple pixels per byte.
// 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 = (1 << bitDepth) - 1;
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:
break;
case PngFilter.Sub:
for (var i = pxStride; i < line.Length; i++)
line[i] += line[i - pxStride];
break;
case PngFilter.Up:
for (var i = 0; i < line.Length; i++)
line[i] += prevLine[i];
break;
case PngFilter.Average:
for (var i = 0; i < pxStride; i++)
line[i] += Average(0, prevLine[i]);
for (var i = pxStride; i < line.Length; i++)
line[i] += Average(line[i - pxStride], prevLine[i]);
break;
case PngFilter.Paeth:
for (var i = 0; i < pxStride; i++)
line[i] += Paeth(0, prevLine[i], 0);
for (var i = pxStride; i < line.Length; i++)
line[i] += Paeth(line[i - pxStride], prevLine[i], prevLine[i - pxStride]);
break;
default:
throw new InvalidOperationException("Unsupported Filter");
}
prevLine = line;
}
// Drain remaining Zlib footer bytes if necessary.
Span<byte> drain = stackalloc byte[16];
while (ds.Read(drain) > 0) { }
}
finally
{
ArrayPool<byte>.Shared.Return(prevLineBuffer);
}
static byte Average(byte a, byte b) => (byte)((a + b) / 2);
static byte Paeth(byte a, byte b, byte c)
{
var p = a + b - c;
var pa = Math.Abs(p - a);
var pb = Math.Abs(p - b);
var pc = Math.Abs(p - c);
return (pa <= pb && pa <= pc) ? a :
(pb <= pc) ? b : c;
}
}
public Png(byte[] data, SpriteFrameType type, int width, int height, Color[] palette = null,
Dictionary<string, string> embeddedData = null)
{
@@ -420,27 +234,14 @@ namespace OpenRA.FileFormats
// Convert to big endian
Data = new byte[data.Length];
var stride = PixelStride;
var elements = width * height * stride;
var src = data.AsSpan();
var dst = Data.AsSpan();
if (type == SpriteFrameType.Bgra32)
for (var i = 0; i < width * height; i++)
{
for (var i = 0; i < elements; i += 4)
{
dst[i + 0] = src[i + 2];
dst[i + 1] = src[i + 1];
dst[i + 2] = src[i + 0];
dst[i + 3] = src[i + 3];
}
}
else
{
for (var i = 0; i < elements; i += 3)
{
dst[i + 0] = src[i + 2];
dst[i + 1] = src[i + 1];
dst[i + 2] = src[i + 0];
}
Data[stride * i] = data[stride * i + 2];
Data[stride * i + 1] = data[stride * i + 1];
Data[stride * i + 2] = data[stride * i + 0];
if (type == SpriteFrameType.Bgra32)
Data[stride * i + 3] = data[stride * i + 3];
}
break;
@@ -457,8 +258,7 @@ namespace OpenRA.FileFormats
public static bool Verify(Stream s)
{
var pos = s.Position;
Span<byte> sigBuffer = stackalloc byte[8];
var isPng = s.Read(sigBuffer) == 8 && sigBuffer.SequenceEqual(Signature);
var isPng = Signature.Aggregate(true, (current, t) => current && s.ReadUInt8() == t);
s.Position = pos;
return isPng;
}
@@ -469,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))
@@ -481,33 +281,28 @@ namespace OpenRA.FileFormats
throw new InvalidDataException("Unknown pixel format");
}
static void WritePngChunk(MemoryStream output, uint type, MemoryStream input)
static void WritePngChunk(Stream output, string type, Stream input)
{
Span<byte> header = stackalloc byte[8];
BinaryPrimitives.WriteInt32BigEndian(header[..4], (int)input.Length);
BinaryPrimitives.WriteUInt32BigEndian(header[4..], type);
input.Position = 0;
if (!input.TryGetBuffer(out var dataSegment))
dataSegment = new ArraySegment<byte>(input.ToArray());
var typeBytes = Encoding.ASCII.GetBytes(type);
output.Write(IPAddress.HostToNetworkOrder((int)input.Length));
output.WriteArray(typeBytes);
ReadOnlySpan<byte> data = dataSegment.AsSpan(0, (int)input.Length);
var data = input.ReadAllBytes();
output.WriteArray(data);
output.Write(header);
output.Write(data);
var crc = 0xFFFFFFFF;
crc = CRC32.Update(crc, header[4..]);
crc = CRC32.Update(crc, data);
var finalCrc = CRC32.Finish(crc);
output.Write(IPAddress.NetworkToHostOrder((int)finalCrc));
var crc32 = new Crc32();
crc32.Update(typeBytes);
crc32.Update(data);
output.Write(IPAddress.NetworkToHostOrder((int)crc32.Value));
}
public byte[] Save(CompressionLevel compression = CompressionLevel.SmallestSize)
public byte[] Save()
{
using (var output = new MemoryStream())
{
output.Write(Signature);
output.WriteArray(Signature);
using (var header = new MemoryStream())
{
header.Write(IPAddress.HostToNetworkOrder(Width));
@@ -522,7 +317,7 @@ namespace OpenRA.FileFormats
header.WriteByte(0); // Filter
header.WriteByte(0); // Interlacing
WritePngChunk(output, ChunkIHDR, header);
WritePngChunk(output, "IHDR", header);
}
var alphaPalette = false;
@@ -538,7 +333,7 @@ namespace OpenRA.FileFormats
alphaPalette |= c.A > 0;
}
WritePngChunk(output, ChunkPLTE, palette);
WritePngChunk(output, "PLTE", palette);
}
}
@@ -549,44 +344,46 @@ namespace OpenRA.FileFormats
foreach (var c in Palette)
alpha.WriteByte(c.A);
WritePngChunk(output, ChunkTRNS, alpha);
WritePngChunk(output, "tRNS", alpha);
}
}
using (var data = new MemoryStream())
{
using (var compressed = new ZLibStream(data, compression, true))
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);
}
}
WritePngChunk(output, ChunkIDAT, data);
compressed.Flush();
compressed.Finish();
WritePngChunk(output, "IDAT", data);
}
}
foreach (var kv in EmbeddedData)
{
using (var text = new MemoryStream())
{
text.Write(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
WritePngChunk(output, ChunkTEXT, text);
text.WriteArray(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
WritePngChunk(output, "tEXt", text);
}
}
WritePngChunk(output, ChunkIEND, new MemoryStream());
WritePngChunk(output, "IEND", new MemoryStream());
return output.ToArray();
}
}
public void Save(string path, CompressionLevel compression = CompressionLevel.SmallestSize)
public void Save(string path)
{
File.WriteAllBytes(path, Save(compression));
File.WriteAllBytes(path, Save());
}
}
}

View File

@@ -27,7 +27,8 @@ namespace OpenRA.FileFormats
public ReplayMetadata(GameInformation info)
{
ArgumentNullException.ThrowIfNull(info);
if (info == null)
throw new ArgumentNullException(nameof(info));
GameInfo = info;
}
@@ -46,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)
@@ -61,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,22 +23,22 @@ 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
{
public IEnumerable<IReadOnlyPackage> MountedPackages => mountedPackages.Keys;
readonly Dictionary<IReadOnlyPackage, int> mountedPackages = [];
readonly Dictionary<string, IReadOnlyPackage> explicitMounts = [];
readonly Dictionary<IReadOnlyPackage, int> mountedPackages = new();
readonly Dictionary<string, IReadOnlyPackage> explicitMounts = new();
readonly string modID;
// Mod packages that should not be disposed
readonly List<IReadOnlyPackage> modPackages = [];
readonly List<IReadOnlyPackage> modPackages = new();
readonly IReadOnlyDictionary<string, Manifest> installedMods;
readonly IPackageLoader[] packageLoaders;
Cache<string, List<IReadOnlyPackage>> fileIndex = new(_ => []);
Cache<string, List<IReadOnlyPackage>> fileIndex = new(_ => new List<IReadOnlyPackage>());
public FileSystem(string modID, IReadOnlyDictionary<string, Manifest> installedMods, IPackageLoader[] packageLoaders)
{
@@ -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
@@ -178,16 +182,14 @@ namespace OpenRA.FileSystem
explicitMounts.Clear();
modPackages.Clear();
fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => []);
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

@@ -35,7 +35,7 @@ namespace OpenRA.FileSystem
return Directory.GetFiles(Name, "*", SearchOption.TopDirectoryOnly)
.Concat(Directory.GetDirectories(Name))
.Select(Path.GetFileName)
.Order();
.OrderBy(f => f);
}
}
@@ -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,15 +93,15 @@ namespace OpenRA.FileSystem
}
}
public sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage
sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage
{
readonly MemoryStream pkgStream = new();
public ReadWriteZipFile(string filename = null, bool create = false)
public ReadWriteZipFile(string filename, bool create = false)
{
// SharpZipLib breaks when asked to update archives loaded from outside streams or files
// We can work around this by creating a clean in-memory-only file, cutting all outside references
if (!string.IsNullOrEmpty(filename) && !create)
if (!create)
{
using (var copy = new MemoryStream(File.ReadAllBytes(filename)))
{
@@ -114,33 +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;
}
public ReadWriteZipFile(byte[] data)
{
using (var copy = new MemoryStream(data))
{
pkgStream.Capacity = (int)copy.Length;
copy.CopyTo(pkgStream);
}
pkgStream.Position = 0;
pkg = new ZipFile(pkgStream);
Name = null;
// Remove subfields that can break ZIP updating.
foreach (ZipEntry entry in pkg)
entry.ExtraData = null;
}
void Commit()
{
if (!string.IsNullOrEmpty(Name))
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)
@@ -167,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,202 +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.Collections.Immutable;
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;
}
}
[AttributeUsage(AttributeTargets.Class)]
public sealed class IncludeStaticFluentReferencesAttribute : Attribute
{
public readonly Type[] Types;
public IncludeStaticFluentReferencesAttribute(params Type[] types)
{
Types = types;
}
}
[AttributeUsage(AttributeTargets.Class)]
public sealed class IncludeChromeLogicArgsFluentReferencesAttribute : Attribute
{
public readonly string[] MethodNames;
public IncludeChromeLogicArgsFluentReferencesAttribute(params string[] methodNames)
{
MethodNames = methodNames;
}
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class IncludeFluentReferencesAttribute : Attribute
{
public readonly LintDictionaryReference DictionaryReference;
public IncludeFluentReferencesAttribute() { }
public IncludeFluentReferencesAttribute(LintDictionaryReference dictionaryReference = LintDictionaryReference.None)
{
DictionaryReference = dictionaryReference;
}
}
public class FluentBundle
{
readonly Linguini.Bundle.FluentBundle bundle;
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem)
: this(culture, paths, fileSystem, error => Log.Write("debug", error.Message)) { }
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem, string text)
: this(culture, paths, fileSystem, text, error => Log.Write("debug", error.Message)) { }
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem, Action<ParseError> onError)
: this(culture, paths, fileSystem, null, onError) { }
public FluentBundle(string culture, string text, Action<ParseError> onError)
: this(culture, default, null, text, onError) { }
public FluentBundle(string culture, ImmutableArray<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)
{
ArgumentNullException.ThrowIfNull(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 = [];
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,94 +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.Immutable;
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(Manifest manifest, IReadOnlyFileSystem fileSystem)
{
lock (SyncObject)
{
modFluentBundle = new FluentBundle(manifest.FluentCulture, manifest.FluentMessages, fileSystem);
if (fileSystem is Map map && map.FluentMessageDefinitions != null)
{
var files = ImmutableArray<string>.Empty;
if (map.FluentMessageDefinitions.Value != null)
files = FieldLoader.GetValue<ImmutableArray<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(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

@@ -9,7 +9,6 @@
*/
#endregion
using System.Collections.Frozen;
using System.Collections.Generic;
namespace OpenRA
@@ -24,15 +23,15 @@ namespace OpenRA
public class Fonts : IGlobalModData
{
[FieldLoader.LoadUsing(nameof(LoadFonts))]
public readonly FrozenDictionary<string, FontData> FontList;
public readonly Dictionary<string, FontData> FontList;
static object LoadFonts(MiniYaml y)
{
var ret = new Dictionary<string, FontData>(y.Nodes.Length);
var ret = new Dictionary<string, FontData>();
foreach (var node in y.Nodes)
ret.Add(node.Key, FieldLoader.Load<FontData>(node.Value));
return ret.ToFrozenDictionary();
return ret;
}
}
}

View File

@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@@ -28,10 +27,9 @@ using OpenRA.Widgets;
namespace OpenRA
{
[IncludeStaticFluentReferences(typeof(Server.Server), typeof(Player), typeof(UnitOrders), typeof(OrderManager))]
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
@@ -168,7 +166,6 @@ namespace OpenRA
{
return ModData.WidgetLoader.LoadWidget(new WidgetArgs(args)
{
{ "modData", ModData },
{ "world", world },
{ "orderManager", OrderManager },
{ "worldRenderer", worldRenderer },
@@ -183,17 +180,7 @@ namespace OpenRA
}
public static event Action BeforeGameStart = () => { };
public static event Action AfterGameStart = () => { };
internal static void StartGame(string uid, WorldType type)
{
var preview = ModData.MapCache[uid];
if (preview.Status != MapStatus.Available)
throw new InvalidDataException($"Invalid map uid: {uid}");
StartGame(preview.ToMap(), type);
}
internal static void StartGame(Map map, WorldType type)
internal static void StartGame(string mapUID, WorldType type)
{
// Dispose of the old world before creating a new one.
worldRenderer?.Dispose();
@@ -202,23 +189,7 @@ namespace OpenRA
BeforeGameStart();
using (new PerfTimer("NewWorld"))
{
ModData.PrepareMap(map);
// The depth buffer needs to be initialized with enough range to cover:
// - the height of the screen
// - the z-offset of tiles from MaxTerrainHeight below the bottom of the screen (pushed into view)
// - additional z-offset from actors on top of MaxTerrainHeight terrain
// - a small margin so that tiles rendered partially above the top edge of the screen aren't pushed behind the clip plane
// We need an offset of mapGrid.MaximumTerrainHeight * mapGrid.TileSize.Height / 2 to cover the terrain height
// and choose to use mapGrid.MaximumTerrainHeight * mapGrid.TileSize.Height / 4 for each of the actor and top-edge cases
var margin = 0;
if (map.Grid.EnableDepthBuffer)
margin = map.Rules.TerrainInfo.TileSize.Height * map.Grid.MaximumTerrainHeight;
Renderer.SetDepthMargin(margin);
OrderManager.World = new World(map, ModData, OrderManager, type);
}
OrderManager.World = new World(mapUID, ModData, OrderManager, type);
OrderManager.World.GameOver += FinishBenchmark;
@@ -252,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()
@@ -372,7 +337,7 @@ namespace OpenRA
var explicitModPaths = Array.Empty<string>();
if (modID != null && (File.Exists(modID) || Directory.Exists(modID)))
{
explicitModPaths = [modID];
explicitModPaths = new[] { modID };
modID = Path.GetFileNameWithoutExtension(modID);
}
@@ -387,48 +352,6 @@ namespace OpenRA
Log.AddChannel("nat", "nat.log");
Log.AddChannel("client", "client.log");
Nat.Initialize();
var modSearchArg = args.GetValue("Engine.ModSearchPaths", null);
var modSearchPaths = modSearchArg != null ?
FieldLoader.GetValue<ImmutableArray<string>>("Engine.ModsPath", modSearchArg) :
[Path.Combine(Platform.EngineDir, "mods")];
Mods = new InstalledMods(modSearchPaths, explicitModPaths);
Console.WriteLine("Internal mods:");
foreach (var mod in Mods)
Console.WriteLine($"\t{mod.Key} ({mod.Value.Metadata.Version})");
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
ExternalMods = new ExternalMods();
if (modID == null)
throw new InvalidOperationException("Game.Mod argument missing.");
if (Mods.TryGetValue(modID, out var manifest))
{
var launchPath = args.GetValue("Engine.LaunchPath", null);
var launchArgs = new List<string>();
// Sanitize input from platform-specific launchers
// Process.Start requires paths to not be quoted, even if they contain spaces
if (launchPath != null && launchPath[0] == '"' && launchPath[^1] == '"')
launchPath = launchPath[1..^1];
// Metadata registration requires an explicit launch path
if (launchPath != null)
ExternalMods.Register(Mods[modID], launchPath, launchArgs, ModRegistration.User);
ExternalMods.ClearInvalidRegistrations(ModRegistration.User);
}
else
throw new InvalidOperationException($"Unknown or invalid mod '{modID}'.");
Console.WriteLine("External mods:");
foreach (var mod in ExternalMods)
Console.WriteLine($"\t{mod.Key} ({mod.Value.Version})");
var platforms = new[] { Settings.Game.Platform, "Default", null };
foreach (var p in platforms)
{
@@ -438,8 +361,23 @@ namespace OpenRA
Settings.Game.Platform = p;
try
{
var platform = CreatePlatform(p);
Renderer = new Renderer(platform, Settings.Graphics, manifest.RendererConstants.VertexBatchSize);
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);
break;
@@ -455,23 +393,47 @@ namespace OpenRA
}
}
InitializeMod(manifest, args);
Nat.Initialize();
var modSearchArg = args.GetValue("Engine.ModSearchPaths", null);
var modSearchPaths = modSearchArg != null ?
FieldLoader.GetValue<string[]>("Engine.ModsPath", modSearchArg) :
new[] { Path.Combine(Platform.EngineDir, "mods") };
Mods = new InstalledMods(modSearchPaths, explicitModPaths);
Console.WriteLine("Internal mods:");
foreach (var mod in Mods)
Console.WriteLine($"\t{mod.Key}: {mod.Value.Metadata.Title} ({mod.Value.Metadata.Version})");
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
ExternalMods = new ExternalMods();
if (modID != null && Mods.TryGetValue(modID, out _))
{
var launchPath = args.GetValue("Engine.LaunchPath", null);
var launchArgs = new List<string>();
// Sanitize input from platform-specific launchers
// Process.Start requires paths to not be quoted, even if they contain spaces
if (launchPath != null && launchPath.First() == '"' && launchPath.Last() == '"')
launchPath = launchPath[1..^1];
// Metadata registration requires an explicit launch path
if (launchPath != null)
ExternalMods.Register(Mods[modID], launchPath, launchArgs, ModRegistration.User);
ExternalMods.ClearInvalidRegistrations(ModRegistration.User);
}
Console.WriteLine("External mods:");
foreach (var mod in ExternalMods)
Console.WriteLine($"\t{mod.Key}: {mod.Value.Title} ({mod.Value.Version})");
InitializeMod(modID, args);
}
public static IPlatform CreatePlatform(string platformName)
{
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + platformName + ".dll");
var loader = new AssemblyLoader(rendererPath);
var platformType = loader.LoadDefaultAssembly().GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
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(Manifest manifest, Arguments args)
public static void InitializeMod(string mod, Arguments args)
{
// Clear static state if we have switched mods
LobbyInfoChanged = () => { };
@@ -495,29 +457,38 @@ namespace OpenRA
ModData = null;
Console.WriteLine($"Loading mod: {manifest.Id}");
if (mod == null)
throw new InvalidOperationException("Game.Mod argument missing.");
if (!Mods.ContainsKey(mod))
throw new InvalidOperationException($"Unknown or invalid mod '{mod}'.");
Console.WriteLine($"Loading mod: {mod}");
Sound.StopVideo();
ModData = new ModData(manifest, Mods, true);
ModData = new ModData(Mods[mod], Mods, true);
LocalPlayerProfile = new LocalPlayerProfile(Path.Combine(Platform.SupportDir, Settings.Game.AuthProfile), ModData.GetOrCreate<PlayerDatabase>());
LocalPlayerProfile = new LocalPlayerProfile(Path.Combine(Platform.SupportDir, Settings.Game.AuthProfile), ModData.Manifest.Get<PlayerDatabase>());
if (!ModData.LoadScreen.BeforeLoad(ModData))
if (!ModData.LoadScreen.BeforeLoad())
return;
ModData.InitializeLoaders(ModData.DefaultFileSystem);
Renderer.InitializeFonts(ModData);
using (new PerfTimer("LoadMaps"))
ModData.MapCache.LoadMaps(ModData);
ModData.MapCache.LoadMaps();
var grid = ModData.Manifest.Contains<MapGrid>() ? ModData.Manifest.Get<MapGrid>() : null;
Renderer.InitializeDepthBuffer(grid);
Cursor?.Dispose();
Cursor = new CursorManager(ModData);
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;
@@ -531,16 +502,10 @@ namespace OpenRA
ModData.LoadScreen.StartGame(args);
}
public static void LoadEditor(string uid)
public static void LoadEditor(string mapUid)
{
JoinLocal();
StartGame(uid, WorldType.Editor);
}
public static void LoadEditor(Map map)
{
JoinLocal();
StartGame(map, WorldType.Editor);
StartGame(mapUid, WorldType.Editor);
}
public static void LoadShellMap()
@@ -559,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)
@@ -614,11 +578,11 @@ namespace OpenRA
Directory.CreateDirectory(directory);
var filename = TimestampedFilename(true);
var path = Path.Combine(directory, $"{filename}.png");
var path = Path.Combine(directory, string.Concat(filename, ".png"));
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)));
}
}
@@ -637,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);
@@ -649,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())
{
@@ -661,7 +618,7 @@ namespace OpenRA
world.Tick();
PerfHistory.Tick(!world.Paused);
PerfHistory.Tick();
}
// Wait until we have done our first world Tick before TickRendering
@@ -707,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)
@@ -717,7 +674,7 @@ namespace OpenRA
}
Ui.PrepareRenderables();
worldRenderer?.EndFrame();
Renderer.WorldModelRenderer.EndFrame();
}
// worldRenderer is null during the initial install/download screen
@@ -725,7 +682,7 @@ namespace OpenRA
// Use worldRenderer.World instead of OrderManager.World to avoid a rendering mismatch while processing orders
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
{
Renderer.BeginWorld(worldRenderer.Viewport.CenterLocation, worldRenderer.Viewport.ViewportSize);
Renderer.BeginWorld(worldRenderer.Viewport.Rectangle);
Sound.SetListenerPosition(worldRenderer.Viewport.CenterPosition);
using (new PerfSample("render_world"))
worldRenderer.Draw();
@@ -740,12 +697,15 @@ namespace OpenRA
Ui.Draw();
if (HideCursor)
Cursor?.SetCursor(null);
else
if (ModData != null && ModData.CursorProvider != null)
{
Cursor?.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default");
Cursor?.Render(Renderer);
if (HideCursor)
Cursor.SetCursor(null);
else
{
Cursor.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default");
Cursor.Render(Renderer);
}
}
}
@@ -759,13 +719,12 @@ namespace OpenRA
}
}
var isActive = !(worldRenderer?.World.Paused ?? true);
PerfHistory.Items["render"].Tick(isActive);
PerfHistory.Items["batches"].Tick(isActive);
PerfHistory.Items["render_world"].Tick(isActive);
PerfHistory.Items["render_widgets"].Tick(isActive);
PerfHistory.Items["render_flip"].Tick(isActive);
PerfHistory.Items["terrain_lighting"].Tick(isActive);
PerfHistory.Items["render"].Tick();
PerfHistory.Items["batches"].Tick();
PerfHistory.Items["render_world"].Tick();
PerfHistory.Items["render_widgets"].Tick();
PerfHistory.Items["render_flip"].Tick();
PerfHistory.Items["terrain_lighting"].Tick();
}
static void Loop()
@@ -819,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
@@ -862,45 +821,33 @@ namespace OpenRA
var haveSomeTimeUntilNextLogic = now < nextLogic;
var isTimeToRender = now >= nextRender;
if (!Renderer.WindowIsSuspended)
if (!Renderer.WindowIsSuspended && ((isTimeToRender && haveSomeTimeUntilNextLogic) || forceRender))
{
if (isTimeToRender || forceRender)
{
if (haveSomeTimeUntilNextLogic || forceRender)
RenderTick();
nextRender = now + renderInterval;
nextRender = now + renderInterval;
// Pick the minimum allowed FPS (the lower between 'minReplayFPS'
// and the user's max frame rate) and convert it to maximum time
// allowed between screen updates.
// We do this before rendering to include the time rendering takes
// in this interval.
var maxRenderInterval = Math.Max(1000 / MinReplayFps, renderInterval);
forcedNextRender = now + maxRenderInterval;
// Pick the minimum allowed FPS (the lower between 'minReplayFPS'
// and the user's max frame rate) and convert it to maximum time
// allowed between screen updates.
// We do this before rendering to include the time rendering takes
// in this interval.
var maxRenderInterval = Math.Max(1000 / MinReplayFps, renderInterval);
forcedNextRender = now + maxRenderInterval;
renderBeforeNextTick = false;
}
RenderTick();
renderBeforeNextTick = false;
}
else
// Simulate a render tick if it was time to render but we skip actually rendering
if (Renderer.WindowIsSuspended && isTimeToRender)
{
// Simulate a render tick if it was time to render but we skip actually rendering
if (isTimeToRender || forceRender)
{
// Make sure that nextUpdate is set to a proper minimum interval
nextRender = now + renderInterval;
// Make sure that nextUpdate is set to a proper minimum interval
nextRender = now + renderInterval;
// Still process SDL events to allow a restore to come through
Renderer.Window.PumpInput(new NullInputHandler());
// Still process SDL events to allow a restore to come through
Renderer.Window.PumpInput(new NullInputHandler());
// Ensure that we still logic tick despite not rendering
renderBeforeNextTick = false;
}
else
{
// Avoid busy wait.
Thread.Sleep((int)(nextRender - now));
}
// Ensure that we still logic tick despite not rendering
renderBeforeNextTick = false;
}
}
else
@@ -966,22 +913,21 @@ 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()
{
Name = "Skirmish Game",
Map = map,
AdvertiseOnline = false,
AdvertiseOnLocalNetwork = !isSkirmish
AdvertiseOnline = false
};
// Always connect to local games using the same loopback connection
@@ -989,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();
}
@@ -1019,7 +965,7 @@ namespace OpenRA
Order.Command($"state {Session.ClientState.Ready}")
};
var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.Path) == 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

@@ -10,7 +10,6 @@
#endregion
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Network;
@@ -20,9 +19,6 @@ namespace OpenRA
{
public class GameInformation
{
[FluentReference("name", "number")]
const string EnumeratedBotName = "enumerated-bot-name";
public string Mod;
public string Version;
@@ -40,41 +36,26 @@ namespace OpenRA
public TimeSpan Duration => EndTimeUtc > StartTimeUtc ? EndTimeUtc - StartTimeUtc : TimeSpan.Zero;
public IList<Player> Players { get; }
public FrozenSet<int> DisabledSpawnPoints = FrozenSet<int>.Empty;
public HashSet<int> DisabledSpawnPoints = new();
public MapPreview MapPreview => Game.ModData.MapCache[MapUid];
public IEnumerable<Player> HumanPlayers { get { return Players.Where(p => p.IsHuman); } }
public bool IsSinglePlayer => HumanPlayers.Count() == 1;
[FieldLoader.Ignore]
public MapGenerationArgs MapGenerationArgs;
readonly Dictionary<OpenRA.Player, Player> playersByRuntime;
public GameInformation()
{
Players = [];
playersByRuntime = [];
Players = new List<Player>();
playersByRuntime = new Dictionary<OpenRA.Player, Player>();
}
public MapPreview GetMapPreview(ModData modData)
{
var preview = modData.MapCache[MapUid];
if (preview.Status != MapStatus.Available && MapGenerationArgs != null)
{
preview.UpdateFromGenerationArgs(MapGenerationArgs);
preview.Generate();
}
return preview;
}
public static GameInformation Deserialize(string data, string path)
public static GameInformation Deserialize(string data)
{
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,10 +69,6 @@ namespace OpenRA
case "Player":
info.Players.Add(FieldLoader.Load<Player>(node.Value));
break;
case "MapGenerationArgs":
info.MapGenerationArgs = FieldLoader.Load<MapGenerationArgs>(node.Value);
break;
}
}
@@ -108,24 +85,23 @@ 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++)
nodes.Add(new MiniYamlNode($"Player@{i}", FieldSaver.Save(Players[i])));
if (MapGenerationArgs != null)
nodes.Add(new MiniYamlNode("MapGenerationArgs", new MiniYaml("", MapGenerationArgs.Serialize())));
return nodes.WriteToString();
}
/// <summary>Adds the player information at start-up.</summary>
public void AddPlayer(OpenRA.Player runtimePlayer, Session lobbyInfo)
{
ArgumentNullException.ThrowIfNull(runtimePlayer);
if (runtimePlayer == null)
throw new ArgumentNullException(nameof(runtimePlayer));
ArgumentNullException.ThrowIfNull(lobbyInfo);
if (lobbyInfo == null)
throw new ArgumentNullException(nameof(lobbyInfo));
// We don't care about spectators and map players
if (runtimePlayer.NonCombatant || !runtimePlayer.Playable)
@@ -142,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,
@@ -168,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
@@ -191,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>
@@ -32,8 +32,8 @@ namespace OpenRA
/// You can remove inherited traits by adding a - in front of them as in -TraitName: to inherit everything, but this trait.
/// </summary>
public readonly string Name;
readonly TypeDictionary traits = [];
TraitInfo[] constructOrderCache = null;
readonly TypeDictionary traits = new();
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

@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using OpenRA.FileSystem;
@@ -125,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));
@@ -183,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));
@@ -227,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)
{
@@ -261,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<ImmutableArray<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

@@ -10,56 +10,55 @@
#endregion
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace OpenRA.GameRules
{
public class SoundInfo
{
public readonly FrozenDictionary<string, ImmutableArray<string>> Variants = FrozenDictionary<string, ImmutableArray<string>>.Empty;
public readonly FrozenDictionary<string, ImmutableArray<string>> Prefixes = FrozenDictionary<string, ImmutableArray<string>>.Empty;
public readonly FrozenDictionary<string, ImmutableArray<string>> Voices = FrozenDictionary<string, ImmutableArray<string>>.Empty;
public readonly FrozenDictionary<string, ImmutableArray<string>> Notifications = FrozenDictionary<string, ImmutableArray<string>>.Empty;
public readonly Dictionary<string, string[]> Variants = new();
public readonly Dictionary<string, string[]> Prefixes = new();
public readonly Dictionary<string, string[]> Voices = new();
public readonly Dictionary<string, string[]> Notifications = new();
public readonly string DefaultVariant = ".aud";
public readonly string DefaultPrefix = "";
public readonly FrozenSet<string> DisableVariants = FrozenSet<string>.Empty;
public readonly FrozenSet<string> DisablePrefixes = FrozenSet<string>.Empty;
public readonly HashSet<string> DisableVariants = new();
public readonly HashSet<string> DisablePrefixes = new();
public readonly Lazy<FrozenDictionary<string, SoundPool>> VoicePools;
public readonly Lazy<FrozenDictionary<string, SoundPool>> NotificationsPools;
public readonly Lazy<Dictionary<string, SoundPool>> VoicePools;
public readonly Lazy<Dictionary<string, SoundPool>> NotificationsPools;
public SoundInfo(MiniYaml y)
{
FieldLoader.Load(this, y);
VoicePools = Exts.Lazy(() => Voices.ToFrozenDictionary(a => a.Key, a => new SoundPool(1f, SoundPool.DefaultInterruptType, a.Value)));
VoicePools = Exts.Lazy(() => Voices.ToDictionary(a => a.Key, a => new SoundPool(1f, SoundPool.DefaultInterruptType, a.Value)));
NotificationsPools = Exts.Lazy(() => ParseSoundPool(y, "Notifications"));
}
static FrozenDictionary<string, SoundPool> ParseSoundPool(MiniYaml y, string key)
static Dictionary<string, SoundPool> ParseSoundPool(MiniYaml y, string key)
{
var classifiction = y.NodeWithKey(key);
var ret = new Dictionary<string, SoundPool>(classifiction.Value.Nodes.Length);
var ret = new Dictionary<string, SoundPool>();
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);
var names = FieldLoader.GetValue<ImmutableArray<string>>(t.Key, t.Value.Value);
var names = FieldLoader.GetValue<string[]>(t.Key, t.Value.Value);
var sp = new SoundPool(volumeModifier, interruptType, names);
ret.Add(t.Key, sp);
}
return ret.ToFrozenDictionary();
return ret;
}
}
@@ -69,10 +68,10 @@ namespace OpenRA.GameRules
public const InterruptType DefaultInterruptType = InterruptType.DoNotPlay;
public readonly float VolumeModifier;
public readonly InterruptType Type;
readonly ImmutableArray<string> clips;
readonly List<string> liveclips = [];
readonly string[] clips;
readonly List<string> liveclips = new();
public SoundPool(float volumeModifier, InterruptType interruptType, ImmutableArray<string> clips)
public SoundPool(float volumeModifier, InterruptType interruptType, params string[] clips)
{
VolumeModifier = volumeModifier;
Type = interruptType;

View File

@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.Effects;
using OpenRA.Primitives;
@@ -37,7 +36,7 @@ namespace OpenRA.GameRules
public class WarheadArgs
{
public WeaponInfo Weapon;
public int[] DamageModifiers = [];
public int[] DamageModifiers = Array.Empty<int>();
public WPos? Source;
public WRot ImpactOrientation;
public WPos ImpactPosition;
@@ -83,13 +82,13 @@ namespace OpenRA.GameRules
public readonly WVec FollowingBurstTargetOffset = WVec.Zero;
[Desc("The sound played each time the weapon is fired.")]
public readonly ImmutableArray<string> Report = default;
public readonly string[] Report = null;
[Desc("Sound played only on first burst in a salvo.")]
public readonly ImmutableArray<string> StartBurstReport = default;
public readonly string[] StartBurstReport = null;
[Desc("The sound played when the weapon is reloaded.")]
public readonly ImmutableArray<string> AfterFireSound = default;
public readonly string[] AfterFireSound = null;
[Desc("Delay in ticks to play reloading sound.")]
public readonly int AfterFireSoundDelay = 0;
@@ -117,7 +116,7 @@ namespace OpenRA.GameRules
[Desc("Delay in ticks between firing shots from the same ammo magazine. If one entry, it will be used for all bursts.",
"If multiple entries, their number needs to match Burst - 1.")]
public readonly ImmutableArray<int> BurstDelays = [5];
public readonly int[] BurstDelays = { 5 };
[Desc("The minimum range the weapon can fire.")]
public readonly WDist MinRange = WDist.Zero;
@@ -129,7 +128,7 @@ namespace OpenRA.GameRules
public readonly IProjectileInfo Projectile;
[FieldLoader.LoadUsing(nameof(LoadWarheads))]
public readonly ImmutableArray<IWarhead> Warheads = [];
public readonly List<IWarhead> Warheads = new();
/// <summary>
/// This constructor is used solely for documentation generation.
@@ -140,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([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");
@@ -161,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)
@@ -171,7 +169,7 @@ namespace OpenRA.GameRules
retList.Add(ret);
}
return retList.ToImmutableArray();
return retList;
}
public bool IsValidTarget(BitSet<TargetableType> targetTypes)

View File

@@ -10,13 +10,13 @@
#endregion
using System.Collections.Generic;
using OpenRA.Traits;
using System.Linq;
namespace OpenRA
{
public class GameSpeed
{
[FluentReference]
[TranslationReference]
[FieldLoader.Require]
public readonly string Name;
@@ -32,14 +32,13 @@ namespace OpenRA
[FieldLoader.Require]
public readonly string DefaultSpeed;
[IncludeFluentReferences(LintDictionaryReference.Values)]
[FieldLoader.LoadUsing(nameof(LoadSpeeds))]
public readonly Dictionary<string, GameSpeed> Speeds;
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

@@ -10,7 +10,6 @@
#endregion
using System;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.Primitives;
using OpenRA.Support;
@@ -23,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;
@@ -45,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;
@@ -61,23 +58,18 @@ namespace OpenRA.Graphics
var tintModifiers = CurrentSequence.IgnoreWorldTint ? TintModifiers.IgnoreWorldTint : TintModifiers.None;
var alpha = CurrentSequence.GetAlpha(CurrentFrame);
var (image, rotation) = CurrentSequence.GetSpriteWithRotation(CurrentFrame, facingFunc());
var imageRenderable = new SpriteRenderable(
image, pos, offset, CurrentSequence.ZOffset + zOffset, palette,
CurrentSequence.Scale, alpha, float3.Ones, tintModifiers, IsDecoration, rotation);
var imageRenderable = new SpriteRenderable(image, pos, offset, CurrentSequence.ZOffset + zOffset, palette, CurrentSequence.Scale, alpha, float3.Ones, tintModifiers, IsDecoration,
rotation);
var shadow = CurrentSequence.GetShadow(CurrentFrame, facingFunc());
if (shadow != null)
{
var 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 [shadowRenderable, imageRenderable];
return new IRenderable[] { shadowRenderable, imageRenderable };
}
return [imageRenderable];
return new IRenderable[] { imageRenderable };
}
public IRenderable[] RenderUI(WorldRenderer wr, int2 pos, in WVec offset, int zOffset, PaletteReference palette, float scale = 1f, float rotation = 0f)
@@ -93,10 +85,10 @@ namespace OpenRA.Graphics
{
var shadowPos = pos - new int2((int)(scale * shadow.Size.X / 2), (int)(scale * shadow.Size.Y / 2));
var shadowRenderable = new UISpriteRenderable(shadow, WPos.Zero + offset, shadowPos, CurrentSequence.ShadowZOffset + zOffset, palette, scale, 1f, rotation);
return [shadowRenderable, imageRenderable];
return new IRenderable[] { shadowRenderable, imageRenderable };
}
return [imageRenderable];
return new IRenderable[] { imageRenderable };
}
public Rectangle ScreenBounds(WorldRenderer wr, WPos pos, in WVec offset)
@@ -251,9 +243,9 @@ namespace OpenRA.Graphics
return sequences.GetSequence(Name, sequenceName);
}
public string GetRandomExistingSequence(ImmutableArray<string> sequences, MersenneTwister random)
public string GetRandomExistingSequence(string[] sequences, MersenneTwister random)
{
return sequences.Where(HasSequence).RandomOrDefault(random);
return sequences.Where(s => HasSequence(s)).RandomOrDefault(random);
}
}
}

View File

@@ -10,9 +10,7 @@
#endregion
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Primitives;
@@ -49,9 +47,9 @@ namespace OpenRA.Graphics
public readonly string Image2x = null;
public readonly string Image3x = null;
public readonly ImmutableArray<int> PanelRegion = default;
public readonly int[] PanelRegion = null;
public readonly PanelSides PanelSides = PanelSides.All;
public readonly FrozenDictionary<string, Rectangle> Regions = FrozenDictionary<string, Rectangle>.Empty;
public readonly Dictionary<string, Rectangle> Regions = new();
}
public static IReadOnlyDictionary<string, Collection> Collections => collections;
@@ -73,18 +71,17 @@ namespace OpenRA.Graphics
dpiScale = Game.Renderer.WindowScale;
fileSystem = modData.DefaultFileSystem;
collections = [];
cachedSheets = [];
cachedSprites = [];
cachedPanelSprites = [];
cachedCollectionSheets = [];
collections = new Dictionary<string, Collection>();
cachedSheets = new Dictionary<string, (Sheet, int)>();
cachedSprites = new Dictionary<string, Dictionary<string, Sprite>>();
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);
}
@@ -172,7 +169,7 @@ namespace OpenRA.Graphics
var sheetDensity = SheetForCollection(collection);
if (cachedCollection == null)
{
cachedCollection = [];
cachedCollection = new Dictionary<string, Sprite>();
cachedSprites.Add(collectionName, cachedCollection);
}
@@ -230,23 +227,19 @@ 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)
return [];
if (!collection.Regions.Any())
return Array.Empty<Sprite>();
// Support manual definitions for unusual dialog layouts
sprites =
[
sprites = new[]
{
TryGetImage(collectionName, "corner-tl"),
TryGetImage(collectionName, "border-t"),
TryGetImage(collectionName, "corner-tr"),
@@ -256,7 +249,7 @@ namespace OpenRA.Graphics
TryGetImage(collectionName, "corner-bl"),
TryGetImage(collectionName, "border-b"),
TryGetImage(collectionName, "corner-br")
];
};
}
cachedPanelSprites.Add(collectionName, sprites);

View File

@@ -11,13 +11,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Graphics
{
public sealed class CursorManager : IDisposable
public sealed class CursorManager
{
sealed class Cursor
{
@@ -30,8 +28,8 @@ namespace OpenRA.Graphics
public IHardwareCursor[] Cursors;
}
readonly Dictionary<string, Cursor> cursors = [];
public readonly SheetBuilder SheetBuilder;
readonly Dictionary<string, Cursor> cursors = new();
readonly SheetBuilder sheetBuilder;
readonly GraphicSettings graphicSettings;
Cursor cursor;
@@ -40,35 +38,16 @@ namespace OpenRA.Graphics
readonly bool hardwareCursorsDisabled = false;
bool hardwareCursorsDoubled = false;
public CursorManager(ModData modData)
public CursorManager(CursorProvider cursorProvider)
{
hardwareCursorsDisabled = Game.Settings.Graphics.DisableHardwareCursors;
graphicSettings = Game.Settings.Graphics;
hardwareCursorsDisabled = graphicSettings.DisableHardwareCursors;
SheetBuilder = new SheetBuilder(SheetType.BGRA, modData.Manifest.RendererConstants.CursorSheetSize);
// Overwrite previous definitions if there are duplicates
var pals = new Dictionary<string, IProvidesCursorPaletteInfo>();
foreach (var p in modData.DefaultRules.Actors[SystemActors.World].TraitInfos<IProvidesCursorPaletteInfo>())
if (p.Palette != null)
pals[p.Palette] = p;
var paletteCache = new Cache<string, ImmutablePalette>(p => pals[p].ReadPalette(modData.DefaultFileSystem));
var frameCache = new FrameCache(modData.DefaultFileSystem, modData.SpriteLoaders);
// Sort the cursors for better packing onto the sheet.
foreach (var kv in modData.Cursors)
sheetBuilder = new SheetBuilder(SheetType.BGRA);
foreach (var kv in cursorProvider.Cursors)
{
var cursorSprites = frameCache[kv.Value.Src];
var length = kv.Value.Length ?? cursorSprites.Length - kv.Value.Start;
if (kv.Value.Start > cursorSprites.Length)
throw new YamlException($"Cursor {kv.Value.Name}: {nameof(kv.Value.Start)} is greater than the length of the sprite sequence.");
if (kv.Value.Length > cursorSprites.Length)
throw new YamlException($"Cursor {kv.Value.Name}: {nameof(kv.Value.Length)} is greater than the length of the sprite sequence.");
var frames = cursorSprites.Skip(kv.Value.Start).Take(length).ToArray();
var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? paletteCache[kv.Value.Palette] : null;
var frames = kv.Value.Frames;
var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? cursorProvider.Palettes[kv.Value.Palette] : null;
var c = new Cursor
{
@@ -99,7 +78,7 @@ namespace OpenRA.Graphics
type = SpriteFrameType.Bgra32;
}
c.Sprites[c.Length++] = SheetBuilder.Add(data, type, f.Size, 0, hotspot);
c.Sprites[c.Length++] = sheetBuilder.Add(data, type, f.Size, 0, hotspot);
// Bounds relative to the hotspot
c.Bounds = Rectangle.Union(c.Bounds, new Rectangle(hotspot, f.Size));
@@ -111,12 +90,12 @@ namespace OpenRA.Graphics
cursors.Add(kv.Key, c);
}
// Allow the utility to create a cursor manager.
if (Game.Renderer != null)
{
CreateOrUpdateHardwareCursors();
Update();
}
CreateOrUpdateHardwareCursors();
foreach (var s in sheetBuilder.AllSheets)
s.ReleaseBuffer();
Update();
}
void CreateOrUpdateHardwareCursors()
@@ -149,8 +128,6 @@ namespace OpenRA.Graphics
}
}
SheetBuilder.Current?.ReleaseBuffer();
hardwareCursorsDoubled = graphicSettings.CursorDouble;
}
@@ -252,10 +229,6 @@ namespace OpenRA.Graphics
var width = frame.Size.Width;
var height = frame.Size.Height;
if (width == 0 || height == 0)
return [];
var data = new byte[4 * width * height];
unsafe
{
@@ -300,8 +273,11 @@ namespace OpenRA.Graphics
{
for (var i = 0; i < c.Cursors.Length; i++)
{
c.Cursors[i]?.Dispose();
c.Cursors[i] = null;
if (c.Cursors[i] != null)
{
c.Cursors[i].Dispose();
c.Cursors[i] = null;
}
}
}
}
@@ -311,7 +287,7 @@ namespace OpenRA.Graphics
ClearHardwareCursors();
cursors.Clear();
SheetBuilder.Dispose();
sheetBuilder.Dispose();
}
}
}

View File

@@ -0,0 +1,66 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Traits;
namespace OpenRA.Graphics
{
public sealed class CursorProvider
{
public readonly IReadOnlyDictionary<string, CursorSequence> Cursors;
public readonly IReadOnlyDictionary<string, ImmutablePalette> Palettes;
public CursorProvider(ModData modData)
{
var fileSystem = modData.DefaultFileSystem;
var sequenceYaml = MiniYaml.Merge(modData.Manifest.Cursors.Select(
s => MiniYaml.FromStream(fileSystem.Open(s), s)));
var nodesDict = new MiniYaml(null, sequenceYaml).ToDictionary();
// Overwrite previous definitions if there are duplicates
var pals = new Dictionary<string, IProvidesCursorPaletteInfo>();
foreach (var p in modData.DefaultRules.Actors[SystemActors.World].TraitInfos<IProvidesCursorPaletteInfo>())
if (p.Palette != null)
pals[p.Palette] = p;
Palettes = 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 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));
Cursors = cursors;
}
public bool HasCursorSequence(string cursor)
{
return Cursors.ContainsKey(cursor);
}
public CursorSequence GetCursorSequence(string cursor)
{
try { return Cursors[cursor]; }
catch (KeyNotFoundException)
{
throw new InvalidOperationException($"Cursor does not have a sequence `{cursor}`");
}
}
}
}

View File

@@ -9,44 +9,60 @@
*/
#endregion
using System.Linq;
namespace OpenRA.Graphics
{
public class CursorSequence
{
public readonly string Name;
public readonly string Src;
public readonly int Start;
public readonly int? Length;
public readonly int Length;
public readonly string Palette;
public readonly int2 Hotspot;
public CursorSequence(string name, string cursorSrc, string palette, MiniYaml info)
public readonly ISpriteFrame[] Frames;
public CursorSequence(FrameCache cache, string name, string cursorSrc, string palette, MiniYaml info)
{
var d = info.ToDictionary();
Start = Exts.ParseIntegerInvariant(d["Start"].Value);
Palette = palette;
Name = name;
Src = cursorSrc;
if (d.TryGetValue("X", out var yaml))
var cursorSprites = cache[cursorSrc];
Frames = cursorSprites.Skip(Start).ToArray();
if ((d.TryGetValue("Length", out var yaml) && yaml.Value == "*") ||
(d.TryGetValue("End", out yaml) && yaml.Value == "*"))
Length = Frames.Length;
else if (d.TryGetValue("Length", out yaml))
Length = Exts.ParseIntegerInvariant(yaml.Value);
else if (d.TryGetValue("End", out yaml))
Length = Exts.ParseIntegerInvariant(yaml.Value) - Start;
else
Length = 1;
Frames = Frames.Take(Length).ToArray();
if (Start > cursorSprites.Length)
throw new YamlException($"Cursor {name}: {nameof(Start)} is greater than the length of the sprite sequence.");
if (Length > cursorSprites.Length)
throw new YamlException($"Cursor {name}: {nameof(Length)} is greater than the length of the sprite sequence.");
if (d.TryGetValue("X", out yaml))
{
Exts.TryParseInt32Invariant(yaml.Value, out var x);
Exts.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);
}
Start = Exts.ParseInt32Invariant(d["Start"].Value);
if (d.TryGetValue("Length", out yaml))
Length = yaml.Value != "*" ? Exts.ParseInt32Invariant(yaml.Value) : null;
else if (d.TryGetValue("End", out yaml) && yaml.Value == "*")
Length = yaml.Value != "*" ? Exts.ParseInt32Invariant(yaml.Value) - Start : null;
else
Length = 1;
}
}
}

View File

@@ -21,11 +21,11 @@ namespace OpenRA.Graphics
public ITexture ColorShifts { get; }
public int Height { get; private set; }
readonly Dictionary<string, ImmutablePalette> palettes = [];
readonly Dictionary<string, MutablePalette> mutablePalettes = [];
readonly Dictionary<string, int> indices = [];
byte[] buffer = [];
float[] colorShiftBuffer = [];
readonly Dictionary<string, ImmutablePalette> palettes = new();
readonly Dictionary<string, MutablePalette> mutablePalettes = new();
readonly Dictionary<string, int> indices = new();
byte[] buffer = Array.Empty<byte>();
float[] colorShiftBuffer = Array.Empty<float>();
public HardwarePalette()
{
@@ -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,23 +30,65 @@ namespace OpenRA.Graphics
Rectangle AggregateBounds { get; }
}
public interface IModelWidget
public readonly struct ModelRenderData
{
string Palette { get; }
float Scale { get; }
void Setup(Func<bool> isVisible, Func<string> getPalette, Func<string> getPlayerPalette,
Func<float> getScale, Func<IModel> getVoxel, Func<WRot> getRotation);
public readonly int Start;
public readonly int Count;
public readonly Sheet Sheet;
public ModelRenderData(int start, int count, Sheet sheet)
{
Start = start;
Count = count;
Sheet = sheet;
}
}
public readonly record struct ModelRenderData(int Start, int Count, Sheet Sheet);
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

@@ -11,13 +11,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Cnc.Traits
namespace OpenRA.Graphics
{
public class ModelRenderProxy
{
@@ -35,54 +32,42 @@ namespace OpenRA.Mods.Cnc.Traits
}
}
[TraitLocation(SystemActors.World | SystemActors.EditorWorld)]
[Desc("Render voxels")]
public class ModelRendererInfo : TraitInfo, Requires<IModelCacheInfo>
{
public readonly int RenderBufferSize = 2048;
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 ImmutableArray<float> ShadowDiffuse = [0, 0, 0];
static readonly ImmutableArray<float> ShadowAmbient = [1, 1, 1];
static readonly float[] ShadowDiffuse = new float[] { 0, 0, 0 };
static readonly float[] ShadowAmbient = new float[] { 1, 1, 1 };
static readonly float2 SpritePadding = new(2, 2);
static readonly float[] ZeroVector = [0, 0, 0, 1];
static readonly float[] ZVector = [0, 0, 1, 1];
static readonly float[] ZeroVector = new float[] { 0, 0, 0, 1 };
static readonly float[] ZVector = new float[] { 0, 0, 1, 1 };
static readonly float[] FlipMtx = Util.ScaleMatrix(1, -1, 1);
static readonly float[] ShadowScaleFlipMtx = Util.ScaleMatrix(2, -2, 2);
static readonly float[] GroundNormal = [0, 0, 1, 1];
static readonly float[] GroundNormal = { 0, 0, 1, 1 };
readonly Renderer renderer;
readonly IShader shader;
public readonly IModelCache ModelCache;
readonly Dictionary<Sheet, IFrameBuffer> mappedBuffers = [];
readonly Stack<KeyValuePair<Sheet, IFrameBuffer>> unmappedBuffers = [];
readonly List<(Sheet Sheet, Action Func)> doRender = [];
readonly int sheetSize;
readonly Dictionary<Sheet, IFrameBuffer> mappedBuffers = new();
readonly Stack<KeyValuePair<Sheet, IFrameBuffer>> unmappedBuffers = new();
readonly List<(Sheet Sheet, Action Func)> doRender = new();
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 = info.RenderBufferSize;
var a = 2f / sheetSize;
public void SetViewportParams()
{
var a = 2f / renderer.SheetSize;
var view = new[]
{
a, 0, 0, 0,
@@ -96,7 +81,7 @@ namespace OpenRA.Mods.Cnc.Traits
public ModelRenderProxy RenderAsync(
WorldRenderer wr, IEnumerable<ModelAnimation> models, in WRot camera, float scale,
in WRot groundOrientation, in WRot lightSource, ImmutableArray<float> lightAmbientColor, ImmutableArray<float> lightDiffuseColor,
in WRot groundOrientation, in WRot lightSource, float[] lightAmbientColor, float[] lightDiffuseColor,
PaletteReference color, PaletteReference normals, PaletteReference shadowPalette)
{
if (!isInFrame)
@@ -158,10 +143,10 @@ namespace OpenRA.Mods.Cnc.Traits
// Corners of the shadow quad, in shadow-space
var corners = new float[][]
{
[stl.X, stl.Y, 0, 1],
[sbr.X, sbr.Y, 0, 1],
[sbr.X, stl.Y, 0, 1],
[stl.X, sbr.Y, 0, 1]
new[] { stl.X, stl.Y, 0, 1 },
new[] { sbr.X, sbr.Y, 0, 1 },
new[] { sbr.X, stl.Y, 0, 1 },
new[] { stl.X, sbr.Y, 0, 1 }
};
var shadowScreenTransform = Util.MatrixMultiply(cameraTransform, invShadowTransform);
@@ -191,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)
{
@@ -228,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);
@@ -254,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);
}
@@ -283,18 +266,18 @@ namespace OpenRA.Mods.Cnc.Traits
ModelRenderData renderData,
IModelCache cache,
float[] t, float[] lightDirection,
ImmutableArray<float> ambientLight, ImmutableArray<float> diffuseLight,
float colorPaletteTextureIndex, float normalsPaletteTextureIndex)
float[] ambientLight, float[] diffuseLight,
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.AsMemory(), 3);
shader.SetVec("DiffuseLight", diffuseLight.AsMemory(), 3);
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()
@@ -315,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();
}
@@ -370,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);
@@ -387,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 ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
new ShaderVertexAttribute("aVertexTexMetadata", ShaderVertexAttributeType.Float, 2, 28),
];
}
}

View File

@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using OpenRA.Primitives;
@@ -31,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)
@@ -65,18 +64,18 @@ namespace OpenRA.Graphics
Buffer.BlockCopy(colors, 0, destination, destinationOffset * 4, Palette.Size * 4);
}
public ImmutablePalette(string filename, ImmutableArray<int> remapTransparent, ImmutableArray<int> remap)
public ImmutablePalette(string filename, int[] remapTransparent, int[] remap)
{
using (var s = File.OpenRead(filename))
LoadFromStream(s, remapTransparent, remap);
}
public ImmutablePalette(Stream s, ImmutableArray<int> remapTransparent, ImmutableArray<int> remapShadow)
public ImmutablePalette(Stream s, int[] remapTransparent, int[] remapShadow)
{
LoadFromStream(s, remapTransparent, remapShadow);
}
void LoadFromStream(Stream s, ImmutableArray<int> remapTransparent, ImmutableArray<int> remapShadow)
void LoadFromStream(Stream s, int[] remapTransparent, int[] remapShadow)
{
using (var reader = new BinaryReader(s))
for (var i = 0; i < Palette.Size; i++)
@@ -104,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)
@@ -143,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)
@@ -154,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);
}
@@ -66,7 +66,6 @@ namespace OpenRA
void PumpInput(IInputHandler inputHandler);
string GetClipboardText();
bool SetClipboardText(string text);
bool TryOpenUrl(string url);
void GrabWindowMouseFocus();
void ReleaseWindowMouseFocus();
@@ -84,19 +83,16 @@ namespace OpenRA
public interface IGraphicsContext : IDisposable
{
IVertexBuffer<T> CreateEmptyVertexBuffer<T>(int size) where T : struct;
IVertexBuffer<T> CreateVertexBuffer<T>(T[] data, bool dynamic = true) where T : struct;
T[] CreateVertices<T>(int size) where T : struct;
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();
@@ -106,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);
@@ -125,32 +114,16 @@ 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);
void SetVec(string name, float x);
void SetVec(string name, float x, float y);
void SetVec(string name, float x, float y, float z);
void SetVec(string name, ReadOnlyMemory<float> vec, int length);
void SetVec(string name, float[] vec, int length);
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 }
@@ -159,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; }
@@ -181,6 +153,12 @@ namespace OpenRA
TriangleList,
}
public readonly struct Range<T>
{
public readonly T Start, End;
public Range(T start, T end) { Start = start; End = end; }
}
public enum WindowMode
{
Windowed,

View File

@@ -10,19 +10,19 @@
#endregion
using System;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.Primitives;
namespace OpenRA.Graphics
{
public class PlayerColorRemap : IPaletteRemap
{
readonly ImmutableArray<int> remapIndices;
readonly int[] remapIndices;
readonly float hue;
readonly float saturation;
readonly float value;
public PlayerColorRemap(ImmutableArray<int> remapIndices, Color color)
public PlayerColorRemap(int[] remapIndices, Color color)
{
this.remapIndices = remapIndices;

View File

@@ -1,45 +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 record struct RenderPostProcessPassVertex(float X, float Y);
[StructLayout(LayoutKind.Sequential)]
public readonly record struct RenderPostProcessPassTexturedVertex(float X, float Y, float S, float T);
public sealed class RenderPostProcessPassShaderBindings : ShaderBindings
{
public RenderPostProcessPassShaderBindings(string name)
: base("postprocess", "postprocess_" + name) { }
public override ShaderVertexAttribute[] Attributes { get; } =
[
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 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;
@@ -191,7 +197,21 @@ namespace OpenRA.Graphics
{
var tr = new float3(br.X, tl.Y, tl.Z);
var bl = new float3(tl.X, br.Y, br.Z);
DrawPolygon([tl, tr, br, bl], width, color, blendMode);
DrawPolygon(new[] { tl, tr, br, bl }, width, color, blendMode);
}
public void FillTriangle(in float3 a, in float3 b, in float3 c, Color color, BlendMode blendMode = BlendMode.Alpha)
{
color = Util.PremultiplyAlpha(color);
var cr = color.R / 255.0f;
var cg = color.G / 255.0f;
var cb = color.B / 255.0f;
var ca = color.A / 255.0f;
vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0);
vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0, 0);
vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
parent.DrawRGBAVertices(vertices, blendMode);
}
public void FillRect(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha)
@@ -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

@@ -37,23 +37,23 @@ namespace OpenRA.Graphics
public interface ISpriteSequenceLoader
{
int BgraSheetSize { get; }
int IndexedSheetSize { get; }
IReadOnlyDictionary<string, ISpriteSequence> ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode node);
}
public sealed class SequenceSet : IDisposable
{
public readonly string TileSet;
readonly ModData modData;
readonly string tileSet;
readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, ISpriteSequence>> images;
public SpriteCache SpriteCache { get; }
public SequenceSet(IReadOnlyFileSystem fileSystem, ModData modData, string tileSet, MiniYaml additionalSequences)
{
this.modData = modData;
TileSet = tileSet;
var rc = modData.Manifest.RendererConstants;
SpriteCache = new SpriteCache(fileSystem, modData.SpriteLoaders, rc.SequenceBgraSheetSize, rc.SequenceIndexedSheetSize);
this.tileSet = tileSet;
SpriteCache = new SpriteCache(fileSystem, modData.SpriteLoaders, modData.SpriteSequenceLoader.BgraSheetSize, modData.SpriteSequenceLoader.IndexedSheetSize);
using (new Support.PerfTimer("LoadSequences"))
images = Load(fileSystem, additionalSequences);
}
@@ -94,10 +94,10 @@ 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);
images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node);
}
return images;

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.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 record struct ShaderVertexAttribute(string Name, ShaderVertexAttributeType Type, int Components, int 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 committing data to the GPU, so let's not delete our data.
if (Game.Renderer == null)
return false;
// Only transfer if the destination has no data that would be lost by overwriting.
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

@@ -16,6 +16,7 @@ using OpenRA.Primitives;
namespace OpenRA.Graphics
{
[Serializable]
public class SheetOverflowException : Exception
{
public SheetOverflowException(string message)
@@ -33,7 +34,7 @@ namespace OpenRA.Graphics
public sealed class SheetBuilder : IDisposable
{
public readonly SheetType Type;
readonly List<Sheet> sheets = [];
readonly List<Sheet> sheets = new();
readonly Func<Sheet> allocateSheet;
readonly int margin;
int rowHeight = 0;
@@ -65,6 +66,9 @@ namespace OpenRA.Graphics
}
}
public SheetBuilder(SheetType t)
: this(t, Game.Settings.Graphics.SheetSize) { }
public SheetBuilder(SheetType t, int sheetSize, int margin = 1)
: this(t, () => AllocateSheet(t, sheetSize), margin) { }
@@ -72,26 +76,22 @@ namespace OpenRA.Graphics
{
CurrentChannel = t == SheetType.Indexed ? TextureChannel.Red : TextureChannel.RGBA;
Type = t;
Current = allocateSheet();
sheets.Add(Current);
this.allocateSheet = allocateSheet;
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)
{
if (Current == null)
{
Current = allocateSheet();
sheets.Add(Current);
}
// 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;
}
@@ -116,12 +116,6 @@ namespace OpenRA.Graphics
public Sprite Allocate(Size imageSize, float scale = 1f) { return Allocate(imageSize, 0, float3.Zero, scale); }
public Sprite Allocate(Size imageSize, float zRamp, in float3 spriteOffset, float scale = 1f)
{
if (Current == null)
{
Current = allocateSheet();
sheets.Add(Current);
}
if (imageSize.Width + p.X + margin > Current.Size.Width)
{
p = new int2(0, p.Y + rowHeight + margin);
@@ -136,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;
}
@@ -153,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

@@ -11,34 +11,31 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
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,
(ImmutableArray<int> Frames, MiniYamlNode.SourceLocation Location, AdjustFrame AdjustFrame, bool Premultiplied)> spriteReservations = [];
readonly Dictionary<string, List<int>> reservationsByFilename = [];
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, Sprite[]> resolvedSprites = [];
readonly Dictionary<int, ISpriteFrame[]> resolvedFrames = new();
readonly Dictionary<int, Sprite[]> resolvedSprites = new();
readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = [];
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>
{
@@ -50,122 +47,116 @@ namespace OpenRA.Graphics
this.loaders = loaders;
}
public int ReserveSprites(string filename, ImmutableArray<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, location, adjustFrame, premultiplied);
reservationsByFilename.GetOrAdd(filename, _ => []).Add(token);
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 != null ? rs.Frames : Enumerable.Range(0, loadedFrames.Length);
var total = rs.Frames != null ? 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();
sb.Current.ReleaseBuffer();
}
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

@@ -9,6 +9,7 @@
*/
#endregion
using System;
using System.Collections.Generic;
using OpenRA.Primitives;
@@ -16,7 +17,7 @@ namespace OpenRA.Graphics
{
public class SpriteRenderable : IPalettedRenderable, IModifyableRenderable, IFinalizedRenderable
{
public static readonly IEnumerable<IRenderable> None = [];
public static readonly IEnumerable<IRenderable> None = Array.Empty<IRenderable>();
readonly Sprite sprite;
readonly WPos pos;

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 = [];
readonly IndexBufferRc indexBufferWrapper;
static readonly int[] CornerVertexMap = { 0, 1, 2, 2, 3, 0 };
public readonly BlendMode BlendMode;
@@ -30,9 +27,8 @@ namespace OpenRA.Graphics
readonly IVertexBuffer<Vertex> vertexBuffer;
readonly Vertex[] vertices;
readonly bool[] ignoreTint;
readonly HashSet<int> dirtyRows = [];
readonly int indexRowStride;
readonly int vertexRowStride;
readonly HashSet<int> dirtyRows = new();
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.Width;
vertices = new Vertex[vertexRowStride * map.MapSize.Height];
vertexBuffer = Game.Renderer.Context.CreateEmptyVertexBuffer<Vertex>(vertices.Length);
vertices = new Vertex[rowStride * map.MapSize.Y];
palettes = new PaletteReference[map.MapSize.X * map.MapSize.Y];
vertexBuffer = Game.Renderer.Context.CreateVertexBuffer(vertices.Length);
indexRowStride = 6 * map.MapSize.Width;
lock (IndexBuffers)
{
indexBufferWrapper = IndexBuffers.GetValue(world, world => new IndexBufferRc(world));
indexBufferWrapper.AddRef();
}
palettes = new PaletteReference[map.MapSize.Width * map.MapSize.Height];
wr.PaletteInvalidated += UpdatePaletteIndices;
if (wr.TerrainLighting != null)
{
ignoreTint = new bool[vertexRowStride * map.MapSize.Height];
ignoreTint = new bool[rowStride * map.MapSize.Y];
wr.TerrainLighting.CellChanged += UpdateTint;
}
}
@@ -75,12 +65,11 @@ 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.Height; row++)
for (var row = 0; row < map.MapSize.Y; row++)
dirtyRows.Add(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,9 +180,9 @@ 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.Width + uv.U] = palette;
palettes[uv.V * map.MapSize.X + uv.U] = palette;
if (worldRenderer.TerrainLighting != null)
{
@@ -209,8 +198,8 @@ namespace OpenRA.Graphics
var cells = restrictToBounds ? viewport.VisibleCellsInsideBounds : viewport.AllVisibleCells;
// Only draw the rows that are visible.
var firstRow = cells.CandidateMapCoords.TopLeft.V.Clamp(0, map.MapSize.Height);
var lastRow = (cells.CandidateMapCoords.BottomRight.V + 1).Clamp(firstRow, map.MapSize.Height);
var firstRow = cells.CandidateMapCoords.TopLeft.V.Clamp(0, map.MapSize.Y);
var lastRow = (cells.CandidateMapCoords.BottomRight.V + 1).Clamp(firstRow, map.MapSize.Y);
Game.Renderer.Flush();
@@ -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,30 +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.Width * world.Map.MapSize.Height));
}
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;
@@ -19,19 +18,9 @@ namespace OpenRA.Graphics
public static class Util
{
// yes, our channel order is nuts.
static readonly int[] ChannelMasks = [2, 1, 0, 3];
static readonly int[] ChannelMasks = { 2, 1, 0, 3 };
public static uint[] CreateQuadIndices(int quads)
{
var indices = new uint[quads * 6];
ReadOnlySpan<uint> cornerVertexMap = [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;
}
}
@@ -277,13 +246,13 @@ namespace OpenRA.Graphics
size.X * angleSin - size.Y * angleCos,
(size.X * angleSin - size.Y * angleCos) * size.Z / size.Y);
return
[
return new float3[]
{
center - ra,
center + rb,
center + ra,
center - rb
];
};
}
/// <summary>
@@ -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 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

@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.Primitives;
@@ -50,13 +49,14 @@ namespace OpenRA.Graphics
readonly Size tileSize;
// Viewport geometry (world-px)
public float2 CenterLocation { get; private set; }
public int2 CenterLocation { get; private set; }
public WPos CenterPosition => worldRenderer.ProjectedPosition(CenterLocation.ToInt2());
public WPos CenterPosition => worldRenderer.ProjectedPosition(CenterLocation);
public int2 TopLeft => CenterLocation.ToInt2() - ViewportSize.ToInt2() / 2;
public int2 BottomRight => CenterLocation.ToInt2() + ViewportSize.ToInt2() / 2;
public Size ViewportSize { get; private set; }
public Rectangle Rectangle => new(TopLeft, new Size(viewportSize.X, viewportSize.Y));
public int2 TopLeft => CenterLocation - viewportSize / 2;
public int2 BottomRight => CenterLocation + viewportSize / 2;
int2 viewportSize;
ProjectedCellRegion cells;
bool cellsDirty = true;
@@ -69,10 +69,6 @@ namespace OpenRA.Graphics
bool unlockMinZoom;
float unlockedMinZoomScale;
float unlockedMinZoom = 1f;
float defaultScale;
bool overrideUserScale;
public Func<float2> ViewportCenterProvider;
public float Zoom
{
@@ -81,7 +77,7 @@ namespace OpenRA.Graphics
private set
{
zoom = value;
ViewportSize = Size.FromInt2((1f / zoom * new float2(Game.Renderer.NativeResolution)).ToInt2());
viewportSize = (1f / zoom * new float2(Game.Renderer.NativeResolution)).ToInt2();
cellsDirty = true;
allCellsDirty = true;
}
@@ -90,13 +86,6 @@ namespace OpenRA.Graphics
public float MinZoom { get; private set; } = 1f;
public float MaxZoom { get; private set; } = 2f;
public void OverrideDefaultHeight(float height)
{
defaultScale = viewportSizes.DefaultScale * Game.Renderer.NativeResolution.Height / height;
overrideUserScale = true;
UpdateViewportZooms(false);
}
public void AdjustZoom(float dz)
{
// Exponential ensures that equal positive and negative steps have the same effect
@@ -108,9 +97,7 @@ namespace OpenRA.Graphics
var oldCenter = worldRenderer.Viewport.ViewToWorldPx(center);
AdjustZoom(dz);
var newCenter = worldRenderer.Viewport.ViewToWorldPx(center);
var candidateCenterLocation = CenterLocation + oldCenter - newCenter;
CenterLocation = candidateCenterLocation.Clamp(mapBounds);
CenterLocation += oldCenter - newCenter;
}
public void ToggleZoom()
@@ -150,17 +137,16 @@ namespace OpenRA.Graphics
public Viewport(WorldRenderer wr, Map map)
{
worldRenderer = wr;
tileSize = map.Rules.TerrainInfo.TileSize;
viewportSizes = Game.ModData.GetOrCreate<WorldViewportSizes>();
var grid = Game.ModData.Manifest.Get<MapGrid>();
viewportSizes = Game.ModData.Manifest.Get<WorldViewportSizes>();
graphicSettings = Game.Settings.Graphics;
defaultScale = viewportSizes.DefaultScale;
// Calculate map bounds in world-px
if (wr.World.Type == WorldType.Editor)
{
// The full map is visible in the editor
var width = map.MapSize.Width * tileSize.Width;
var height = map.MapSize.Height * tileSize.Height;
var width = map.MapSize.X * grid.TileSize.Width;
var height = map.MapSize.Y * grid.TileSize.Height;
if (wr.World.Map.Grid.Type == MapGridType.RectangularIsometric)
height /= 2;
@@ -175,6 +161,8 @@ namespace OpenRA.Graphics
CenterLocation = (tl + br) / 2;
}
tileSize = grid.TileSize;
UpdateViewportZooms();
}
@@ -182,9 +170,6 @@ namespace OpenRA.Graphics
{
if (lastViewportDistance != graphicSettings.ViewportDistance)
UpdateViewportZooms();
if (ViewportCenterProvider != null)
Center(ViewportCenterProvider());
}
static float CalculateMinimumZoom(float minHeight, float maxHeight)
@@ -222,17 +207,15 @@ namespace OpenRA.Graphics
lastViewportDistance = graphicSettings.ViewportDistance;
var vd = graphicSettings.ViewportDistance;
if (overrideUserScale || (viewportSizes.AllowNativeZoom && vd == WorldViewport.Native))
MinZoom = defaultScale;
if (viewportSizes.AllowNativeZoom && vd == WorldViewport.Native)
MinZoom = viewportSizes.DefaultScale;
else
{
var range = viewportSizes.GetSizeRange(vd);
MinZoom = CalculateMinimumZoom(range.X, range.Y) * defaultScale;
MinZoom = CalculateMinimumZoom(range.X, range.Y) * viewportSizes.DefaultScale;
}
MaxZoom = Math.Min(
MinZoom * viewportSizes.MaxZoomScale,
Game.Renderer.NativeResolution.Height * defaultScale / viewportSizes.MaxZoomWindowHeight);
MaxZoom = Math.Min(MinZoom * viewportSizes.MaxZoomScale, Game.Renderer.NativeResolution.Height * viewportSizes.DefaultScale / viewportSizes.MaxZoomWindowHeight);
if (unlockMinZoom)
{
@@ -248,17 +231,16 @@ 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)
{
var world = ViewToWorldPx(view);
var world = worldRenderer.Viewport.ViewToWorldPx(view);
var map = worldRenderer.World.Map;
var candidates = CandidateMouseoverCells(world).ToList();
@@ -271,7 +253,7 @@ namespace OpenRA.Graphics
{
var ramp = map.Grid.Ramps[map.Ramp.Contains(uv) ? map.Ramp[uv] : 0];
var pos = map.CenterOfCell(uv.ToCPos(map)) - new WVec(0, 0, ramp.CenterHeightOffset);
var screen = ramp.Corners.Select(c => worldRenderer.ScreenPxPosition(pos + c)).ToImmutableArray();
var screen = ramp.Corners.Select(c => worldRenderer.ScreenPxPosition(pos + c)).ToArray();
if (screen.PolygonContains(world))
return uv.ToCPos(map);
}
@@ -300,42 +282,27 @@ 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--)
yield return new MPos(u, v);
}
public int2 ViewToWorldPx(int2 view) => (graphicSettings.UIScale / Zoom * view.ToFloat2() + CenterLocation - ViewportSize.ToInt2() / 2).ToInt2();
public int2 WorldToViewPx(int2 world) => (Zoom / graphicSettings.UIScale * (world - CenterLocation + ViewportSize.ToInt2() / 2)).ToInt2();
public int2 WorldToViewPx(in float3 world) => (Zoom / graphicSettings.UIScale * (world - CenterLocation + ViewportSize.ToInt2() / 2).XY).ToInt2();
public int2 ViewToWorldPx(int2 view) { return (graphicSettings.UIScale / Zoom * view.ToFloat2()).ToInt2() + TopLeft; }
public int2 WorldToViewPx(int2 world) { return (Zoom / graphicSettings.UIScale * (world - TopLeft).ToFloat2()).ToInt2(); }
public int2 WorldToViewPx(in float3 world) { return (Zoom / graphicSettings.UIScale * (world - TopLeft).XY).ToInt2(); }
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)
@@ -345,17 +312,10 @@ namespace OpenRA.Graphics
allCellsDirty = true;
}
public void Center(float2 pos)
{
CenterLocation = worldRenderer.ScreenPosition(pos).Clamp(mapBounds);
cellsDirty = true;
allCellsDirty = true;
}
public void Scroll(float2 delta, bool ignoreBorders)
{
// Convert scroll delta from world-px to viewport-px
CenterLocation += 1f / Zoom * delta;
CenterLocation += (1f / Zoom * delta).ToInt2();
cellsDirty = true;
allCellsDirty = true;

View File

@@ -12,7 +12,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using OpenRA.Effects;
using OpenRA.Primitives;
using OpenRA.Traits;
@@ -32,61 +31,44 @@ namespace OpenRA.Graphics
public event Action PaletteInvalidated = null;
readonly HashSet<Actor> onScreenActors = [];
readonly HashSet<Actor> onScreenActors = new();
readonly HardwarePalette palette = new();
readonly Dictionary<string, PaletteReference> palettes = [];
readonly Dictionary<string, PaletteReference> palettes = new();
readonly IRenderTerrain terrainRenderer;
readonly Lazy<DebugVisualizations> debugVis;
readonly Func<string, PaletteReference> createPaletteReference;
readonly bool enableDepthBuffer;
readonly List<IFinalizedRenderable> preparedRenderables = [];
readonly List<IFinalizedRenderable> preparedOverlayRenderables = [];
readonly List<IFinalizedRenderable> preparedAnnotationRenderables = [];
readonly List<IFinalizedRenderable> preparedRenderables = new();
readonly List<IFinalizedRenderable> preparedOverlayRenderables = new();
readonly List<IFinalizedRenderable> preparedAnnotationRenderables = new();
readonly List<IRenderable> renderablesBuffer = [];
readonly IRenderer[] renderers;
readonly IRenderPostProcessPass[] postProcessPasses;
long[] renderablesKeysBuffer = [];
readonly List<IRenderable> renderablesBuffer = new();
internal WorldRenderer(ModData modData, World world)
{
World = world;
TileSize = World.Map.Rules.TerrainInfo.TileSize;
TileSize = World.Map.Grid.TileSize;
TileScale = World.Map.Grid.TileScale;
Viewport = new Viewport(this, world.Map);
createPaletteReference = CreatePaletteReference;
var mapGrid = modData.GetOrCreate<MapGrid>();
var mapGrid = modData.Manifest.Get<MapGrid>();
enableDepthBuffer = mapGrid.EnableDepthBuffer;
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();
debugVis = Exts.Lazy(() => world.WorldActor.TraitOrDefault<DebugVisualizations>());
}
public void UpdatePalettesForPlayer(string internalName, Color color, bool replaceExisting)
@@ -159,14 +141,7 @@ namespace OpenRA.Graphics
renderablesBuffer.AddRange(e.Render(this));
// Renderables must be ordered using a stable sorting algorithm to avoid flickering artefacts
if (renderablesKeysBuffer.Length < renderablesBuffer.Count)
renderablesKeysBuffer = new long[Exts.NextPowerOf2(renderablesBuffer.Count)];
for (var i = 0; i < renderablesBuffer.Count; i++)
renderablesKeysBuffer[i] = ((long)RenderableZPositionComparisonKey(renderablesBuffer[i]) << 32) + i;
var keys = renderablesKeysBuffer.AsSpan(0, renderablesBuffer.Count);
keys.Sort(CollectionsMarshal.AsSpan(renderablesBuffer));
foreach (var renderable in renderablesBuffer)
foreach (var renderable in renderablesBuffer.OrderBy(RenderableZPositionComparisonKey))
preparedRenderables.Add(renderable.PrepareRender(this));
// PERF: Reuse collection to avoid allocations.
@@ -295,8 +270,6 @@ namespace OpenRA.Graphics
if (enableDepthBuffer)
Game.Renderer.ClearDepthBuffer();
ApplyPostProcessing(PostProcessPassType.AfterActors);
World.ApplyToActorsWithTrait<IRenderAboveWorld>((actor, trait) =>
{
if (actor.IsInWorld && !actor.Disposed)
@@ -306,8 +279,6 @@ namespace OpenRA.Graphics
if (enableDepthBuffer)
Game.Renderer.ClearDepthBuffer();
ApplyPostProcessing(PostProcessPassType.AfterWorld);
World.ApplyToActorsWithTrait<IRenderShroud>((actor, trait) => trait.RenderShroud(this));
if (enableDepthBuffer)
@@ -321,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();
@@ -380,8 +337,6 @@ namespace OpenRA.Graphics
}
}
ApplyPostProcessing(PostProcessPassType.AfterAnnotations);
Game.Renderer.Flush();
preparedRenderables.Clear();
@@ -395,22 +350,12 @@ namespace OpenRA.Graphics
Game.Renderer.SetPalette(palette);
}
/// <summary>
/// Converts a world position to a screen position.
/// </summary>
// Conversion between world and screen coordinates
public float2 ScreenPosition(WPos pos)
{
return new float2((float)TileSize.Width * pos.X / TileScale, (float)TileSize.Height * (pos.Y - pos.Z) / TileScale);
}
/// <summary>
/// Converts a world position to a screen position.
/// </summary>
public float2 ScreenPosition(float2 pos)
{
return new float2(TileSize.Width * pos.X / TileScale, TileSize.Height * pos.Y / TileScale);
}
public float3 Screen3DPosition(WPos pos)
{
// The projection from world coordinates to screen coordinates has
@@ -450,7 +395,7 @@ namespace OpenRA.Graphics
public float[] ScreenVector(in WVec vec)
{
var xyz = ScreenVectorComponents(vec);
return [xyz.X, xyz.Y, xyz.Z, 1f];
return new[] { xyz.X, xyz.Y, xyz.Z, 1f };
}
public int2 ScreenPxOffset(in WVec vec)

View File

@@ -9,27 +9,18 @@
*/
#endregion
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Linq;
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 FrozenSet<string> Types = FrozenSet<string>.Empty;
[FluentReference]
public readonly FrozenSet<string> Contexts = FrozenSet<string>.Empty;
public readonly HashSet<string> Types = new();
public readonly HashSet<string> Contexts = new();
public readonly bool Readonly = false;
public bool HasDuplicates { get; internal set; }
@@ -40,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<FrozenSet<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<ImmutableArray<string>>("Contexts", contextYaml.Value)
.Select(c => ContextFluentPrefix + "." + c).ToFrozenSet();
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

@@ -17,16 +17,13 @@ namespace OpenRA
{
public sealed class HotkeyManager
{
[YamlNode("Keys", shared: true)]
sealed class HotkeySettings : SettingsModule { }
readonly Dictionary<string, Hotkey> settings;
readonly Dictionary<string, HotkeyDefinition> definitions = new();
readonly Dictionary<string, Hotkey> keys = new();
readonly Dictionary<string, HotkeyDefinition> definitions = [];
readonly Dictionary<string, Hotkey> keys = [];
readonly HotkeySettings hotkeySettings;
public HotkeyManager(IReadOnlyFileSystem fileSystem, ObjectCreator objectCreator, Manifest manifest)
public HotkeyManager(IReadOnlyFileSystem fileSystem, Dictionary<string, Hotkey> settings, Manifest manifest)
{
hotkeySettings = Game.Settings.GetOrCreate<HotkeySettings>(objectCreator, manifest.Id);
this.settings = settings;
var keyDefinitions = MiniYaml.Load(fileSystem, manifest.Hotkeys, null);
foreach (var kd in keyDefinitions)
@@ -36,9 +33,11 @@ namespace OpenRA
keys[kd.Key] = definition.Default;
}
foreach (var node in hotkeySettings.Yaml.Nodes)
if (definitions.TryGetValue(node.Key, out var definition) && !definition.Readonly)
keys[node.Key] = FieldLoader.GetValue<Hotkey>(node.Key, node.Value.Value);
foreach (var kv in settings)
{
if (definitions.TryGetValue(kv.Key, out var definition) && !definition.Readonly)
keys[kv.Key] = kv.Value;
}
foreach (var hd in definitions)
hd.Value.HasDuplicates = GetFirstDuplicate(hd.Value, this[hd.Value.Name].GetValue()) != null;
@@ -69,9 +68,10 @@ namespace OpenRA
return;
keys[name] = value;
hotkeySettings.Yaml.Nodes.RemoveAll(n => n.Key == name);
if (value != definition.Default)
hotkeySettings.Yaml.Nodes.Add(new MiniYamlNodeBuilder(name, FieldSaver.FormatValue(value)));
settings[name] = value;
else
settings.Remove(name);
var hadDuplicates = definition.HasDuplicates;
definition.HasDuplicates = GetFirstDuplicate(definition, this[definition.Name].GetValue()) != null;
@@ -108,10 +108,5 @@ namespace OpenRA
public HotkeyReference this[string name] => new(GetHotkeyReference(name));
public IEnumerable<HotkeyDefinition> Definitions => definitions.Values;
public void Save()
{
hotkeySettings.Save();
}
}
}

View File

@@ -26,7 +26,11 @@ namespace OpenRA
var total = response.Content.Headers.ContentLength ?? -1;
var canReportProgress = total > 0;
await using (var contentStream = await response.Content.ReadAsStreamAsync(token))
#if NET5_0_OR_GREATER
using (var contentStream = await response.Content.ReadAsStreamAsync(token))
#else
using (var contentStream = await response.Content.ReadAsStreamAsync())
#endif
{
var totalRead = 0L;
var buffer = new byte[8192];

View File

@@ -27,21 +27,25 @@ namespace OpenRA
public static bool TryParse(string s, out Hotkey result)
{
result = Invalid;
if (s == null)
if (string.IsNullOrWhiteSpace(s))
return false;
Span<Range> ranges = stackalloc Range[2];
var span = s.AsSpan();
var count = span.Split(ranges, ' ');
if (count == 0)
return false;
var parts = s.Split(' ');
if (!Enum.TryParse(span[ranges[0]], true, out Keycode key))
return false;
if (!Enum<Keycode>.TryParse(parts[0], true, out var key))
{
if (!int.TryParse(parts[0], out var c))
return false;
key = (Keycode)c;
}
var mods = Modifiers.None;
if (count == 2 && !Enum.TryParse(span[ranges[1]], true, out mods))
return false;
if (parts.Length >= 2)
{
var modString = s[s.IndexOf(' ')..];
if (!Enum<Modifiers>.TryParse(modString, true, out mods))
return false;
}
result = new Hotkey(key, mods);
return true;
@@ -87,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
{
@@ -23,7 +22,25 @@ namespace OpenRA
}
public enum MouseInputEvent { Down, Move, Up, Scroll }
public record struct MouseInput(MouseInputEvent Event, MouseButton Button, int2 Location, int2 Delta, Modifiers Modifiers, int MultiTapCount);
public struct MouseInput
{
public MouseInputEvent Event;
public MouseButton Button;
public int2 Location;
public int2 Delta;
public Modifiers Modifiers;
public int MultiTapCount;
public MouseInput(MouseInputEvent ev, MouseButton button, int2 location, int2 delta, Modifiers mods, int multiTapCount)
{
Event = ev;
Button = button;
Location = location;
Delta = delta;
Modifiers = mods;
MultiTapCount = multiTapCount;
}
}
[Flags]
public enum MouseButton
@@ -44,33 +61,6 @@ namespace OpenRA
Meta = 8,
}
public static class ModifiersExts
{
[FluentReference]
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

@@ -50,4 +50,11 @@ namespace OpenRA
Sync.RunUnsynced(world, () => Ui.HandleInput(input));
}
}
public class MouseButtonPreference
{
public MouseButton Action => Game.Settings.Game.UseClassicMouseStyle ? MouseButton.Left : MouseButton.Right;
public MouseButton Cancel => Game.Settings.Game.UseClassicMouseStyle ? MouseButton.Right : MouseButton.Left;
}
}

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

@@ -56,7 +56,8 @@ namespace OpenRA
// (b) Therefore:
// - au + 2bv adds (a + b) to (x, y)
// - a correction factor is added if v is odd
var y = (V - (V & 1)) / 2 - U;
var offset = (V & 1) == 1 ? 1 : 0;
var y = (V - offset) / 2 - U;
var x = V - y;
return new CPos(x, y);
}

View File

@@ -9,92 +9,103 @@
*/
#endregion
using System.Collections.Frozen;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Primitives;
namespace OpenRA
{
public class ModMetadata
public interface IGlobalModData { }
public sealed class TerrainFormat : IGlobalModData
{
// FieldLoader used here, must matching naming in YAML.
#pragma warning disable IDE1006 // Naming Styles
[FluentReference]
public readonly string Title;
public readonly string Version;
public readonly string Website;
public readonly string WebIcon32;
[FluentReference(optional: true)]
public 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 readonly string Type;
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
public TerrainFormat(MiniYaml yaml)
{
Type = yaml.Value;
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
}
}
public class RendererConstants
public sealed class SpriteSequenceFormat : IGlobalModData
{
public readonly int FontSheetSize = 512;
public readonly int CursorSheetSize = 512;
public readonly int MapPreviewSheetSize = 2048;
public readonly int SequenceBgraSheetSize = 2048;
public readonly int SequenceIndexedSheetSize = 2048;
public readonly int VertexBatchSize = 8192;
public readonly string Type;
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
public SpriteSequenceFormat(MiniYaml yaml)
{
Type = yaml.Value;
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
}
}
public sealed class ModelSequenceFormat : IGlobalModData
{
public readonly string Type;
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
public ModelSequenceFormat(MiniYaml yaml)
{
Type = yaml.Value;
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
}
}
public class ModMetadata
{
public string Title;
public string Version;
public string Website;
public string WebIcon32;
public string WindowTitle;
public bool Hidden;
}
/// <summary>Describes what is to be loaded in order to run a mod.</summary>
public sealed class Manifest
public sealed class Manifest : IDisposable
{
public readonly string Id;
public readonly IReadOnlyPackage Package;
public readonly ModMetadata Metadata;
public readonly ImmutableArray<string>
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 FrozenDictionary<string, string> MapFolders;
public readonly MiniYaml FileSystem;
public readonly IReadOnlyDictionary<string, string> Packages;
public readonly IReadOnlyDictionary<string, string> MapFolders;
public readonly MiniYaml LoadScreen;
public readonly string DefaultOrderGenerator;
public readonly RendererConstants RendererConstants;
public readonly ImmutableArray<string> Assemblies = [];
public readonly ImmutableArray<string> SoundFormats = [];
public readonly ImmutableArray<string> SpriteFormats = [];
public readonly ImmutableArray<string> PackageFormats = [];
public readonly ImmutableArray<string> VideoFormats = [];
public readonly string SpriteSequenceFormat;
public readonly string TerrainFormat;
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>();
// TODO: This should be controlled by a user-selected translation bundle!
public readonly string FluentCulture = "en";
public readonly bool AllowUnusedFluentMessagesInExternalPackages = true;
static readonly FrozenSet<string> ReservedModuleNames = new HashSet<string>
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",
"SpriteSequenceFormat", "TerrainFormat", "RequiresMods", "PackageFormats", "AllowUnusedFluentMessagesInExternalPackages", "RendererConstants"
}.ToFrozenSet();
"RequiresMods", "PackageFormats"
};
public readonly FrozenDictionary<string, MiniYaml> GlobalModData;
readonly TypeDictionary modules = new();
readonly Dictionary<string, MiniYaml> yaml;
bool customDataLoaded;
public Manifest(string modId, IReadOnlyPackage package)
{
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).ToList();
var nodes = MiniYaml.FromStream(package.GetStream("mod.yaml"), "mod.yaml");
for (var i = nodes.Count - 1; i >= 0; i--)
{
if (nodes[i].Key != "Include")
@@ -107,31 +118,32 @@ 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
var yaml = new MiniYaml(null, MiniYaml.Merge([nodes])).ToDictionary();
yaml = new MiniYaml(null, MiniYaml.Merge(new[] { nodes })).ToDictionary();
Metadata = FieldLoader.Load<ModMetadata>(yaml["Metadata"]);
// 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");
@@ -148,59 +160,131 @@ namespace OpenRA
if (yaml.TryGetValue("SupportsMapsFrom", out var entry))
compat.AddRange(entry.Value.Split(',').Select(c => c.Trim()));
MapCompatibility = compat.ToImmutableArray();
MapCompatibility = compat.ToArray();
if (yaml.TryGetValue("DefaultOrderGenerator", out entry))
DefaultOrderGenerator = entry.Value;
if (yaml.TryGetValue("Assemblies", out entry))
Assemblies = FieldLoader.GetValue<ImmutableArray<string>>("Assemblies", entry.Value);
if (yaml.TryGetValue("PackageFormats", out entry))
PackageFormats = FieldLoader.GetValue<ImmutableArray<string>>("PackageFormats", entry.Value);
PackageFormats = FieldLoader.GetValue<string[]>("PackageFormats", entry.Value);
if (yaml.TryGetValue("SoundFormats", out entry))
SoundFormats = FieldLoader.GetValue<ImmutableArray<string>>("SoundFormats", entry.Value);
SoundFormats = FieldLoader.GetValue<string[]>("SoundFormats", entry.Value);
if (yaml.TryGetValue("SpriteFormats", out entry))
SpriteFormats = FieldLoader.GetValue<ImmutableArray<string>>("SpriteFormats", entry.Value);
SpriteFormats = FieldLoader.GetValue<string[]>("SpriteFormats", entry.Value);
if (yaml.TryGetValue("VideoFormats", out entry))
VideoFormats = FieldLoader.GetValue<ImmutableArray<string>>("VideoFormats", entry.Value);
VideoFormats = FieldLoader.GetValue<string[]>("VideoFormats", entry.Value);
}
if (yaml.TryGetValue("SpriteSequenceFormat", out entry))
SpriteSequenceFormat = entry.Value;
public void LoadCustomData(ObjectCreator oc)
{
foreach (var kv in yaml)
{
if (reservedModuleNames.Contains(kv.Key))
continue;
if (yaml.TryGetValue("TerrainFormat", out entry))
TerrainFormat = entry.Value;
var t = oc.FindType(kv.Key);
if (t == null || !typeof(IGlobalModData).IsAssignableFrom(t))
throw new InvalidDataException($"`{kv.Key}` is not a valid mod manifest entry.");
if (yaml.TryGetValue("AllowUnusedFluentMessagesInExternalPackages", out entry))
AllowUnusedFluentMessagesInExternalPackages =
FieldLoader.GetValue<bool>("AllowUnusedFluentMessagesInExternalPackages", entry.Value);
IGlobalModData module;
var ctor = t.GetConstructor(new[] { typeof(MiniYaml) });
if (ctor != null)
{
// Class has opted-in to DIY initialization
module = (IGlobalModData)ctor.Invoke(new object[] { kv.Value });
}
else
{
// Automatically load the child nodes using FieldLoader
module = oc.CreateObject<IGlobalModData>(kv.Key);
FieldLoader.Load(module, kv.Value);
}
if (yaml.TryGetValue("RendererConstants", out entry))
RendererConstants = FieldLoader.Load<RendererConstants>(entry);
modules.Add(module);
}
customDataLoaded = true;
}
static string[] YamlList(Dictionary<string, MiniYaml> yaml, string key)
{
if (!yaml.ContainsKey(key))
return Array.Empty<string>();
return yaml[key].ToDictionary().Keys.ToArray();
}
static IReadOnlyDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
{
if (!yaml.ContainsKey(key))
return new Dictionary<string, string>();
return yaml[key].ToDictionary(my => my.Value);
}
public bool Contains<T>() where T : IGlobalModData
{
return modules.Contains<T>();
}
/// <summary>Load a cached IGlobalModData instance.</summary>
public T Get<T>() where T : IGlobalModData
{
if (!customDataLoaded)
throw new InvalidOperationException("Attempted to call Manifest.Get() before loading custom data!");
var module = modules.GetOrDefault<T>();
// Lazily create the default values if not explicitly defined.
if (module == null)
{
module = (T)Game.ModData.ObjectCreator.CreateBasic(typeof(T));
modules.Add(module);
}
return module;
}
/// <summary>
/// Load an uncached IGlobalModData instance directly from the manifest yaml.
/// This should only be used by external mods that want to query data from this mod.
/// </summary>
public T Get<T>(ObjectCreator oc) where T : IGlobalModData
{
var t = typeof(T);
if (!yaml.TryGetValue(t.Name, out var data))
{
// Lazily create the default values if not explicitly defined.
return (T)oc.CreateBasic(t);
}
IGlobalModData module;
var ctor = t.GetConstructor(new[] { typeof(MiniYaml) });
if (ctor != null)
{
// Class has opted-in to DIY initialization
module = (IGlobalModData)ctor.Invoke(new object[] { data.Value });
}
else
RendererConstants = new RendererConstants();
{
// Automatically load the child nodes using FieldLoader
module = oc.CreateObject<IGlobalModData>(t.Name);
FieldLoader.Load(module, data);
}
GlobalModData = yaml.Where(n => !ReservedModuleNames.Contains(n.Key))
.ToFrozenDictionary(n => n.Key, n => n.Value);
return (T)module;
}
static ImmutableArray<string> YamlList(Dictionary<string, MiniYaml> yaml, string key)
public void Dispose()
{
if (!yaml.TryGetValue(key, out var value))
return [];
return value.Nodes.Select(n => n.Key).ToImmutableArray();
}
static FrozenDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
{
if (!yaml.TryGetValue(key, out var value))
return FrozenDictionary<string, string>.Empty;
return value.ToDictionary(my => my.Value).ToFrozenDictionary();
foreach (var module in modules)
{
var disposableModule = module as IDisposable;
disposableModule?.Dispose();
}
}
}
}

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)
{
@@ -216,8 +217,10 @@ namespace OpenRA
}
}
public class LocationInit(CPos value) : ValueActorInit<CPos>(value), ISingleInstanceInit
public class LocationInit : ValueActorInit<CPos>, ISingleInstanceInit
{
public LocationInit(CPos value)
: base(value) { }
}
public class OwnerInit : ActorInit, ISingleInstanceInit

View File

@@ -14,7 +14,7 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using OpenRA.Primitives;
using OpenRA.Traits;
@@ -30,15 +30,15 @@ namespace OpenRA
internal TypeDictionary InitDict => initDict.Value;
public ActorReference(string type)
: this(type, new MiniYaml("")) { }
: this(type, new Dictionary<string, MiniYaml>()) { }
public ActorReference(string type, MiniYaml inits)
public ActorReference(string type, Dictionary<string, MiniYaml> inits)
{
Type = type;
initDict = Exts.Lazy(() =>
{
var dict = new TypeDictionary();
foreach (var i in inits.Nodes)
foreach (var i in inits)
{
var init = LoadInit(i.Key, i.Value);
if (init is ISingleInstanceInit && dict.Contains(init.GetType()))
@@ -54,8 +54,13 @@ namespace OpenRA
public ActorReference(string type, TypeDictionary inits)
{
Type = type;
var initsClone = new TypeDictionary(inits);
initDict = Exts.Lazy(() => initsClone);
initDict = new Lazy<TypeDictionary>(() =>
{
var dict = new TypeDictionary();
foreach (var i in inits)
dict.Add(i);
return dict;
});
}
static ActorInit LoadInit(string initName, MiniYaml initYaml)
@@ -65,21 +70,21 @@ namespace OpenRA
if (type == null)
throw new InvalidDataException($"Unknown initializer type '{initInstance[0]}Init'");
var init = (ActorInit)RuntimeHelpers.GetUninitializedObject(type);
var init = (ActorInit)FormatterServices.GetUninitializedObject(type);
if (initInstance.Length > 1)
type.GetField(nameof(ActorInit.InstanceName)).SetValue(init, initInstance[1]);
var loader = type.GetMethod("Initialize", [typeof(MiniYaml)]);
var loader = type.GetMethod("Initialize", new[] { typeof(MiniYaml) });
if (loader == null)
throw new InvalidDataException($"{initInstance[0]}Init does not define a yaml-assignable type.");
loader.Invoke(init, [initYaml]);
loader.Invoke(init, new[] { initYaml });
return init;
}
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)
@@ -93,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(); }
@@ -105,7 +110,11 @@ namespace OpenRA
public ActorReference Clone()
{
return new ActorReference(Type, initDict.Value);
var clone = new ActorReference(Type);
foreach (var init in initDict.Value)
clone.initDict.Value.Add(init);
return clone;
}
public void Add(ActorInit init)
@@ -116,15 +125,6 @@ namespace OpenRA
InitDict.Add(init);
}
public void Replace<T>(T init) where T : ActorInit, ISingleInstanceInit
{
var original = GetOrDefault<T>();
if (original != null)
Remove(original);
Add(init);
}
public void Remove(ActorInit o) { initDict.Value.Remove(o); }
public int RemoveAll<T>() where T : ActorInit
@@ -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,121 +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;
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();
}
/// <summary>Returns the minimal region that covers at least the specified cells.</summary>
public static CellCoordsRegion BoundingRegion(IReadOnlyCollection<CPos> cells)
{
if (cells == null || cells.Count == 0)
throw new ArgumentException("cells must not be null or empty.", nameof(cells));
var minX = int.MaxValue;
var minY = int.MaxValue;
var maxX = int.MinValue;
var maxY = int.MinValue;
foreach (var cell in cells)
{
if (minX > cell.X)
minX = cell.X;
if (maxX < cell.X)
maxX = cell.X;
if (minY > cell.Y)
minY = cell.Y;
if (maxY < cell.Y)
maxY = cell.Y;
}
return new CellCoordsRegion(new CPos(minX, minY), new CPos(maxX, maxY));
}
public CPos TopLeft { get; }
public CPos BottomRight { get; }
}
}

View File

@@ -63,16 +63,12 @@ namespace OpenRA
var u = (x - y) / 2;
var v = x + y;
if (!Bounds.Contains(u, v))
throw new IndexOutOfRangeException();
return v * Size.Width + u;
}
// Resolve an array index from map coordinates
int Index(MPos uv)
{
if (!Bounds.Contains(uv.U, uv.V))
throw new IndexOutOfRangeException();
return uv.V * Size.Width + uv.U;
}
@@ -149,9 +145,6 @@ namespace OpenRA
{
return uv.Clamp(new Rectangle(0, 0, Size.Width - 1, Size.Height - 1));
}
public CellRegion CellRegion =>
new(GridType, new MPos(0, 0), new MPos(Size.Width - 1, Size.Height - 1));
}
// Helper functions

View File

@@ -25,7 +25,7 @@ namespace OpenRA
protected readonly Rectangle Bounds;
protected CellLayerBase(Map map)
: this(map.Grid.Type, map.MapSize) { }
: this(map.Grid.Type, new Size(map.MapSize.X, map.MapSize.Y)) { }
protected CellLayerBase(MapGridType gridType, Size size)
{
@@ -40,19 +40,19 @@ namespace OpenRA
if (Size != anotherLayer.Size || GridType != anotherLayer.GridType)
throw new ArgumentException("Layers must have a matching size and shape (grid type).", nameof(anotherLayer));
anotherLayer.Entries.AsSpan().CopyTo(Entries);
Array.Copy(anotherLayer.Entries, Entries, Entries.Length);
}
/// <summary>Clears the layer contents with their default value.</summary>
public virtual void Clear()
{
Entries.AsSpan().Clear();
Array.Clear(Entries, 0, Entries.Length);
}
/// <summary>Clears the layer contents with a known value.</summary>
public virtual void Clear(T clearValue)
{
Entries.AsSpan().Fill(clearValue);
Array.Fill(Entries, clearValue);
}
public IEnumerator<T> GetEnumerator()
@@ -64,15 +64,5 @@ namespace OpenRA
{
return GetEnumerator();
}
public ReadOnlyMemory<T> AsReadOnlyMemory()
{
return Entries.AsMemory();
}
public Memory<T> AsMemory()
{
return Entries.AsMemory();
}
}
}

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;
@@ -33,12 +32,12 @@ namespace OpenRA
public readonly uint HeightsOffset;
public readonly uint ResourcesOffset;
public BinaryDataHeader(Stream s, Size expectedSize)
public BinaryDataHeader(Stream s, int2 expectedSize)
{
Format = s.ReadUInt8();
var width = s.ReadUInt16();
var height = s.ReadUInt16();
if (width != expectedSize.Width || height != expectedSize.Height)
if (width != expectedSize.X || height != expectedSize.Y)
throw new InvalidDataException("Invalid tile data");
if (Format == 1)
@@ -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)
@@ -112,7 +111,7 @@ namespace OpenRA
else if (type == Type.MiniYaml)
field.SetValue(map, node.Value);
else
FieldLoader.LoadFieldOrProperty(map, fieldName, node.Value.Value);
FieldLoader.LoadField(map, fieldName, node.Value.Value);
}
if (property != null)
@@ -122,7 +121,7 @@ namespace OpenRA
else if (type == Type.MiniYaml)
property.SetValue(map, node.Value, null);
else
FieldLoader.LoadFieldOrProperty(map, fieldName, node.Value.Value);
FieldLoader.LoadField(map, fieldName, node.Value.Value);
}
}
@@ -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
@@ -158,28 +157,28 @@ 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
public int MapFormat { get; private set; }
@@ -193,25 +192,25 @@ namespace OpenRA
public bool LockPreview;
public Rectangle Bounds;
public MapVisibility Visibility = MapVisibility.Lobby;
public ImmutableArray<string> Categories = ["Conquest"];
public string[] Categories = { "Conquest" };
public Size MapSize { get; private set; }
public int2 MapSize { get; private set; }
// Player and actor yaml. Public for access by the map importers and lint checks.
public IReadOnlyCollection<MiniYamlNode> PlayerDefinitions = [];
public IReadOnlyCollection<MiniYamlNode> ActorDefinitions = [];
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 = [];
public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = new();
// Generated data
public readonly MapGrid Grid;
@@ -275,15 +274,12 @@ 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
if (streams.Count == 0)
return CryptoUtil.SHA1Hash([]);
return CryptoUtil.SHA1Hash(Array.Empty<byte>());
var merged = streams[0];
for (var i = 1; i < streams.Count; i++)
@@ -321,25 +317,26 @@ namespace OpenRA
/// Initializes a new map created by the editor or importer.
/// The map will not receive a valid UID until after it has been saved and reloaded.
/// </summary>
public Map(ModData modData, ITerrainInfo terrainInfo, Size size)
public Map(ModData modData, ITerrainInfo terrainInfo, int width, int height)
{
this.modData = modData;
MapSize = size;
Grid = modData.GetOrCreate<MapGrid>();
var size = new Size(width, height);
Grid = modData.Manifest.Get<MapGrid>();
Title = "Name your map here";
Author = "Your name here";
MapSize = new int2(size);
Tileset = terrainInfo.Id;
// Empty rules that can be added to by the importers.
// Will be dropped on save if nothing is added to it
RuleDefinitions = new MiniYaml("");
Tiles = new CellLayer<TerrainTile>(Grid.Type, MapSize);
Resources = new CellLayer<ResourceTile>(Grid.Type, MapSize);
Height = new CellLayer<byte>(Grid.Type, MapSize);
Ramp = new CellLayer<byte>(Grid.Type, MapSize);
Tiles = new CellLayer<TerrainTile>(Grid.Type, size);
Resources = new CellLayer<ResourceTile>(Grid.Type, size);
Height = new CellLayer<byte>(Grid.Type, size);
Ramp = new CellLayer<byte>(Grid.Type, size);
Tiles.Clear(terrainInfo.DefaultTerrainTile);
if (Grid.MaximumTerrainHeight > 0)
@@ -360,22 +357,23 @@ 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 ?? [];
ActorDefinitions = yaml.NodeWithKeyOrDefault("Actors")?.Value.Nodes ?? [];
PlayerDefinitions = MiniYaml.NodesOrEmpty(yaml, "Players");
ActorDefinitions = MiniYaml.NodesOrEmpty(yaml, "Actors");
Grid = modData.GetOrCreate<MapGrid>();
Grid = modData.Manifest.Get<MapGrid>();
Tiles = new CellLayer<TerrainTile>(Grid.Type, MapSize);
Resources = new CellLayer<ResourceTile>(Grid.Type, MapSize);
Height = new CellLayer<byte>(Grid.Type, MapSize);
Ramp = new CellLayer<byte>(Grid.Type, MapSize);
var size = new Size(MapSize.X, MapSize.Y);
Tiles = new CellLayer<TerrainTile>(Grid.Type, size);
Resources = new CellLayer<ResourceTile>(Grid.Type, size);
Height = new CellLayer<byte>(Grid.Type, size);
Ramp = new CellLayer<byte>(Grid.Type, size);
using (var s = Package.GetStream("map.bin"))
{
@@ -383,9 +381,9 @@ namespace OpenRA
if (header.TilesOffset > 0)
{
s.Position = header.TilesOffset;
for (var i = 0; i < MapSize.Width; i++)
for (var i = 0; i < MapSize.X; i++)
{
for (var j = 0; j < MapSize.Height; j++)
for (var j = 0; j < MapSize.Y; j++)
{
var tile = s.ReadUInt16();
var index = s.ReadUInt8();
@@ -402,9 +400,9 @@ namespace OpenRA
if (header.ResourcesOffset > 0)
{
s.Position = header.ResourcesOffset;
for (var i = 0; i < MapSize.Width; i++)
for (var i = 0; i < MapSize.X; i++)
{
for (var j = 0; j < MapSize.Height; j++)
for (var j = 0; j < MapSize.Y; j++)
{
var type = s.ReadUInt8();
var density = s.ReadUInt8();
@@ -416,8 +414,8 @@ namespace OpenRA
if (header.HeightsOffset > 0)
{
s.Position = header.HeightsOffset;
for (var i = 0; i < MapSize.Width; i++)
for (var j = 0; j < MapSize.Height; j++)
for (var i = 0; i < MapSize.X; i++)
for (var j = 0; j < MapSize.Y; j++)
Height[new MPos(i, j)] = s.ReadUInt8().Clamp((byte)0, Grid.MaximumTerrainHeight);
}
}
@@ -453,7 +451,7 @@ namespace OpenRA
Sequences = new SequenceSet(this, modData, Tileset, SequenceDefinitions);
var tl = new MPos(0, 0).ToCPos(this);
var br = new MPos(MapSize.Width - 1, MapSize.Height - 1).ToCPos(this);
var br = new MPos(MapSize.X - 1, MapSize.Y - 1).ToCPos(this);
AllCells = new CellRegion(Grid.Type, tl, br);
var btl = new PPos(Bounds.Left, Bounds.Top);
@@ -514,7 +512,7 @@ namespace OpenRA
foreach (var cell in AllCells)
{
var uv = cell.ToMPos(Grid.Type);
cellProjection[uv] = [];
cellProjection[uv] = Array.Empty<PPos>();
inverseCellProjection[uv] = new List<MPos>(1);
}
@@ -530,7 +528,7 @@ namespace OpenRA
if (Grid.MaximumTerrainHeight == 0)
{
uv = cell.ToMPos(Grid.Type);
cellProjection[cell] = [(PPos)uv];
cellProjection[cell] = new[] { (PPos)uv };
var inverse = inverseCellProjection[uv];
inverse.Clear();
inverse.Add(uv);
@@ -605,11 +603,11 @@ namespace OpenRA
// Any changes to this function should be reflected when setting projectionSafeBounds.
var height = mapHeight[uv];
if (height == 0)
return [(PPos)uv];
return new[] { (PPos)uv };
// 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>();
@@ -648,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;
@@ -682,24 +677,24 @@ namespace OpenRA
writer.Write(TileFormat);
// Size
writer.Write((ushort)MapSize.Width);
writer.Write((ushort)MapSize.Height);
writer.Write((ushort)MapSize.X);
writer.Write((ushort)MapSize.Y);
// Data offsets
const int TilesOffset = 17;
var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.Width * MapSize.Height + 17 : 0;
var resourcesOffset = (Grid.MaximumTerrainHeight > 0 ? 4 : 3) * MapSize.Width * MapSize.Height + 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.Width; i++)
for (var i = 0; i < MapSize.X; i++)
{
for (var j = 0; j < MapSize.Height; j++)
for (var j = 0; j < MapSize.Y; j++)
{
var tile = Tiles[new MPos(i, j)];
writer.Write(tile.Type);
@@ -710,16 +705,16 @@ namespace OpenRA
// Height data
if (heightsOffset != 0)
for (var i = 0; i < MapSize.Width; i++)
for (var j = 0; j < MapSize.Height; j++)
for (var i = 0; i < MapSize.X; i++)
for (var j = 0; j < MapSize.Y; j++)
writer.Write(Height[new MPos(i, j)]);
// Resource data
if (resourcesOffset != 0)
{
for (var i = 0; i < MapSize.Width; i++)
for (var i = 0; i < MapSize.X; i++)
{
for (var j = 0; j < MapSize.Height; j++)
for (var j = 0; j < MapSize.Y; j++)
{
var tile = Resources[new MPos(i, j)];
writer.Write(tile.Type);
@@ -756,7 +751,7 @@ namespace OpenRA
var positions = new List<(MPos Uv, Color Color)>();
foreach (var actor in actors)
{
var s = new ActorReference(actor.Value.Value, actor.Value);
var s = new ActorReference(actor.Value.Value, actor.Value.ToDictionary());
var ai = Rules.Actors[actor.Value.Value];
var impsis = ai.TraitInfos<IMapPreviewSignatureInfo>();
@@ -777,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
{
@@ -797,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;
@@ -819,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;
@@ -842,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;
@@ -856,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)
@@ -1042,7 +1024,7 @@ namespace OpenRA
return (PPos)CellContaining(projectedPos).ToMPos(Grid.Type);
}
static readonly PPos[] NoProjectedCells = [];
static readonly PPos[] NoProjectedCells = Array.Empty<PPos>();
public PPos[] ProjectedCellsCovering(MPos uv)
{
if (!initializedCellProjection)
@@ -1062,7 +1044,7 @@ namespace OpenRA
InitializeCellProjection();
if (!inverseCellProjection.Contains(uv))
return [];
return new List<MPos>();
return inverseCellProjection[uv];
}
@@ -1087,15 +1069,16 @@ namespace OpenRA
var oldMapResources = Resources;
var oldMapHeight = Height;
var oldMapRamp = Ramp;
var newSize = new Size(width, height);
MapSize = new Size(width, height);
Tiles = CellLayer.Resize(oldMapTiles, MapSize, oldMapTiles[MPos.Zero]);
Resources = CellLayer.Resize(oldMapResources, MapSize, oldMapResources[MPos.Zero]);
Height = CellLayer.Resize(oldMapHeight, MapSize, oldMapHeight[MPos.Zero]);
Ramp = CellLayer.Resize(oldMapRamp, MapSize, oldMapRamp[MPos.Zero]);
Tiles = CellLayer.Resize(oldMapTiles, newSize, oldMapTiles[MPos.Zero]);
Resources = CellLayer.Resize(oldMapResources, newSize, oldMapResources[MPos.Zero]);
Height = CellLayer.Resize(oldMapHeight, newSize, oldMapHeight[MPos.Zero]);
Ramp = CellLayer.Resize(oldMapRamp, newSize, oldMapRamp[MPos.Zero]);
MapSize = new int2(newSize);
var tl = new MPos(0, 0);
var br = new MPos(MapSize.Width - 1, MapSize.Height - 1);
var br = new MPos(MapSize.X - 1, MapSize.Y - 1);
AllCells = new CellRegion(Grid.Type, tl.ToCPos(this), br.ToCPos(this));
SetBounds(new PPos(tl.U + 1, tl.V + 1), new PPos(br.U - 1, br.V - 1));
}
@@ -1142,11 +1125,6 @@ namespace OpenRA
}
public byte GetTerrainIndex(CPos cell)
{
return GetTerrainIndex(cell.ToMPos(this));
}
public byte GetTerrainIndex(MPos uv)
{
// Lazily initialize a cache for terrain indexes.
if (cachedTerrainIndexes == null)
@@ -1155,6 +1133,7 @@ namespace OpenRA
cachedTerrainIndexes.Clear(InvalidCachedTerrainIndex);
}
var uv = cell.ToMPos(this);
var terrainIndex = cachedTerrainIndexes[uv];
// PERF: Cache terrain indexes per cell on demand.
@@ -1169,12 +1148,7 @@ namespace OpenRA
public TerrainTypeInfo GetTerrainInfo(CPos cell)
{
return GetTerrainInfo(cell.ToMPos(this));
}
public TerrainTypeInfo GetTerrainInfo(MPos uv)
{
return Rules.TerrainInfo.TerrainTypes[GetTerrainIndex(uv)];
return Rules.TerrainInfo.TerrainTypes[GetTerrainIndex(cell)];
}
public CPos Clamp(CPos cell)
@@ -1206,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
@@ -1275,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;
@@ -1375,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;
}
}
}
@@ -1433,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

@@ -20,7 +20,6 @@ using OpenRA.FileSystem;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Support;
using FS = OpenRA.FileSystem.FileSystem;
namespace OpenRA
{
@@ -28,27 +27,26 @@ namespace OpenRA
{
public static readonly MapPreview UnknownMap = new(null, null, MapGridType.Rectangular, null);
public IReadOnlyDictionary<IReadOnlyPackage, MapClassification> MapLocations => mapLocations;
readonly Dictionary<IReadOnlyPackage, MapClassification> mapLocations = [];
readonly Dictionary<IReadOnlyPackage, MapClassification> mapLocations = new();
public bool LoadPreviewImages = true;
readonly Manifest manifest;
readonly FS modFiles;
Cache<string, MapPreview> previews;
readonly Cache<string, MapPreview> previews;
readonly ModData modData;
readonly SheetBuilder sheetBuilder;
Thread previewLoaderThread;
bool previewLoaderThreadShutDown = true;
readonly object syncRoot = new();
readonly Queue<MapPreview> generateMinimap = [];
readonly Queue<MapPreview> generateMinimap = new();
public HashSet<string> StringPool { get; } = [];
public Dictionary<string, string> StringPool { get; } = new Dictionary<string, string>();
readonly List<MapDirectoryTracker> mapDirectoryTrackers = [];
readonly List<MapDirectoryTracker> mapDirectoryTrackers = new();
/// <summary>
/// The most recently modified or loaded map at runtime.
/// </summary>
public string LastModifiedMap { get; private set; } = null;
readonly Dictionary<string, string> mapUpdates = [];
readonly Dictionary<string, string> mapUpdates = new();
string lastLoadedLastModifiedMap;
@@ -68,11 +66,13 @@ namespace OpenRA
return null;
}
public MapCache(Manifest manifest, FS modFiles)
public MapCache(ModData modData)
{
this.manifest = manifest;
this.modFiles = modFiles;
sheetBuilder = new SheetBuilder(SheetType.BGRA, manifest.RendererConstants.MapPreviewSheetSize);
this.modData = modData;
var gridType = Exts.Lazy(() => modData.Manifest.Get<MapGrid>().Type);
previews = new Cache<string, MapPreview>(uid => new MapPreview(modData, uid, gridType.Value, this));
sheetBuilder = new SheetBuilder(SheetType.BGRA);
}
public void UpdateMaps()
@@ -81,24 +81,23 @@ namespace OpenRA
tracker.UpdateMaps(this);
}
public void LoadMaps(ModData modData)
public void LoadMaps()
{
// Utility mod that does not support maps
if (manifest.MapFolders.Count == 0)
if (!modData.Manifest.Contains<MapGrid>())
return;
var gridType = modData.GetOrCreate<MapGrid>().Type;
previews = new Cache<string, MapPreview>(uid => new MapPreview(modData, uid, gridType, this));
var mapGrid = modData.Manifest.Get<MapGrid>();
// Enumerate map directories
foreach (var kv in manifest.MapFolders)
foreach (var kv in modData.Manifest.MapFolders)
{
var name = kv.Key;
var classification = string.IsNullOrEmpty(kv.Value)
? MapClassification.Unknown : Enum.Parse<MapClassification>(kv.Value);
? 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,10 +106,10 @@ 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 = modFiles.OpenPackage(name);
package = modData.ModFiles.OpenPackage(name);
}
catch
{
@@ -121,38 +120,38 @@ namespace OpenRA
}
mapLocations.Add(package, classification);
mapDirectoryTrackers.Add(new MapDirectoryTracker(package, classification));
mapDirectoryTrackers.Add(new MapDirectoryTracker(mapGrid, package, classification));
}
// PERF: Load the mod YAML once outside the loop, and reuse it when resolving each maps custom YAML.
var modDataRules = modData.GetRulesYaml();
foreach (var kv in MapLocations)
{
foreach (var map in kv.Key.Contents)
LoadMapInternal(map, kv.Key, kv.Value, null, gridType, modDataRules);
LoadMapInternal(map, kv.Key, kv.Value, mapGrid, null, modDataRules);
}
// We only want to track maps in runtime, not at loadtime
LastModifiedMap = null;
}
public void LoadMap(string map, IReadOnlyPackage package, MapClassification classification, string oldMap)
public void LoadMap(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap)
{
LoadMapInternal(map, package, classification, oldMap);
LoadMapInternal(map, package, classification, mapGrid, oldMap, null);
}
void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, string oldMap,
MapGridType? gridType = null, MiniYamlNode[][] modDataRules = null)
void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap, IEnumerable<List<MiniYamlNode>> modDataRules)
{
IReadOnlyPackage mapPackage = null;
try
{
using (new PerfTimer(map))
{
mapPackage = package.OpenPackage(map, modFiles);
mapPackage = package.OpenPackage(map, modData.ModFiles);
if (mapPackage != null)
{
var uid = Map.ComputeUID(mapPackage);
previews[uid].UpdateFromMapWithoutOwningPackage(mapPackage, package, classification, gridType, modDataRules);
mapPackage.Dispose();
previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type, modDataRules);
if (oldMap != uid)
{
@@ -177,8 +176,12 @@ namespace OpenRA
public IEnumerable<IReadWritePackage> EnumerateMapDirPackages(MapClassification classification = MapClassification.System)
{
// Utility mod that does not support maps
if (!modData.Manifest.Contains<MapGrid>())
yield break;
// Enumerate map directories
foreach (var kv in manifest.MapFolders)
foreach (var kv in modData.Manifest.MapFolders)
{
if (!Enum.TryParse(kv.Value, out MapClassification packageClassification))
continue;
@@ -187,16 +190,16 @@ 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)modFiles.OpenPackage(name))
using (var package = (IReadWritePackage)modData.ModFiles.OpenPackage(name))
yield return package;
}
}
@@ -216,12 +219,11 @@ namespace OpenRA
foreach (var mapDirPackage in mapDirPackages)
foreach (var map in mapDirPackage.Contents)
if (mapDirPackage.OpenPackage(map, modFiles) is IReadWritePackage mapPackage)
if (mapDirPackage.OpenPackage(map, modData.ModFiles) is IReadWritePackage mapPackage)
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)
@@ -231,37 +233,44 @@ namespace OpenRA
.ToList();
foreach (var uid in queryUids)
previews[uid].BeginRemoteSearch();
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null);
Task.Run(async () =>
{
var client = HttpClientFactory.Create();
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
// Limit each query to 50 maps at a time to avoid request size limits
foreach (var batchUids in queryUids.Chunk(50))
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 result = await client.GetStreamAsync(url);
foreach (var kv in MiniYaml.FromStream(result, url, stringPool: stringPool))
previews[kv.Key].CompleteRemoteSearch(kv.Value, mapDetailsReceived);
}
catch (Exception e)
{
Log.Write("debug", "Remote map query failed with error:");
Log.Write("debug", e);
Log.Write("debug", $"URL was: {url}");
}
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)
{
var p = previews[uid];
if (p.Status == MapStatus.Searching)
p.CompleteRemoteSearch(null, mapQueryFailed);
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}");
foreach (var uid in batchUids)
{
var p = previews[uid];
p.UpdateRemoteSearch(MapStatus.Unavailable, null);
mapQueryFailed?.Invoke(p);
}
}
}
@@ -273,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)
{
@@ -297,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)
@@ -328,9 +337,7 @@ namespace OpenRA
}
// Release the buffer by forcing changes to be written out to the texture, allowing the buffer to be reclaimed by GC.
if (sheetBuilder.Current != null)
Game.RunAfterTick(sheetBuilder.Current.ReleaseBuffer);
Game.RunAfterTick(sheetBuilder.Current.ReleaseBuffer);
Log.Write("debug", "MapCache.LoadAsyncInternal ended");
}
@@ -341,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;
}
@@ -399,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

@@ -20,24 +20,26 @@ namespace OpenRA
public sealed class MapDirectoryTracker : IDisposable
{
readonly FileSystemWatcher watcher;
readonly MapGrid mapGrid;
readonly IReadOnlyPackage package;
readonly MapClassification classification;
enum MapAction { Add, Delete, Update }
readonly Dictionary<string, MapAction> mapActionQueue = [];
readonly Dictionary<string, MapAction> mapActionQueue = new();
bool dirty = false;
public MapDirectoryTracker(IReadOnlyPackage package, MapClassification classification)
public MapDirectoryTracker(MapGrid mapGrid, IReadOnlyPackage package, MapClassification classification)
{
this.mapGrid = mapGrid;
this.package = package;
this.classification = classification;
watcher = new FileSystemWatcher(package.Name);
watcher.Changed += (_, e) => AddMapAction(MapAction.Update, e.FullPath);
watcher.Created += (_, e) => AddMapAction(MapAction.Add, e.FullPath);
watcher.Deleted += (_, e) => AddMapAction(MapAction.Delete, e.FullPath);
watcher.Renamed += (_, e) => AddMapAction(MapAction.Add, e.FullPath, e.OldFullPath);
watcher.Changed += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Update, e.FullPath);
watcher.Created += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Add, e.FullPath);
watcher.Deleted += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Delete, e.FullPath);
watcher.Renamed += (object sender, RenamedEventArgs e) => AddMapAction(MapAction.Add, e.FullPath, e.OldFullPath);
watcher.IncludeSubdirectories = true;
watcher.EnableRaisingEvents = true;
@@ -84,7 +86,7 @@ namespace OpenRA
dirty = false;
foreach (var mapAction in mapActionQueue)
{
var map = mapcache.FirstOrDefault(x => x.Path == 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)
@@ -96,7 +98,7 @@ namespace OpenRA
{
Console.WriteLine(mapAction.Key + " was updated");
map.Invalidate();
mapcache.LoadMap(mapAction.Key.Replace(package.Name + Path.DirectorySeparatorChar, ""), package, classification, map.Uid);
mapcache.LoadMap(mapAction.Key.Replace(package.Name + Path.DirectorySeparatorChar, ""), package, classification, mapGrid, map.Uid);
}
}
else
@@ -104,7 +106,7 @@ namespace OpenRA
if (mapAction.Value != MapAction.Delete)
{
Console.WriteLine(mapAction.Key + " was added");
mapcache.LoadMap(mapAction.Key.Replace(package?.Name + Path.DirectorySeparatorChar, ""), package, classification, null);
mapcache.LoadMap(mapAction.Key.Replace(package?.Name + Path.DirectorySeparatorChar, ""), package, classification, mapGrid, null);
}
}
}

View File

@@ -1,58 +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.Generic;
using OpenRA.Primitives;
namespace OpenRA
{
public class MapGenerationArgs
{
[FieldLoader.Require]
public string Uid = null;
[FieldLoader.Require]
public string Generator = null;
[FieldLoader.Require]
public string Tileset = null;
[FieldLoader.Require]
public Size Size = default;
[FieldLoader.Require]
public string Title = null;
[FieldLoader.Require]
public string Author = null;
[FieldLoader.LoadUsing(nameof(LoadSettings))]
public MiniYaml Settings = null;
static MiniYaml LoadSettings(MiniYaml yaml)
{
return yaml.NodeWithKey("Settings").Value;
}
public List<MiniYamlNode> Serialize()
{
return
[
new("Uid", Uid),
new("Generator", Generator),
new("Tileset", Tileset),
new("Size", FieldSaver.FormatValue(Size)),
new("Settings", Settings),
new("Title", Title),
new("Author", Author)
];
}
}
}

View File

@@ -10,9 +10,9 @@
#endregion
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA
@@ -25,8 +25,8 @@ namespace OpenRA
public readonly struct CellRamp
{
public readonly int CenterHeightOffset;
public readonly ImmutableArray<WVec> Corners;
public readonly ImmutableArray<ImmutableArray<WVec>> Polygons;
public readonly WVec[] Corners;
public readonly WVec[][] Polygons;
public readonly WRot Orientation;
public CellRamp(MapGridType type, WRot orientation,
@@ -37,43 +37,43 @@ namespace OpenRA
Orientation = orientation;
if (type == MapGridType.RectangularIsometric)
{
Corners =
[
Corners = new[]
{
new WVec(0, -724, 724 * (int)tl),
new WVec(724, 0, 724 * (int)tr),
new WVec(0, 724, 724 * (int)br),
new WVec(-724, 0, 724 * (int)bl),
];
};
}
else
{
Corners =
[
Corners = new[]
{
new WVec(-512, -512, 512 * (int)tl),
new WVec(512, -512, 512 * (int)tr),
new WVec(512, 512, 512 * (int)br),
new WVec(-512, 512, 512 * (int)bl)
];
};
}
if (split == RampSplit.X)
{
Polygons =
[
[Corners[0], Corners[1], Corners[3]],
[Corners[1], Corners[2], Corners[3]]
];
Polygons = new[]
{
new[] { Corners[0], Corners[1], Corners[3] },
new[] { Corners[1], Corners[2], Corners[3] }
};
}
else if (split == RampSplit.Y)
{
Polygons =
[
[Corners[0], Corners[1], Corners[2]],
[Corners[0], Corners[2], Corners[3]]
];
Polygons = new[]
{
new[] { Corners[0], Corners[1], Corners[2] },
new[] { Corners[0], Corners[2], Corners[3] }
};
}
else
Polygons = [Corners];
Polygons = new[] { Corners };
// Initial value must be assigned before HeightOffset can be called
CenterHeightOffset = 0;
@@ -84,11 +84,10 @@ namespace OpenRA
{
// Enumerate over the polygons, assuming that they are triangles
// If the ramp is not split we will take the first three vertices of the corners as a valid triangle
int u;
int v;
ImmutableArray<WVec> p;
var i = 0;
do
WVec[] p = null;
var u = 0;
var v = 0;
for (var i = 0; i < Polygons.Length; i++)
{
p = Polygons[i];
u = ((p[1].Y - p[2].Y) * (dX - p[2].X) - (p[1].X - p[2].X) * (dY - p[2].Y)) / 1024;
@@ -97,10 +96,7 @@ namespace OpenRA
// Point is within the triangle if 0 <= u,v <= 1024
if (u >= 0 && u <= 1024 && v >= 0 && v <= 1024)
break;
i++;
}
while (i < Polygons.Length);
// Calculate w from u,v and interpolate height
return (u * p[0].Z + v * p[1].Z + (1024 - u - v) * p[2].Z) / 1024;
@@ -110,6 +106,7 @@ namespace OpenRA
public class MapGrid : IGlobalModData
{
public readonly MapGridType Type = MapGridType.Rectangular;
public readonly Size TileSize = new(24, 24);
public readonly byte MaximumTerrainHeight = 0;
public readonly SubCell DefaultSubCell = (SubCell)byte.MaxValue;
@@ -117,19 +114,19 @@ namespace OpenRA
public readonly bool EnableDepthBuffer = false;
public readonly ImmutableArray<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
];
public readonly WVec[] SubCellOffsets =
{
new WVec(0, 0, 0), // full cell - index 0
new WVec(-299, -256, 0), // top left - index 1
new WVec(256, -256, 0), // top right - index 2
new WVec(0, 0, 0), // center - index 3
new WVec(-299, 256, 0), // bottom left - index 4
new WVec(256, 256, 0), // bottom right - index 5
};
public ImmutableArray<CellRamp> Ramps { get; }
public CellRamp[] Ramps { get; }
internal readonly ImmutableArray<ImmutableArray<CVec>> TilesByDistance;
internal readonly CVec[][] TilesByDistance;
public int TileScale { get; }
@@ -162,9 +159,8 @@ namespace OpenRA
var halfBackward = -halfForward;
// Slope types are hardcoded following the convention from the TS and RA2 map format
Ramps =
[
Ramps = new[]
{
// Flat
new CellRamp(Type, WRot.None),
@@ -197,16 +193,16 @@ namespace OpenRA
new CellRamp(Type, WRot.None, tl: RampCornerHeight.Half, br: RampCornerHeight.Half, split: RampSplit.Y),
new CellRamp(Type, WRot.None, tr: RampCornerHeight.Half, bl: RampCornerHeight.Half, split: RampSplit.X),
new CellRamp(Type, WRot.None, tl: RampCornerHeight.Half, br: RampCornerHeight.Half, split: RampSplit.X),
];
};
TilesByDistance = CreateTilesByDistance();
}
ImmutableArray<ImmutableArray<CVec>> CreateTilesByDistance()
CVec[][] CreateTilesByDistance()
{
var ts = new List<CVec>[MaximumTileSearchRange + 1];
for (var i = 0; i < MaximumTileSearchRange + 1; i++)
ts[i] = [];
ts[i] = new List<CVec>();
for (var j = -MaximumTileSearchRange; j <= MaximumTileSearchRange; j++)
for (var i = -MaximumTileSearchRange; i <= MaximumTileSearchRange; i++)
@@ -238,7 +234,7 @@ namespace OpenRA
});
}
return ts.Select(list => list.ToImmutableArray()).ToImmutableArray();
return ts.Select(list => list.ToArray()).ToArray();
}
public WVec OffsetOfSubCell(SubCell subCell)

View File

@@ -10,7 +10,6 @@
#endregion
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.Traits;
@@ -25,7 +24,7 @@ namespace OpenRA
public readonly Dictionary<string, PlayerReference> Players;
public MapPlayers()
: this([]) { }
: this(new List<MiniYamlNode>()) { }
public MapPlayers(IEnumerable<MiniYamlNode> playerDefinitions)
{
@@ -55,7 +54,7 @@ namespace OpenRA
Name = "Creeps",
Faction = firstFaction,
NonCombatant = true,
Enemies = Exts.MakeArray(playerCount, i => $"Multi{i}").ToImmutableArray()
Enemies = Exts.MakeArray(playerCount, i => $"Multi{i}")
}
}
};
@@ -67,7 +66,7 @@ namespace OpenRA
Name = $"Multi{index}",
Faction = "Random",
Playable = true,
Enemies = ["Creeps"]
Enemies = new[] { "Creeps" }
};
Players.Add(p.Name, p);
}

View File

@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
@@ -24,11 +23,10 @@ using OpenRA.FileSystem;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Support;
using OpenRA.Traits;
namespace OpenRA
{
public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError, Generatable, Generating }
public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError }
// Used for grouping maps in the UI
[Flags]
@@ -37,8 +35,7 @@ namespace OpenRA
Unknown = 0,
System = 1,
User = 2,
Remote = 4,
Generated = 8
Remote = 4
}
[SuppressMessage("StyleCop.CSharp.NamingRules",
@@ -51,10 +48,10 @@ namespace OpenRA
{
public readonly string title;
public readonly string author;
public readonly ImmutableArray<string> categories;
public readonly string[] categories;
public readonly int players;
public readonly Rectangle bounds;
public readonly ImmutableArray<short> spawnpoints = [];
public readonly short[] spawnpoints = Array.Empty<short>();
public readonly MapGridType map_grid_type;
public readonly string minimap;
public readonly bool downloading;
@@ -62,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
@@ -72,12 +68,12 @@ namespace OpenRA
{
public int MapFormat;
public string Title;
public ImmutableArray<string> Categories;
public string[] Categories;
public string Author;
public string TileSet;
public MapPlayers Players;
public int PlayerCount;
public ImmutableArray<CPos> SpawnPoints;
public CPos[] SpawnPoints;
public MapGridType GridType;
public Rectangle Bounds;
public Png Preview;
@@ -85,7 +81,6 @@ namespace OpenRA
public MapClassification Class;
public MapVisibility Visibility;
public DateTime ModifiedDate;
public MapGenerationArgs GenerationArgs;
public MiniYaml RuleDefinitions;
public MiniYaml WeaponDefinitions;
@@ -94,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; }
@@ -117,7 +111,7 @@ namespace OpenRA
return key == "world" || key == "player";
}
public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary<string, MiniYaml> yaml, MiniYamlNode[][] modDataRules)
public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary<string, MiniYaml> yaml, IEnumerable<List<MiniYamlNode>> modDataRules)
{
RuleDefinitions = LoadRuleSection(yaml, "Rules");
WeaponDefinitions = LoadRuleSection(yaml, "Weapons");
@@ -126,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 = ImmutableArray<string>.Empty;
if (FluentMessageDefinitions.Value != null)
files = FieldLoader.GetValue<ImmutableArray<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,26 +135,19 @@ namespace OpenRA
var files = Enumerable.Empty<string>();
if (RuleDefinitions.Value != null)
{
var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", RuleDefinitions.Value);
files = files.Concat(mapFiles);
var mapFiles = FieldLoader.GetValue<string[]>("value", RuleDefinitions.Value);
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;
}
}
@@ -199,40 +167,24 @@ namespace OpenRA
}
}
readonly object syncRoot = new();
static readonly CPos[] NoSpawns = Array.Empty<CPos>();
readonly MapCache cache;
readonly ModData modData;
IReadOnlyPackage package;
public readonly string Uid;
public string Path { get; private set; }
void LoadPackage()
{
if (package == null && parentPackage != null)
package = parentPackage.OpenPackage(Path, modData.ModFiles);
}
public Map ToMap()
{
LoadPackage();
using (new PerfTimer("Map"))
return new Map(modData, package);
}
public IReadOnlyPackage Package { get; private set; }
IReadOnlyPackage parentPackage;
volatile InnerData innerData;
public int MapFormat => innerData.MapFormat;
public string Title => innerData.Title;
public ImmutableArray<string> Categories => innerData.Categories;
public string[] Categories => innerData.Categories;
public string Author => innerData.Author;
public string TileSet => innerData.TileSet;
public MapPlayers Players => innerData.Players;
public int PlayerCount => innerData.PlayerCount;
public ImmutableArray<CPos> SpawnPoints => innerData.SpawnPoints;
public CPos[] SpawnPoints => innerData.SpawnPoints;
public MapGridType GridType => innerData.GridType;
public Rectangle Bounds => innerData.Bounds;
public Png Preview => innerData.Preview;
@@ -248,37 +200,20 @@ namespace OpenRA
public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo;
public DateTime ModifiedDate => innerData.ModifiedDate;
public MapGenerationArgs GenerationArgs => innerData.GenerationArgs;
public long DownloadBytes { get; private set; }
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;
@@ -327,12 +262,12 @@ namespace OpenRA
{
MapFormat = 0,
Title = "Unknown Map",
Categories = ["Unknown"],
Categories = new[] { "Unknown" },
Author = "Unknown Author",
TileSet = "unknown",
Players = null,
PlayerCount = 0,
SpawnPoints = [],
SpawnPoints = NoSpawns,
GridType = gridType,
Bounds = Rectangle.Empty,
Preview = null,
@@ -342,40 +277,71 @@ namespace OpenRA
};
}
/// <summary>
/// Updates internal state from a map without taking ownership of its package.
/// A new copy of the map package will be opened lazily when needed.
/// </summary>
public void UpdateFromMapWithoutOwningPackage(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification,
MapGridType? gridType = null, MiniYamlNode[][] modDataRules = null)
// For linting purposes only!
public MapPreview(Map map, ModData modData)
{
UpdateFromMap(p, classification, gridType, modDataRules);
parentPackage = parent;
package = null;
this.modData = modData;
cache = modData.MapCache;
Uid = map.Uid;
Package = map.Package;
var mapPlayers = new MapPlayers(map.PlayerDefinitions);
var spawns = new List<CPos>();
foreach (var kv in map.ActorDefinitions.Where(d => d.Value.Value == "mpspawn"))
{
var s = new ActorReference(kv.Value.Value, kv.Value.ToDictionary());
spawns.Add(s.Get<LocationInit>().Value);
}
innerData = new InnerData
{
MapFormat = map.MapFormat,
Title = map.Title,
Categories = map.Categories,
Author = map.Author,
TileSet = map.Tileset,
Players = mapPlayers,
PlayerCount = mapPlayers.Players.Count(x => x.Value.Playable),
SpawnPoints = spawns.ToArray(),
GridType = map.Grid.Type,
Bounds = map.Bounds,
Preview = null,
Status = MapStatus.Available,
Class = MapClassification.Unknown,
Visibility = map.Visibility,
};
innerData.SetCustomRules(modData, this, new Dictionary<string, MiniYaml>()
{
{ "Rules", map.RuleDefinitions },
{ "Translations", map.TranslationDefinitions },
{ "Weapons", map.WeaponDefinitions },
{ "Voices", map.VoiceDefinitions },
{ "Music", map.MusicDefinitions },
{ "Notifications", map.NotificationDefinitions },
{ "Sequences", map.SequenceDefinitions },
{ "ModelSequences", map.ModelSequenceDefinitions }
}, null);
}
/// <summary>
/// Updates internal state from a map and takes ownership of its package.
/// The package remains in memory and must not be disposed.
/// </summary>
public void UpdateFromMap(IReadOnlyPackage p, MapClassification classification,
MapGridType? gridType = null, MiniYamlNode[][] modDataRules = null)
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType, IEnumerable<List<MiniYamlNode>> modDataRules)
{
Path = p.Name;
package = p;
Dictionary<string, MiniYaml> yaml;
using (var yamlStream = p.GetStream("map.yaml"))
{
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();
}
Package = p;
parentPackage = parent;
var newData = innerData.Clone();
newData.GridType = gridType;
newData.Class = classification;
newData.GridType = gridType ?? modData.GetOrCreate<MapGrid>().Type;
if (yaml.TryGetValue("MapFormat", out var temp))
{
@@ -388,7 +354,7 @@ namespace OpenRA
newData.Title = temp.Value;
if (yaml.TryGetValue("Categories", out temp))
newData.Categories = FieldLoader.GetValue<ImmutableArray<string>>("Categories", temp.Value);
newData.Categories = FieldLoader.GetValue<string[]>("Categories", temp.Value);
if (yaml.TryGetValue("Tileset", out temp))
newData.TileSet = temp.Value;
@@ -409,7 +375,7 @@ namespace OpenRA
if (yaml.TryGetValue("MapFormat", out temp))
newData.MapFormat = FieldLoader.GetValue<int>("MapFormat", temp.Value);
newData.Status = modData.Manifest.MapCompatibility.Contains(requiresMod) ?
newData.Status = mapCompatibility == null || mapCompatibility.Contains(requiresMod) ?
MapStatus.Available : MapStatus.Unavailable;
try
@@ -420,18 +386,18 @@ namespace OpenRA
var spawns = new List<CPos>();
foreach (var kv in actorDefinitions.Nodes.Where(d => d.Value.Value == "mpspawn"))
{
var s = new ActorReference(kv.Value.Value, kv.Value);
var s = new ActorReference(kv.Value.Value, kv.Value.ToDictionary());
spawns.Add(s.Get<LocationInit>().Value);
}
newData.SpawnPoints = spawns.ToImmutableArray();
newData.SpawnPoints = spawns.ToArray();
}
else
newData.SpawnPoints = [];
newData.SpawnPoints = Array.Empty<CPos>();
}
catch (Exception)
{
newData.SpawnPoints = [];
newData.SpawnPoints = Array.Empty<CPos>();
newData.Status = MapStatus.Unavailable;
}
@@ -455,122 +421,31 @@ namespace OpenRA
using (var dataStream = p.GetStream("map.png"))
newData.Preview = new Png(dataStream);
newData.ModifiedDate = p.Name != null ? File.GetLastWriteTime(p.Name) : DateTime.Now;
newData.ModifiedDate = File.GetLastWriteTime(p.Name);
// Assign the new data atomically
// Local maps have higher precedence than remote/generated maps,
// so should always replace their metadata
lock (syncRoot)
innerData = newData;
innerData = newData;
}
public void UpdateFromGenerationArgs(MapGenerationArgs args)
{
var newData = innerData.Clone();
newData.Class = MapClassification.Generated;
newData.Status = MapStatus.Generatable;
newData.Title = args.Title;
newData.Author = args.Author;
newData.TileSet = args.Tileset;
newData.GenerationArgs = args;
newData.MapFormat = Map.CurrentMapFormat;
try
{
var generator = modData.DefaultRules.Actors[SystemActors.EditorWorld]
.TraitInfos<IMapGeneratorInfo>()
.FirstOrDefault(info => info.Type == args.Generator);
if (generator == null)
throw new Exception($"Unknown map generator type {args.Generator}");
if (!generator.TryGenerateMetadata(modData, args, out var players, out var ruleDefinitions))
throw new Exception("Failed to generate map metadata");
newData.Players = players;
newData.PlayerCount = newData.Players.Players.Count(x => x.Value.Playable);
newData.SetCustomRules(modData, this, ruleDefinitions, null);
// Placeholder to satisfy server-side lint checks
newData.SpawnPoints = Exts.MakeArray(newData.PlayerCount, i => new CPos(i, i)).ToImmutableArray();
}
catch (Exception e)
{
Log.Write("debug", "Map generation failed with error:");
Log.Write("debug", e);
newData.Status = MapStatus.Unavailable;
}
lock (syncRoot)
innerData = newData;
}
public void Generate()
{
if (Class != MapClassification.Generated || Status != MapStatus.Generatable)
return;
lock (syncRoot)
innerData.Status = MapStatus.Generating;
Task.Run(() =>
{
try
{
var generator = modData.DefaultRules.Actors[SystemActors.EditorWorld]
.TraitInfos<IMapGeneratorInfo>()
.FirstOrDefault(info => info.Type == GenerationArgs.Generator);
if (generator == null)
throw new Exception($"Unknown map generator type {GenerationArgs.Generator}");
var map = generator.Generate(modData, GenerationArgs);
// Uid is generated when the map is saved
map.Save(new ZipFileLoader.ReadWriteZipFile());
if (map.Uid != GenerationArgs.Uid)
throw new InvalidOperationException("Map generation UID mismatch");
Game.RunAfterTick(() => UpdateFromMap(map.Package, MapClassification.Generated));
}
catch (Exception e)
{
Log.Write("debug", "Map generation failed with error:");
Log.Write("debug", e);
lock (syncRoot)
innerData.Status = MapStatus.Unavailable;
}
});
}
public void BeginRemoteSearch()
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action<MapPreview> parseMetadata = null)
{
var newData = innerData.Clone();
newData.Status = status;
newData.Class = MapClassification.Remote;
newData.Status = MapStatus.Searching;
// We may have been resolved to a local/generated map by another
// async task. Make sure we don't stomp over their state!
lock (syncRoot)
if (innerData.Class == MapClassification.Unknown || innerData.Class == MapClassification.Remote)
innerData = newData;
}
public void CompleteRemoteSearch(MiniYaml yaml, Action<MapPreview> parseMetadata = null)
{
var newData = innerData.Clone();
newData.Class = MapClassification.Remote;
newData.Status = MapStatus.Unavailable;
if (yaml != null)
if (status == MapStatus.DownloadAvailable)
{
try
{
var r = FieldLoader.Load<RemoteMapData>(yaml);
newData.Status = r.downloading ? MapStatus.DownloadAvailable : MapStatus.Unavailable;
// Map download has been disabled server side
if (!r.downloading)
{
newData.Status = MapStatus.Unavailable;
return;
}
newData.Title = r.title;
newData.Categories = r.categories;
newData.Author = r.author;
@@ -582,7 +457,7 @@ namespace OpenRA
var spawns = new CPos[r.spawnpoints.Length / 2];
for (var j = 0; j < r.spawnpoints.Length; j += 2)
spawns[j / 2] = new CPos(r.spawnpoints[j], r.spawnpoints[j + 1]);
newData.SpawnPoints = spawns.ToImmutableArray();
newData.SpawnPoints = spawns;
newData.GridType = r.map_grid_type;
if (cache.LoadPreviewImages)
{
@@ -599,48 +474,32 @@ 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 (!modData.Manifest.MapCompatibility.Contains(r.game_mod))
newData.Status = MapStatus.Unavailable;
}
catch (Exception e)
{
Log.Write("debug", "Failed parsing mapserver response:");
Log.Write("debug", e);
newData.Status = MapStatus.Unavailable;
}
}
// We may have been resolved to a local/generated map by another
// async task. Make sure we don't stomp over their state!
MapClassification mapClassification;
lock (syncRoot)
{
mapClassification = innerData.Class;
if (mapClassification == MapClassification.Remote)
innerData = newData;
}
// Commit updated data before running the callbacks
innerData = newData;
if (mapClassification == MapClassification.Remote)
{
if (innerData.Preview != null)
cache.CacheMinimap(this);
parseMetadata?.Invoke(this);
}
// Update the status and class unconditionally
innerData = newData;
}
public void Install(string mapRepositoryUrl)
public void Install(string mapRepositoryUrl, Action onSuccess)
{
if ((Status != MapStatus.DownloadError && Status != MapStatus.DownloadAvailable) || !Game.Settings.Game.AllowDownloading)
return;
@@ -693,11 +552,14 @@ namespace OpenRA
mapInstallPackage.Update(mapFilename, fileStream.ToArray());
Log.Write("debug", $"Downloaded map to '{mapFilename}'");
var p = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles);
if (p == null)
var package = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles);
if (package == null)
innerData.Status = MapStatus.DownloadError;
else
UpdateFromMapWithoutOwningPackage(p, mapInstallPackage, MapClassification.User, GridType);
{
UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType, null);
Game.RunAfterTick(onSuccess);
}
}
catch (Exception e)
{
@@ -710,31 +572,29 @@ namespace OpenRA
public void Invalidate()
{
lock (syncRoot)
{
innerData.Class = MapClassification.Unknown;
innerData.Status = MapStatus.Unavailable;
}
innerData.Status = MapStatus.Unavailable;
}
public void Dispose()
{
package?.Dispose();
package = null;
if (Package != null)
{
Package.Dispose();
Package = null;
}
}
public void Delete()
{
Invalidate();
(parentPackage as IReadWritePackage)?.Delete(Path);
(parentPackage as IReadWritePackage)?.Delete(Package.Name);
}
Stream IReadOnlyFileSystem.Open(string filename)
{
// Explicit package paths never refer to a map
LoadPackage();
if (!filename.Contains('|') && package.Contains(filename))
return package.GetStream(filename);
if (!filename.Contains('|') && Package.Contains(filename))
return Package.GetStream(filename);
return modData.DefaultFileSystem.Open(filename);
}
@@ -750,8 +610,7 @@ namespace OpenRA
// Explicit package paths never refer to a map
if (!filename.Contains('|'))
{
LoadPackage();
s = package.GetStream(filename);
s = Package.GetStream(filename);
if (s != null)
return true;
}
@@ -762,18 +621,17 @@ namespace OpenRA
bool IReadOnlyFileSystem.Exists(string filename)
{
// Explicit package paths never refer to a map
LoadPackage();
if (!filename.Contains('|') && package.Contains(filename))
if (!filename.Contains('|') && Package.Contains(filename))
return true;
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,7 @@
*/
#endregion
using System.Collections.Immutable;
using System;
using OpenRA.Primitives;
namespace OpenRA
@@ -31,7 +31,7 @@ namespace OpenRA
public string Faction;
public bool LockColor = false;
public Color Color = Game.ModData.GetOrCreate<DefaultPlayer>().Color;
public Color Color = Game.ModData.Manifest.Get<DefaultPlayer>().Color;
/// <summary>
/// Sets the "Home" location, which can be used by traits and scripts to e.g. set the initial camera
@@ -54,8 +54,8 @@ namespace OpenRA
public bool LockHandicap = false;
public int Handicap = 0;
public ImmutableArray<string> Allies = [];
public ImmutableArray<string> Enemies = [];
public string[] Allies = Array.Empty<string>();
public string[] Enemies = Array.Empty<string>();
public PlayerReference() { }
public PlayerReference(MiniYaml my) { FieldLoader.Load(this, my); }

View File

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

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