72 Commits

Author SHA1 Message Date
hacker
50277c59d7 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
2025-11-02 03:08:22 +00:00
hacker
2a13752203 Added Dockerfile 2025-11-02 03:05:27 +00:00
hacker
8ce3ea5f2c Log chat messages on the server 2025-11-02 03:05:19 +00:00
Gustas
b4f3d8ae02 Increase perf for parsing remote maps 2025-03-30 18:21:18 +01:00
Gustas
d70c30763b Add a backup for unloaded bots 2025-03-30 18:17:26 +01:00
Gustas
27297a1ba9 Make lobby names dynamic, incase players don't have maps 2025-03-30 18:17:19 +01:00
Gustas
2890ccf2b1 Fix game browser tooltips being untranslated 2025-03-30 18:17:14 +01:00
Gustas
f9f1889112 Fix custom bot names being untranslated in lobby 2025-03-30 10:21:09 +01:00
Gustas
d9c5821329 Fix resource area calculation 2025-03-29 16:54:56 +02:00
Gustas
7fc415de19 Fix rallypoints not showing full target lines 2025-03-29 16:42:13 +02:00
Gustas
ec01f63317 Add missing dispose calls 2025-03-29 13:58:24 +00:00
Gustas
c9ac702217 Fix camera being in an incorrect position of first frame 2025-03-28 21:18:38 +00:00
Gustas
1913c608a6 Fix telemetry data description 2025-03-28 21:13:40 +00:00
Gustas
94ac6cc400 Fix alignment regressions 2025-03-28 21:13:32 +00:00
JovialFeline
97d9520bf0 Get Fluent bot names in server list 2025-03-27 23:05:59 +02:00
N.N
e7300cc430 Fix wrong Spread in Warheads
(cherry picked from commit e78ab81b26)
2025-03-23 16:11:40 +02:00
Paul Chote
677d7e47a4 Work around Raspberry Pi GLSL bug.
(cherry picked from commit e8391de36a)
2025-03-23 15:46:01 +02:00
Matthias Mailänder
5f287d38ab Avoid a thirdparty action. 2025-03-18 14:58:23 +02:00
N.N
3a00d5a679 Fix crush anim 2025-03-18 14:27:00 +02:00
Matthias Mailänder
d5d43ee97c Avoid thirdparty action. 2025-03-18 14:18:39 +02:00
Paul Chote
fd12ef1535 Fix FlashPostProcessEffect channel order. 2025-03-18 14:11:29 +02:00
RoosterDragon
de8317d9b8 Harvesters prefer ore near to the refinery.
When searching for ore, harvesters will prefer ore close to their refinery, the dockPos. However this only works when this is set, and is was not being set when expected. Fixing this prevents harvesters moving in straight lines, particularly when the ore is to the left of the refinery, which is the default path search direction.

Fixes a regression from d0974cfdd2.
2025-03-16 10:39:47 +00:00
Gustas
b953f4a40d Fix a potential crash when cancelling capture 2025-03-16 10:29:48 +00:00
Gustas
b565f0903a Fix tree husks on jungle tileset 2025-03-16 10:26:29 +00:00
N.N
da71a8271d Fix frame on 1141 tile 2025-03-12 16:48:39 +02:00
Gustas
54558169e1 Additional punctuation fixup 2025-03-03 21:02:01 +00:00
test1232156
bcb953309a Remove unnecessary punctuation from descriptions 2025-03-02 18:58:41 +02:00
Paul Chote
a371826613 Fix .NET 6 CI false positives. 2025-02-28 21:00:02 +01:00
JovialFeline
82ee3d779c Fix soviet-06 harvester production crash 2025-02-26 23:40:44 +02:00
Paul Chote
4ea0e70589 Sanitize saved skirmish factions. 2025-02-22 15:32:06 +02:00
Gustas
3f4e4db836 vs seems more standard 2025-02-19 21:01:44 +00:00
Gustas
85020bc2af Adjust to a 4 space standard 2025-02-19 20:55:30 +00:00
Gustas
70bb01a2c7 Tooltips should not have punctuation 2025-02-16 14:12:44 +02:00
Gustas
38d4719073 Re-add a mini lobby options menu to missionbrowser 2025-02-16 12:41:29 +01:00
RoosterDragon
aebb97967d Fix a bad comparison against Target.Invalid.
Target.Invalid acts like a NaN, and will not compare equal with itself. Compare against the TargetType instead, which performs the intended comparison.
2025-02-15 13:22:35 +00:00
Gustas
97f81d0aa4 Update SDL to 2.32.0 2025-02-15 13:19:59 +00:00
JovialFeline
2030c17a8b Add Lua documentation details 2025-02-08 14:02:27 +02:00
Gustas
b6dcd8d9af Fix bot harvesters crashing 2025-02-01 20:36:33 +00:00
Gustas
9a78f22e86 Match the check inside .CenterPosition 2025-02-01 20:33:23 +00:00
Gustas
19ca3d610b Allow tileset to be entered in lower case 2025-02-01 20:23:04 +00:00
Gustas
dd74d3572a Fix utility commands losing data 2025-02-01 20:23:00 +00:00
JovialFeline
ad7e5d62cd Fix unrepairable desert bridge 2025-01-30 14:10:15 +02:00
JovialFeline
ece7547695 D2k shellmap: Hide pickup indicators 2025-01-28 19:04:14 +02:00
JovialFeline
944bc7667a Fix some campaign Fluent keys 2025-01-28 18:57:43 +02:00
JovialFeline
0ac28e1f5f Restore StartGameNotification in missions 2025-01-28 16:23:29 +02:00
JovialFeline
ca3a57289e Hide owner row for D2k special mission Outposts 2025-01-28 16:11:40 +02:00
JovialFeline
a4641be7e7 Fix ordos-04 to use internal bot names 2025-01-16 13:09:38 +02:00
Paul Chote
0651308210 Only expose Facing for actors that define a facing. 2025-01-15 18:07:31 +02:00
Paul Chote
cac6aebb6c Only expose Location and CenterPosition for actors that occupy space. 2025-01-15 18:07:31 +02:00
JovialFeline
0baddf46e2 Correct nod-06c capture objective 2025-01-15 17:39:49 +02:00
Gustas
e0d893b736 Raname GrantConditionOnDeployWithCharge to GrantChargedConditionOnToggle
Also allow deploying without cancelling current activity, and make saboteurs use it

(cherry picked from commit 8b4478260e)
2024-12-27 16:38:07 +02:00
Gustas
2ccfa425b2 Cancel movement in invalid states
(cherry picked from commit d9cfb2c278)
2024-12-27 16:31:05 +02:00
Gustas
52102da839 Fix harvesters refusing to queue dock orders whenn empty
(cherry picked from commit 1904012604)
2024-12-27 16:30:58 +02:00
test1232156
fc08f18663 Fix translation key
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
2024-12-21 19:30:50 +02:00
Pavel Penev
cc064d0dad Added descriptions to ContentInstallerFileSystemLoader
(cherry picked from commit 955bf96a84)
2024-12-21 16:04:36 +02:00
Pavel Penev
8b997a5656 Just some package definition reordering
Mostly superficial, but in case of file name collisions we want the mod-specific files to override the common ones.

(cherry picked from commit f210b39bbc)
2024-12-21 16:04:32 +02:00
Pavel Penev
7d8fff535d Added missing D2k packages
(cherry picked from commit 59c0c908cc)
2024-12-21 16:04:28 +02:00
Pavel Penev
0855a9e5bf Fixed whitespace issues in mod.yaml files
An oversight from PR 21598.

(cherry picked from commit 640aa6545b)
2024-12-21 16:04:24 +02:00
Gustas
3fa2b9fe79 Add delete hotkey
(cherry picked from commit 269542c4b0)
2024-12-20 23:57:55 +02:00
Gustas
c561509c82 Add delete button to the map editor
(cherry picked from commit f820adab51)
2024-12-20 23:57:50 +02:00
Gustas
0566b6f787 Update extractors to preserve indentation
(cherry picked from commit 48d5b3c8d9)
2024-12-20 22:00:58 +02:00
Gustas
e2821933a4 Fixup new lines in encyclopedias
(cherry picked from commit 81875a1f3f)
2024-12-20 22:00:57 +02:00
Gustas
34c29f872a Fix unit descriptions having incorrect indentation
(cherry picked from commit 0bd139b061)
2024-12-20 22:00:56 +02:00
test1232156
2192a6b4f9 Encyclopedia fixup
(cherry picked from commit 9719c1a3b0)
2024-12-20 22:00:54 +02:00
RoosterDragon
68ad9163ff Fix BaseBuilderBotModule.LocomotorsForProducibles.
Account for per-actor production (e.g. ProductionQueue) and per-player production (e.g. ClassicProductionQueue). This requires resolving the Production and ProductionQueue traits on both the producing actor, and the owning player actor.

When setting rally points, check the actor didn't die first.
2024-12-19 19:36:39 +02:00
Gustas
5f6eecc1f6 Reduce the size of battlefield news when messages are short
(cherry picked from commit 2a1eeae7ca)
2024-12-19 18:18:26 +02:00
Gustas
5edca87622 Fix Package 'NuGet.CommandLine' 4.4.1 has a known moderate severity vulnerability
(cherry picked from commit 43325fdee7)
2024-12-12 16:01:35 +02:00
Mustafa Alperen Seki
fa01c24f23 Fix broken condition support for InstantlyRepairable. 2024-12-12 00:30:11 +02:00
Pavel Penev
593c2735ce Revert "Restore threaded renderer on Windows in windowed mode."
This reverts commit 874fa67036.
2024-12-12 00:26:05 +02:00
test1232156
48e72c62f3 Translation key fixes
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
2024-12-04 22:10:37 +02:00
Matthias Mailänder
f316d4c757 Update dependency model to fix CVE warnings. 2024-12-02 21:31:01 +02:00
RoosterDragon
d6dba3e545 Fix Animation shadow to account for height.
If a unit is above the terrain, the shadow shouldn't display directly underneath the unit, it should take the height into account and display further down. This fix uses the same adjustment as applied by the WithShadow trait.
2024-11-30 14:45:40 +02:00
1878 changed files with 15110 additions and 61078 deletions

View File

@@ -63,7 +63,7 @@ dotnet_diagnostic.IDE0161.severity = warning
# IDE0200 Remove unnecessary lambda expression
#csharp_style_prefer_method_group_conversion = true
dotnet_diagnostic.IDE0200.severity = warning
dotnet_diagnostic.IDE0200.severity = silent # Requires C# 11
# IDE0210 Convert to top-level statements/IDE0211 Convert to 'Program.Main' style program
csharp_style_prefer_top_level_statements = false
@@ -72,7 +72,7 @@ dotnet_diagnostic.IDE0211.severity = warning
# IDE0290 Use primary constructor
#csharp_style_prefer_primary_constructors = true
dotnet_diagnostic.IDE0290.severity = silent
dotnet_diagnostic.IDE0200.severity = silent # Requires C# 12
# IDE0330 Prefer 'System.Threading.Lock'
#csharp_prefer_system_threading_lock = true
@@ -229,10 +229,6 @@ dotnet_diagnostic.IDE0100.severity = warning
# No options
dotnet_diagnostic.IDE0120.severity = warning
# IDE0121 Simplify LINQ type check and cast
# No options
dotnet_diagnostic.IDE0121.severity = warning
# IDE0130 Namespace does not match folder structure
#dotnet_style_namespace_match_folder = true
# This rule doesn't appear to work (never reports violations)
@@ -296,7 +292,7 @@ dotnet_diagnostic.IDE0220.severity = warning
# IDE0230 Use UTF-8 string literal
#csharp_style_prefer_utf8_string_literals = true
dotnet_diagnostic.IDE0230.severity = warning
dotnet_diagnostic.IDE0230.severity = silent # Requires C# 11
# IDE0240 Nullable directive is redundant
# No options
@@ -312,43 +308,27 @@ dotnet_diagnostic.IDE0241.severity = warning
# IDE0300 Use collection expression for array
# From above, uses dotnet_style_prefer_collection_expression
dotnet_diagnostic.IDE0300.severity = warning
dotnet_diagnostic.IDE0300.severity = silent # Requires C# 12
# IDE0301 Use collection expression for empty
# From above, uses dotnet_style_prefer_collection_expression
dotnet_diagnostic.IDE0301.severity = warning
dotnet_diagnostic.IDE0301.severity = silent # Requires C# 12
# IDE0302 Use collection expression for stackalloc
# From above, uses dotnet_style_prefer_collection_expression
dotnet_diagnostic.IDE0302.severity = warning
dotnet_diagnostic.IDE0302.severity = silent # Requires C# 12
# IDE0303 Use collection expression for 'Create()'
# From above, uses dotnet_style_prefer_collection_expression
dotnet_diagnostic.IDE0303.severity = warning
dotnet_diagnostic.IDE0303.severity = silent # Requires C# 12
# IDE0304 Use collection expression for builder
# From above, uses dotnet_style_prefer_collection_expression
dotnet_diagnostic.IDE0304.severity = warning
dotnet_diagnostic.IDE0304.severity = silent # Requires C# 12
# IDE0305 Use collection expression for fluent
# From above, uses dotnet_style_prefer_collection_expression
dotnet_diagnostic.IDE0305.severity = silent
# IDE0306 Use collection expression for new
# From above, uses dotnet_style_prefer_collection_expression
dotnet_diagnostic.IDE0306.severity = warning
# IDE0340 Use unbound generic type
#csharp_style_prefer_unbound_generic_type_in_nameof = true
dotnet_diagnostic.IDE0340.severity = silent # Requires C# 14
# IDE0350 Use implicitly typed lambda
#csharp_style_prefer_implicitly_typed_lambda_expression = true
dotnet_diagnostic.IDE0350.severity = warning
# IDE0360 Simplify property accessor
#csharp_style_prefer_simple_property_accessors = true
dotnet_diagnostic.IDE0360.severity = silent # Requires C# 13
dotnet_diagnostic.IDE0305.severity = silent # Requires C# 12
## Field preferences
@@ -393,10 +373,6 @@ dotnet_diagnostic.IDE0251.severity = warning
#csharp_prefer_static_anonymous_function = true
dotnet_diagnostic.IDE0320.severity = warning
# IDE0380 Remove unnecessary 'unsafe' modifier
# No options
dotnet_diagnostic.IDE0380.severity = warning
## Null-checking preferences
# IDE1005 Use conditional delegate call
@@ -411,7 +387,7 @@ dotnet_diagnostic.IDE0060.severity = warning
# IDE0280 Use 'nameof'
# No options
dotnet_diagnostic.IDE0280.severity = warning
dotnet_diagnostic.IDE0280.severity = silent # Requires C# 11
## Parentheses preferences
@@ -450,7 +426,7 @@ dotnet_diagnostic.IDE0083.severity = warning
# IDE0170 Simplify property pattern
#csharp_style_prefer_extended_property_pattern = true
dotnet_diagnostic.IDE0170.severity = warning
dotnet_diagnostic.IDE0170.severity = silent # Requires C# 10
## Suppression preferences
@@ -458,10 +434,6 @@ dotnet_diagnostic.IDE0170.severity = warning
#dotnet_remove_unnecessary_suppression_exclusions = none
dotnet_diagnostic.IDE0079.severity = warning
# IDE0370 Remove unnecessary suppression (null-forgiving operator)
# No options
dotnet_diagnostic.IDE0370.severity = warning
## 'this' and 'Me' preferences
# IDE0003/IDE0009 Remove 'this' or 'Me' qualification/Add 'this' or 'Me' qualification
@@ -776,7 +748,7 @@ dotnet_diagnostic.CA1305.severity = warning
dotnet_diagnostic.CA1310.severity = warning
# Specify a culture or use an invariant version.
dotnet_diagnostic.CA1311.severity = warning
dotnet_diagnostic.CA1311.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
### Portability and Interoperability Rules
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/interoperability-warnings
@@ -791,25 +763,19 @@ dotnet_diagnostic.CA1417.severity = warning
dotnet_diagnostic.CA1507.severity = warning
# Use ArgumentNullException throw helper.
dotnet_diagnostic.CA1510.severity = warning
dotnet_diagnostic.CA1510.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
# Use ArgumentException throw helper.
dotnet_diagnostic.CA1511.severity = warning
dotnet_diagnostic.CA1511.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
# Use ArgumentOutOfRangeException throw helper.
dotnet_diagnostic.CA1512.severity = warning
dotnet_diagnostic.CA1512.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
# Use ObjectDisposedException throw helper.
dotnet_diagnostic.CA1513.severity = warning
dotnet_diagnostic.CA1513.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
# Avoid redundant length argument.
dotnet_diagnostic.CA1514.severity = warning
# Consider making public types internal.
dotnet_diagnostic.CA1515.severity = warning
# Use cross-platform intrinsics.
dotnet_diagnostic.CA1516.severity = warning
dotnet_diagnostic.CA1514.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
### Naming Rules
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/naming-warnings
@@ -902,8 +868,8 @@ dotnet_diagnostic.CA1843.severity = warning
# Provide memory-based overrides of async methods when subclassing 'Stream'.
dotnet_diagnostic.CA1844.severity = warning
# Use span-based 'string.Concat'.
dotnet_diagnostic.CA1845.severity = warning
# Use span-based 'string.Concat'. (Not available on mono)
dotnet_diagnostic.CA1845.severity = none
# Prefer AsSpan over Substring.
dotnet_diagnostic.CA1846.severity = warning
@@ -912,34 +878,34 @@ dotnet_diagnostic.CA1846.severity = warning
dotnet_diagnostic.CA1847.severity = warning
# Call async methods when in an async method.
dotnet_diagnostic.CA1849.severity = warning
dotnet_diagnostic.CA1849.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
# Prefer static HashData method over ComputeHash.
dotnet_diagnostic.CA1850.severity = warning
# Prefer static HashData method over ComputeHash. (Not available on mono)
dotnet_diagnostic.CA1850.severity = none # TODO: Change to warning once using .NET 7 or later AND once supported by mono.
# Possible multiple enumerations of IEnumerable collection.
#dotnet_code_quality.CA1851.enumeration_methods =
dotnet_code_quality.CA1851.linq_chain_methods = M:OpenRA.Traits.IRenderModifier.Modify*
dotnet_code_quality.CA1851.assume_method_enumerates_parameters = true
dotnet_diagnostic.CA1851.severity = warning
dotnet_diagnostic.CA1851.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
# Seal internal types.
dotnet_diagnostic.CA1852.severity = warning
dotnet_diagnostic.CA1852.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
# Unnecessary call to 'Dictionary.ContainsKey(key)'.
dotnet_diagnostic.CA1853.severity = warning
dotnet_diagnostic.CA1853.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
# Prefer the IDictionary.TryGetValue(TKey, out TValue) method.
dotnet_diagnostic.CA1854.severity = warning
dotnet_diagnostic.CA1854.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
# Use Span<T>.Clear() instead of Span<T>.Fill().
dotnet_diagnostic.CA1855.severity = warning
dotnet_diagnostic.CA1855.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
# Incorrect usage of ConstantExpected attribute.
dotnet_diagnostic.CA1856.severity = warning
dotnet_diagnostic.CA1856.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
# The parameter expects a constant for optimal performance.
dotnet_diagnostic.CA1857.severity = warning
dotnet_diagnostic.CA1857.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
# Use StartsWith instead of IndexOf.
dotnet_diagnostic.CA1858.severity = warning
@@ -951,24 +917,24 @@ dotnet_diagnostic.CA1860.severity = warning
dotnet_diagnostic.CA1862.severity = warning
# Use 'CompositeFormat'.
dotnet_diagnostic.CA1863.severity = warning
dotnet_diagnostic.CA1863.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
# Prefer the 'IDictionary.TryAdd(TKey, TValue)' method.
dotnet_diagnostic.CA1864.severity = warning
dotnet_diagnostic.CA1864.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
# Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char.
dotnet_diagnostic.CA1865.severity = warning
dotnet_diagnostic.CA1866.severity = warning
dotnet_diagnostic.CA1867.severity = warning
dotnet_diagnostic.CA1865.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
dotnet_diagnostic.CA1866.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
dotnet_diagnostic.CA1867.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
# Unnecessary call to 'Contains' for sets.
dotnet_diagnostic.CA1868.severity = warning
dotnet_diagnostic.CA1868.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
# Cache and reuse 'JsonSerializerOptions' instances.
dotnet_diagnostic.CA1869.severity = warning
dotnet_diagnostic.CA1869.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
# Use a cached 'SearchValues' instance.
dotnet_diagnostic.CA1870.severity = warning
dotnet_diagnostic.CA1870.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
# Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull'.
dotnet_diagnostic.CA1871.severity = warning
@@ -976,18 +942,6 @@ dotnet_diagnostic.CA1871.severity = warning
# Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'.
dotnet_diagnostic.CA1872.severity = warning
# Avoid potentially expensive logging.
dotnet_diagnostic.CA1873.severity = warning
# Use 'Regex.IsMatch'.
dotnet_diagnostic.CA1874.severity = warning
# Use Use 'Regex.Count'.
dotnet_diagnostic.CA1875.severity = warning
# Use 'Path.Combine' or 'Path.Join' overloads.
dotnet_diagnostic.CA1877.severity = warning
### Reliability Rules
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/reliability-warnings
@@ -1010,7 +964,7 @@ dotnet_diagnostic.CA2016.severity = warning
dotnet_diagnostic.CA2018.severity = warning
# ThreadStatic fields should not use inline initialization.
dotnet_diagnostic.CA2019.severity = warning
dotnet_diagnostic.CA2019.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
# Don't call Enumerable.Cast<T> or Enumerable.OfType<T> with incompatible types.
dotnet_diagnostic.CA2021.severity = warning
@@ -1018,18 +972,6 @@ dotnet_diagnostic.CA2021.severity = warning
# Avoid inexact read with Stream.Read.
dotnet_diagnostic.CA2022.severity = warning
# Invalid braces in message template.
dotnet_diagnostic.CA2023.severity = warning
# Do not use StreamReader.EndOfStream in async methods.
dotnet_diagnostic.CA2024.severity = warning
# Do not pass 'IDisposable' instances into unawaited tasks.
dotnet_diagnostic.CA2025.severity = warning
# Prefer JsonElement.Parse over JsonDocument.Parse().RootElement.
dotnet_diagnostic.CA2026.severity = warning
### Security Rules
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/security-warnings
@@ -1099,10 +1041,10 @@ dotnet_diagnostic.CA2250.severity = warning
dotnet_diagnostic.CA2251.severity = warning
# Ensure ThreadStatic is only used with static fields.
dotnet_diagnostic.CA2259.severity = warning
dotnet_diagnostic.CA2259.severity = suggestion # TODO: Change to warning once using .NET 7 or later.
# Prefer generic overload when type is known.
dotnet_diagnostic.CA2263.severity = warning
dotnet_diagnostic.CA2263.severity = none # TODO: Change to warning once mono is dropped.
# Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull'.
dotnet_diagnostic.CA2264.severity = warning
@@ -1318,24 +1260,3 @@ dotnet_diagnostic.RCS1239.severity = warning
# Use element access.
dotnet_diagnostic.RCS1246.severity = warning
# Use enum field explicitly.
dotnet_diagnostic.RCS1257.severity = warning
# Unnecessary enum flag.
dotnet_diagnostic.RCS1258.severity = warning
# Resource can be disposed asynchronously.
dotnet_diagnostic.RCS1261.severity = warning
# Unnecessary raw string literal.
dotnet_diagnostic.RCS1262.severity = warning
# Remove redundant catch block.
dotnet_diagnostic.RCS1265.severity = warning
# Use raw string literal.
dotnet_diagnostic.RCS1266.severity = warning
# Use string interpolation instead of 'string.Concat'.
dotnet_diagnostic.RCS1267.severity = warning

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,28 @@ 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
- name: Remove System .NET
run: sudo apt-get remove -y dotnet*
- name: Install .NET 8.0
uses: actions/setup-dotnet@v5
- name: Clone Repository
uses: actions/checkout@v4
- name: Install .NET 6.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
dotnet-version: '6.0.x'
- name: Check Code
run: |
@@ -38,18 +36,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@v4
- 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@v4
- name: Install .NET 6.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
dotnet-version: '6.0.x'
- name: Check Code
shell: powershell
@@ -61,7 +77,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

@@ -52,21 +52,21 @@ jobs:
echo ${{ needs.prepare.outputs.version_type }}
- name: Clone Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.git_tag }}
- name: Install .NET 8
uses: actions/setup-dotnet@v5
- name: Install .NET 6
uses: actions/setup-dotnet@v4
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@v4
with:
repository: openra/openra.wiki
token: ${{ secrets.DOCS_TOKEN }}
@@ -107,14 +107,14 @@ jobs:
echo ${{ needs.prepare.outputs.version_type }}
- name: Clone Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.git_tag }}
- name: Install .NET 8
uses: actions/setup-dotnet@v5
- name: Install .NET 6
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
dotnet-version: '6.0.x'
- name: Prepare Environment
run: |
@@ -122,7 +122,7 @@ jobs:
# version_type is release/playtest/bleed - the name of the target branch.
- name: Clone docs.openra.net
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
repository: openra/docs
token: ${{ secrets.DOCS_TOKEN }}

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- 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@v4
- name: Install .NET 8.0
uses: actions/setup-dotnet@v5
- name: Install .NET 6.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
dotnet-version: '6.0.x'
- name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -65,15 +65,15 @@ jobs:
macos:
name: macOS Disk Image
runs-on: macos-14
runs-on: macos-13
steps:
- name: Clone Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install .NET 8.0
uses: actions/setup-dotnet@v5
- name: Install .NET 6.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
dotnet-version: '6.0.x'
- name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -101,12 +101,12 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Install .NET 8.0
uses: actions/setup-dotnet@v5
- name: Install .NET 6.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
dotnet-version: '6.0.x'
- name: Prepare Environment
run: |
@@ -119,37 +119,9 @@ 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
gh release upload ${{ github.ref_name }} 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

View File

@@ -36,7 +36,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 +54,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)
@@ -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)

View File

@@ -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

@@ -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>
@@ -52,8 +53,9 @@
<!-- StyleCop/Roslynator -->
<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" />
<!-- Roslynator analyzers fail to run under Mono (AD0001) -->
<PackageReference Include="Roslynator.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
</ItemGroup>
</Project>

View File

@@ -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

@@ -8,7 +8,7 @@ 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)
* [.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,9 +17,11 @@ 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`.
@@ -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:
@@ -149,7 +161,7 @@ endif
@sh -c '. ./packaging/functions.sh; set_mod_version "$(VERSION)" mods/*/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

@@ -89,21 +89,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 +123,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())
@@ -290,18 +287,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 +504,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 +541,7 @@ namespace OpenRA
if (EnabledTargetablePositions.Any())
return enabledTargetableWorldPositions;
return [CenterPosition];
return new[] { CenterPosition };
}
#region Conditions

View File

@@ -78,9 +78,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);
}

View File

@@ -62,7 +62,7 @@ namespace OpenRA
public override string ToString() { return X + "," + Y; }
public static readonly CVec[] Directions =
[
{
new(-1, -1),
new(-1, 0),
new(-1, 1),
@@ -71,7 +71,7 @@ namespace OpenRA
new(1, -1),
new(1, 0),
new(1, 1),
];
};
#region Scripting interface

View File

@@ -20,7 +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 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();
@@ -210,7 +210,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 +236,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,12 +252,14 @@ namespace OpenRA
public static string SHA1Hash(Stream data)
{
return ToHex(SHA1.HashData(data), true);
using var csp = SHA1.Create();
return ToHex(csp.ComputeHash(data), true);
}
public static string SHA1Hash(byte[] data)
{
return ToHex(SHA1.HashData(data), true);
using var csp = SHA1.Create();
return ToHex(csp.ComputeHash(data), true);
}
public static string SHA1Hash(string data)

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;
@@ -29,7 +28,7 @@ namespace OpenRA
public readonly string Id;
public readonly string Version;
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 +40,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()
@@ -123,13 +122,13 @@ namespace OpenRA
return;
var key = ExternalMod.MakeKey(mod);
var yaml = new MiniYamlNode("Registration", new MiniYaml("",
[
var yaml = new MiniYamlNode("Registration", new MiniYaml("", new[]
{
new MiniYamlNode("Id", mod.Id),
new MiniYamlNode("Version", mod.Metadata.Version),
new MiniYamlNode("LaunchPath", launchPath),
new MiniYamlNode("LaunchArgs", new[] { "Game.Mod=" + mod.Id }.Concat(launchArgs).JoinWith(", "))
]));
}));
var iconNodes = new List<MiniYamlNode>();

View File

@@ -11,11 +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;
@@ -79,7 +77,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 +119,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)
@@ -414,6 +418,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)
@@ -456,14 +465,14 @@ namespace OpenRA
// Check for a key conflict:
if (!output.TryAdd(key, element))
{
dupKeys ??= [];
dupKeys ??= new Dictionary<TKey, List<string>>();
if (!dupKeys.TryGetValue(key, out var dupKeyMessages))
{
// Log the initial conflicting value already inserted:
dupKeyMessages =
[
dupKeyMessages = new List<string>
{
logValue(output[key])
];
};
dupKeys.Add(key, dupKeyMessages);
}
@@ -478,7 +487,7 @@ namespace OpenRA
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)}]");
badKeysFormatted.Append($"{logKey(p.Key)}: [{string.Join(",", p.Value)}]");
throw new ArgumentException(badKeysFormatted.ToString());
}
}
@@ -501,16 +510,44 @@ namespace OpenRA
return result;
}
public static T[,] ResizeArray<T>(T[,] ts, T t, int width, int height)
{
var result = new T[width, height];
for (var i = 0; i < width; i++)
{
for (var j = 0; j < height; j++)
{
// Workaround for broken ternary operators in certain versions of mono
// (3.10 and certain versions of the 3.8 series): https://bugzilla.xamarin.com/show_bug.cgi?id=23319
if (i <= ts.GetUpperBound(0) && j <= ts.GetUpperBound(1))
result[i, j] = ts[i, j];
else
result[i, j] = t;
}
}
return result;
}
public static int ToBits(this IEnumerable<bool> bits)
{
var i = 0;
var result = 0;
foreach (var b in bits)
if (b)
result |= 1 << i++;
else
i++;
if (i > 33)
throw new InvalidOperationException("ToBits only accepts up to 32 values.");
return result;
}
public static byte ParseByteInvariant(string s)
{
return byte.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static ushort ParseUshortInvariant(string s)
{
return ushort.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static short ParseInt16Invariant(string s)
{
return short.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
@@ -521,22 +558,6 @@ namespace OpenRA
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static float ParseFloatOrPercentInvariant(string s)
{
var f = float.Parse(s.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo);
return f * (s.Contains('%') ? 0.01f : 1f);
}
public static bool TryParseByteInvariant(string s, out byte i)
{
return byte.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
public static bool TryParseUshortInvariant(string s, out ushort i)
{
return ushort.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
public static bool TryParseInt32Invariant(string s, out int i)
{
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
@@ -547,17 +568,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);
@@ -573,16 +583,6 @@ namespace OpenRA
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);
@@ -665,7 +665,7 @@ namespace OpenRA
if (index == -1)
{
// The remaining string is an empty string
str = [];
str = ReadOnlySpan<char>.Empty;
Current = span;
return true;
}
@@ -677,4 +677,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;
}
}
}

View File

@@ -10,14 +10,13 @@
#endregion
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using OpenRA.Primitives;
using OpenRA.Support;
@@ -25,8 +24,9 @@ namespace OpenRA
{
public static class FieldLoader
{
const char Comma = ',';
const char SplitComma = ',';
[Serializable]
public class MissingFieldsException : YamlException
{
public readonly string[] Missing;
@@ -46,6 +46,13 @@ namespace OpenRA
Header = missing.Length > 1 ? header : headerSingle ?? header;
Missing = missing;
}
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("Missing", Missing);
info.AddValue("Header", Header);
}
}
public static Func<string, Type, string, object> InvalidValueAction = (s, t, f) =>
@@ -61,8 +68,8 @@ namespace OpenRA
static readonly ConcurrentCache<string, IntegerExpression> IntegerExpressionCache =
new(expression => new IntegerExpression(expression));
static readonly FrozenDictionary<Type, Func<string, Type, string, object>> TypeParsers =
new Dictionary<Type, Func<string, Type, string, object>>
static readonly Dictionary<Type, Func<string, Type, string, MemberInfo, object>> TypeParsers =
new()
{
{ typeof(int), ParseInt },
{ typeof(ushort), ParseUShort },
@@ -85,6 +92,7 @@ namespace OpenRA
{ typeof(CVec[]), ParseCVecArray },
{ typeof(BooleanExpression), ParseBooleanExpression },
{ typeof(IntegerExpression), ParseIntegerExpression },
{ typeof(Enum), ParseEnum },
{ typeof(bool), ParseBool },
{ typeof(int2[]), ParseInt2Array },
{ typeof(Size), ParseSize },
@@ -93,44 +101,23 @@ namespace OpenRA
{ typeof(float3), ParseFloat3 },
{ typeof(Rectangle), ParseRectangle },
{ typeof(DateTime), ParseDateTime }
}.ToFrozenDictionary();
};
static readonly FrozenDictionary<Type, Func<string, Type, string, MiniYaml, object>> GenericTypeParsers =
new Dictionary<Type, Func<string, Type, string, MiniYaml, object>>
static readonly Dictionary<Type, Func<string, Type, string, MiniYaml, MemberInfo, object>> GenericTypeParsers =
new()
{
{ typeof(HashSet<>), ParseHashSetOrList },
{ typeof(List<>), ParseHashSetOrList },
{ typeof(Dictionary<,>), ParseDictionary },
{ typeof(ImmutableArray<>), ParseImmutableArray },
{ typeof(FrozenSet<>), ParseFrozenSet },
{ typeof(FrozenDictionary<,>), ParseFrozenDictionary },
{ typeof(BitSet<>), ParseBitSet },
{ typeof(Nullable<>), ParseNullable },
}.ToFrozenDictionary();
};
static readonly object BoxedTrue = true;
static readonly object BoxedFalse = false;
static readonly object[] BoxedInts = Exts.MakeArray(33, i => (object)i);
static readonly MethodInfo ToImmutableArray =
typeof(ImmutableArray)
.GetMethods()
.Single(m =>
m.Name == nameof(ImmutableArray.ToImmutableArray) &&
m.GetParameters()?.First().ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>));
static readonly MethodInfo ToFrozenSet =
typeof(FrozenSet)
.GetMethod(nameof(FrozenSet.ToFrozenSet));
static readonly MethodInfo ToFrozenDictionary =
typeof(FrozenDictionary)
.GetMethods()
.Single(m =>
m.Name == nameof(FrozenDictionary.ToFrozenDictionary) &&
m.GetParameters().Length == 2);
static object ParseInt(string fieldName, Type fieldType, string value)
static object ParseInt(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (Exts.TryParseInt32Invariant(value, out var res))
{
@@ -142,48 +129,48 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseUShort(string fieldName, Type fieldType, string value)
static object ParseUShort(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (Exts.TryParseUshortInvariant(value, out var res))
if (ushort.TryParse(value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var res))
return res;
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseLong(string fieldName, Type fieldType, string value)
static object ParseLong(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (Exts.TryParseInt64Invariant(value, out var res))
if (long.TryParse(value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var res))
return res;
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseFloat(string fieldName, Type fieldType, string value)
static object ParseFloat(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (Exts.TryParseFloatOrPercentInvariant(value, out var res))
return res;
if (value != null && float.TryParse(value.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var res))
return res * (value.Contains('%') ? 0.01f : 1f);
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseDecimal(string fieldName, Type fieldType, string value)
static object ParseDecimal(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null && decimal.TryParse(value.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var res))
return res * (value.Contains('%') ? 0.01m : 1m);
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseString(string fieldName, Type fieldType, string value)
static object ParseString(string fieldName, Type fieldType, string value, MemberInfo field)
{
return value;
}
static object ParseColor(string fieldName, Type fieldType, string value)
static object ParseColor(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (Color.TryParse(value, out var color))
if (value != null && Color.TryParse(value, out var color))
return color;
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseHotkey(string fieldName, Type fieldType, string value)
static object ParseHotkey(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (Hotkey.TryParse(value, out var res))
return res;
@@ -191,12 +178,12 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseHotkeyReference(string fieldName, Type fieldType, string value)
static object ParseHotkeyReference(string fieldName, Type fieldType, string value, MemberInfo field)
{
return Game.ModData.Hotkeys[value];
}
static object ParseWDist(string fieldName, Type fieldType, string value)
static object ParseWDist(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (WDist.TryParse(value, out var res))
return res;
@@ -204,11 +191,11 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseWVec(string fieldName, Type fieldType, string value)
static object ParseWVec(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var parts = value.Split(SplitComma);
if (parts.Length == 3
&& WDist.TryParse(parts[0], out var rx)
&& WDist.TryParse(parts[1], out var ry)
@@ -219,11 +206,11 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseWVecArray(string fieldName, Type fieldType, string value)
static object ParseWVecArray(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var parts = value.Split(SplitComma);
if (parts.Length % 3 != 0)
return InvalidValueAction(value, fieldType, fieldName);
@@ -236,8 +223,6 @@ namespace OpenRA
&& WDist.TryParse(parts[3 * i + 1], out var ry)
&& WDist.TryParse(parts[3 * i + 2], out var rz))
vecs[i] = new WVec(rx, ry, rz);
else
return InvalidValueAction(value, fieldType, fieldName);
}
return vecs;
@@ -246,11 +231,11 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseWPos(string fieldName, Type fieldType, string value)
static object ParseWPos(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var parts = value.Split(SplitComma);
if (parts.Length == 3
&& WDist.TryParse(parts[0], out var rx)
&& WDist.TryParse(parts[1], out var ry)
@@ -261,18 +246,18 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseWAngle(string fieldName, Type fieldType, string value)
static object ParseWAngle(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (Exts.TryParseInt32Invariant(value, out var res))
return new WAngle(res);
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseWRot(string fieldName, Type fieldType, string value)
static object ParseWRot(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var parts = value.Split(SplitComma);
if (parts.Length == 3
&& Exts.TryParseInt32Invariant(parts[0], out var rr)
&& Exts.TryParseInt32Invariant(parts[1], out var rp)
@@ -283,31 +268,27 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseCPos(string fieldName, Type fieldType, string value)
static object ParseCPos(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 3
&& Exts.TryParseInt32Invariant(parts[0], out var x)
&& Exts.TryParseInt32Invariant(parts[1], out var y)
&& Exts.TryParseByteInvariant(parts[2], out var layer))
return new CPos(x, y, layer);
if (parts.Length == 2
&& Exts.TryParseInt32Invariant(parts[0], out x)
&& Exts.TryParseInt32Invariant(parts[1], out y))
return new CPos(x, y);
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 3)
return new CPos(
Exts.ParseInt32Invariant(parts[0]),
Exts.ParseInt32Invariant(parts[1]),
Exts.ParseByteInvariant(parts[2]));
return new CPos(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseCPosArray(string fieldName, Type fieldType, string value)
static object ParseCPosArray(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var parts = value.Split(SplitComma);
if (parts.Length % 2 != 0)
return InvalidValueAction(value, fieldType, fieldName);
@@ -315,11 +296,9 @@ namespace OpenRA
var vecs = new CPos[parts.Length / 2];
for (var i = 0; i < vecs.Length; i++)
{
if (Exts.TryParseInt32Invariant(parts[2 * i], out var rx)
&& Exts.TryParseInt32Invariant(parts[2 * i + 1], out var ry))
if (int.TryParse(parts[2 * i], out var rx)
&& int.TryParse(parts[2 * i + 1], out var ry))
vecs[i] = new CPos(rx, ry);
else
return InvalidValueAction(value, fieldType, fieldName);
}
return vecs;
@@ -328,25 +307,22 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseCVec(string fieldName, Type fieldType, string value)
static object ParseCVec(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 2
&& Exts.TryParseInt32Invariant(parts[0], out var x)
&& Exts.TryParseInt32Invariant(parts[1], out var y))
return new CVec(x, y);
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new CVec(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseCVecArray(string fieldName, Type fieldType, string value)
static object ParseCVecArray(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var parts = value.Split(SplitComma);
if (parts.Length % 2 != 0)
return InvalidValueAction(value, fieldType, fieldName);
@@ -354,11 +330,9 @@ namespace OpenRA
var vecs = new CVec[parts.Length / 2];
for (var i = 0; i < vecs.Length; i++)
{
if (Exts.TryParseInt32Invariant(parts[2 * i], out var rx)
&& Exts.TryParseInt32Invariant(parts[2 * i + 1], out var ry))
if (int.TryParse(parts[2 * i], out var rx)
&& int.TryParse(parts[2 * i + 1], out var ry))
vecs[i] = new CVec(rx, ry);
else
return InvalidValueAction(value, fieldType, fieldName);
}
return vecs;
@@ -367,7 +341,7 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseBooleanExpression(string fieldName, Type fieldType, string value)
static object ParseBooleanExpression(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
@@ -377,14 +351,14 @@ namespace OpenRA
}
catch (InvalidDataException e)
{
throw new YamlException($"FieldLoader: Cannot parse `{value}` into `{fieldName}.{fieldType}`: {e.Message}");
throw new YamlException(e.Message);
}
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseIntegerExpression(string fieldName, Type fieldType, string value)
static object ParseIntegerExpression(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
@@ -394,50 +368,44 @@ namespace OpenRA
}
catch (InvalidDataException e)
{
throw new YamlException($"FieldLoader: Cannot parse `{value}` into `{fieldName}.{fieldType}`: {e.Message}");
throw new YamlException(e.Message);
}
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseEnum(string fieldName, Type fieldType, string value)
static object ParseEnum(string fieldName, Type fieldType, string value, MemberInfo field)
{
// Will allow numeric values that fit the underlying type of the enum, even if they aren't defined enumeration members.
if (Enum.TryParse(fieldType, value, true, out var enumValue))
try
{
return enumValue;
return Enum.Parse(fieldType, value, true);
}
catch (ArgumentException)
{
return InvalidValueAction(value, fieldType, fieldName);
}
}
static object ParseBool(string fieldName, Type fieldType, string value)
static object ParseBool(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (bool.TryParse(value, out var result))
if (bool.TryParse(value.ToLowerInvariant(), out var result))
return result ? BoxedTrue : BoxedFalse;
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseInt2Array(string fieldName, Type fieldType, string value)
static object ParseInt2Array(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length % 2 != 0)
return InvalidValueAction(value, fieldType, fieldName);
var ints = new int2[parts.Length / 2];
for (var i = 0; i < ints.Length; i++)
{
if (Exts.TryParseInt32Invariant(parts[2 * i], out var x)
&& Exts.TryParseInt32Invariant(parts[2 * i + 1], out var y))
ints[i] = new int2(x, y);
else
return InvalidValueAction(value, fieldType, fieldName);
}
ints[i] = new int2(Exts.ParseInt32Invariant(parts[2 * i]), Exts.ParseInt32Invariant(parts[2 * i + 1]));
return ints;
}
@@ -445,130 +413,109 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseSize(string fieldName, Type fieldType, string value)
static object ParseSize(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 2
&& Exts.TryParseInt32Invariant(parts[0], out var width)
&& Exts.TryParseInt32Invariant(parts[1], out var height))
return new Size(width, height);
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new Size(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseInt2(string fieldName, Type fieldType, string value)
static object ParseInt2(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 2
&& Exts.TryParseInt32Invariant(parts[0], out var x)
&& Exts.TryParseInt32Invariant(parts[1], out var y))
return new int2(x, y);
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2)
return InvalidValueAction(value, fieldType, fieldName);
return new int2(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseFloat2(string fieldName, Type fieldType, string value)
static object ParseFloat2(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 2
&& Exts.TryParseFloatOrPercentInvariant(parts[0], out var x)
&& Exts.TryParseFloatOrPercentInvariant(parts[1], out var y))
return new float2(x, y);
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
float xx = 0;
float yy = 0;
if (float.TryParse(parts[0].Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var res))
xx = res * (parts[0].Contains('%') ? 0.01f : 1f);
if (float.TryParse(parts[1].Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out res))
yy = res * (parts[1].Contains('%') ? 0.01f : 1f);
return new float2(xx, yy);
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseFloat3(string fieldName, Type fieldType, string value)
static object ParseFloat3(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 3
&& Exts.TryParseFloatOrPercentInvariant(parts[0], out var x)
&& Exts.TryParseFloatOrPercentInvariant(parts[1], out var y)
&& Exts.TryParseFloatOrPercentInvariant(parts[2], out var z))
return new float3(x, y, z);
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
float.TryParse(parts[0], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var x);
float.TryParse(parts[1], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var y);
// z component is optional for compatibility with older float2 definitions
if (parts.Length == 2
&& Exts.TryParseFloatOrPercentInvariant(parts[0], out x)
&& Exts.TryParseFloatOrPercentInvariant(parts[1], out y))
return new float3(x, y, 0);
float z = 0;
if (parts.Length > 2)
float.TryParse(parts[2], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out z);
return new float3(x, y, z);
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseRectangle(string fieldName, Type fieldType, string value)
static object ParseRectangle(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 4
&& Exts.TryParseInt32Invariant(parts[0], out var x)
&& Exts.TryParseInt32Invariant(parts[1], out var y)
&& Exts.TryParseInt32Invariant(parts[2], out var width)
&& Exts.TryParseInt32Invariant(parts[3], out var height))
return new Rectangle(x, y, width, height);
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new Rectangle(
Exts.ParseInt32Invariant(parts[0]),
Exts.ParseInt32Invariant(parts[1]),
Exts.ParseInt32Invariant(parts[2]),
Exts.ParseInt32Invariant(parts[3]));
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseDateTime(string fieldName, Type fieldType, string value)
static object ParseDateTime(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (DateTime.TryParseExact(value, "yyyy-MM-dd HH-mm-ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dt))
return dt;
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseArray(string fieldName, Type fieldType, string value)
{
var elementType = fieldType.GetElementType();
if (value == null)
return typeof(Array)
.GetMethod(nameof(Array.Empty))
.MakeGenericMethod(elementType)
.Invoke(null, null);
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var ret = Array.CreateInstance(elementType, parts.Length);
for (var i = 0; i < parts.Length; i++)
ret.SetValue(GetValue(fieldName, elementType, parts[i]), i);
return ret;
}
static object ParseHashSetOrList(string fieldName, Type fieldType, string value, MiniYaml yaml)
static object ParseHashSetOrList(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
{
if (value == null)
return Activator.CreateInstance(fieldType);
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
var set = Activator.CreateInstance(fieldType, parts.Length);
var arguments = fieldType.GetGenericArguments();
var addMethod = fieldType.GetMethod(nameof(List<object>.Add), arguments);
var addArgs = new object[1];
for (var i = 0; i < parts.Length; i++)
{
addArgs[0] = GetValue(fieldName, arguments[0], parts[i]);
addArgs[0] = GetValue(fieldName, arguments[0], parts[i].Trim(), field);
addMethod.Invoke(set, addArgs);
}
return set;
}
static object ParseDictionary(string fieldName, Type fieldType, string value, MiniYaml yaml)
static object ParseDictionary(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
{
if (yaml == null)
return Activator.CreateInstance(fieldType);
@@ -579,97 +526,34 @@ namespace OpenRA
var addArgs = new object[2];
foreach (var node in yaml.Nodes)
{
addArgs[0] = GetValue(fieldName, arguments[0], node.Key);
addArgs[1] = GetValue(fieldName, arguments[1], node.Value);
addArgs[0] = GetValue(fieldName, arguments[0], node.Key, field);
addArgs[1] = GetValue(fieldName, arguments[1], node.Value, field);
addMethod.Invoke(dict, addArgs);
}
return dict;
}
static object ParseImmutableArray(string fieldName, Type fieldType, string value, MiniYaml yaml)
{
var typeArgs = fieldType.GenericTypeArguments;
if (value == null)
return typeof(ImmutableArray<>).MakeGenericType(typeArgs)
.GetField(nameof(ImmutableArray<object>.Empty))
.GetValue(null);
object array;
if (typeArgs[0] == typeof(WVec))
array = ParseWVecArray(fieldName, typeArgs[0].MakeArrayType(), value);
else if (typeArgs[0] == typeof(CPos))
array = ParseCPosArray(fieldName, typeArgs[0].MakeArrayType(), value);
else if (typeArgs[0] == typeof(CVec))
array = ParseCVecArray(fieldName, typeArgs[0].MakeArrayType(), value);
else if (typeArgs[0] == typeof(int2))
array = ParseInt2Array(fieldName, typeArgs[0].MakeArrayType(), value);
else
array = ParseArray(fieldName, typeArgs[0].MakeArrayType(), value);
var toImmutableArray = ToImmutableArray.MakeGenericMethod(typeArgs);
return toImmutableArray.Invoke(null, [array]);
}
static object ParseFrozenSet(string fieldName, Type fieldType, string value, MiniYaml yaml)
{
var typeArgs = fieldType.GenericTypeArguments;
if (value == null)
return typeof(FrozenSet<>).MakeGenericType(typeArgs)
.GetProperty(nameof(FrozenSet<object>.Empty))
.GetValue(null);
var set =
ParseHashSetOrList(fieldName, typeof(HashSet<>).MakeGenericType(typeArgs), value, yaml);
var toFrozenSet = ToFrozenSet.MakeGenericMethod(typeArgs);
return toFrozenSet.Invoke(null, [set, null]);
}
static object ParseFrozenDictionary(string fieldName, Type fieldType, string value, MiniYaml yaml)
{
var typeArgs = fieldType.GenericTypeArguments;
if (yaml == null)
return typeof(FrozenDictionary<,>).MakeGenericType(typeArgs)
.GetProperty(nameof(FrozenDictionary<object, object>.Empty))
.GetValue(null);
var dict =
ParseDictionary(fieldName, typeof(Dictionary<,>).MakeGenericType(typeArgs), value, yaml);
var toFrozenDict = ToFrozenDictionary.MakeGenericMethod(typeArgs);
return toFrozenDict.Invoke(null, [dict, null]);
}
static object ParseBitSet(string fieldName, Type fieldType, string value, MiniYaml yaml)
static object ParseBitSet(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var ctor = fieldType.GetConstructor([typeof(string[])]);
return ctor.Invoke([parts]);
}
else
{
var ctor = fieldType.GetConstructor([typeof(string[])]);
return ctor.Invoke([Array.Empty<string>()]);
}
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
var ctor = fieldType.GetConstructor(new[] { typeof(string[]) });
return ctor.Invoke(new object[] { parts.Select(p => p.Trim()).ToArray() });
}
static object ParseNullable(string fieldName, Type fieldType, string value, MiniYaml yaml)
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseNullable(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
{
if (string.IsNullOrEmpty(value))
return null;
var innerType = fieldType.GetGenericArguments()[0];
var innerValue = GetValue("Nullable<T>", innerType, value);
return fieldType.GetConstructor([innerType]).Invoke([innerValue]);
var innerValue = GetValue("Nullable<T>", innerType, value, field);
return fieldType.GetConstructor(new[] { innerType }).Invoke(new[] { innerValue });
}
public static void Load(object self, MiniYaml my)
@@ -690,7 +574,7 @@ namespace OpenRA
val = fli.Loader(my);
else
{
missing ??= [];
missing ??= new List<string>();
missing.Add(fli.YamlName);
continue;
}
@@ -701,7 +585,7 @@ namespace OpenRA
{
if (fli.Attribute.Required)
{
missing ??= [];
missing ??= new List<string>();
missing.Add(fli.YamlName);
}
@@ -723,7 +607,7 @@ namespace OpenRA
if (!md.TryGetValue(yamlName, out var yaml))
return false;
ret = GetValue(field.Name, field.FieldType, yaml);
ret = GetValue(field.Name, field.FieldType, yaml, field);
return true;
}
@@ -734,7 +618,7 @@ namespace OpenRA
return t;
}
public static void LoadFieldOrProperty(object target, string key, string value)
public static void LoadField(object target, string key, string value)
{
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
@@ -743,14 +627,18 @@ namespace OpenRA
var field = target.GetType().GetField(key, Flags);
if (field != null)
{
field.SetValue(target, GetValue(field.Name, field.FieldType, value));
var sa = field.GetCustomAttributes<SerializeAttribute>(false).DefaultIfEmpty(SerializeAttribute.Default).First();
if (!sa.FromYamlKey)
field.SetValue(target, GetValue(field.Name, field.FieldType, value, field));
return;
}
var prop = target.GetType().GetProperty(key, Flags);
if (prop != null)
{
prop.SetValue(target, GetValue(prop.Name, prop.PropertyType, value), null);
var sa = prop.GetCustomAttributes<SerializeAttribute>(false).DefaultIfEmpty(SerializeAttribute.Default).First();
if (!sa.FromYamlKey)
prop.SetValue(target, GetValue(prop.Name, prop.PropertyType, value, prop), null);
return;
}
@@ -762,34 +650,48 @@ namespace OpenRA
return (T)GetValue(field, typeof(T), value, null);
}
static object GetValue(string fieldName, Type fieldType, string value)
public static object GetValue(string fieldName, Type fieldType, string value)
{
return GetValue(fieldName, fieldType, value, null);
}
static object GetValue(string fieldName, Type fieldType, MiniYaml yaml)
public static object GetValue(string fieldName, Type fieldType, string value, MemberInfo field)
{
return GetValue(fieldName, fieldType, yaml.Value, yaml);
return GetValue(fieldName, fieldType, value, null, field);
}
static object GetValue(string fieldName, Type fieldType, string value, MiniYaml yaml)
public static object GetValue(string fieldName, Type fieldType, MiniYaml yaml, MemberInfo field)
{
return GetValue(fieldName, fieldType, yaml.Value, yaml, field);
}
static object GetValue(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
{
value = value?.Trim();
if (fieldType.IsGenericType)
{
if (GenericTypeParsers.TryGetValue(fieldType.GetGenericTypeDefinition(), out var parseFuncGeneric))
return parseFuncGeneric(fieldName, fieldType, value, yaml);
return parseFuncGeneric(fieldName, fieldType, value, yaml, field);
}
else
{
if (TypeParsers.TryGetValue(fieldType, out var parseFunc))
return parseFunc(fieldName, fieldType, value);
return parseFunc(fieldName, fieldType, value, field);
if (fieldType.IsArray && fieldType.GetArrayRank() == 1)
return ParseArray(fieldName, fieldType, value);
{
if (value == null)
return Array.CreateInstance(fieldType.GetElementType(), 0);
if (fieldType.IsEnum)
return ParseEnum(fieldName, fieldType, value);
var options = field != null && field.HasAttribute<AllowEmptyEntriesAttribute>() ?
StringSplitOptions.None : StringSplitOptions.RemoveEmptyEntries;
var parts = value.Split(SplitComma, options);
var ret = Array.CreateInstance(fieldType.GetElementType(), parts.Length);
for (var i = 0; i < parts.Length; i++)
ret.SetValue(GetValue(fieldName, fieldType.GetElementType(), parts[i].Trim(), field), i);
return ret;
}
}
var conv = TypeDescriptor.GetConverter(fieldType);
@@ -813,20 +715,21 @@ namespace OpenRA
{
public readonly FieldInfo Field;
public readonly SerializeAttribute Attribute;
public readonly string YamlName;
public readonly Func<MiniYaml, object> Loader;
public string YamlName => Field.Name;
public FieldLoadInfo(FieldInfo field, SerializeAttribute attr, Func<MiniYaml, object> loader = null)
internal FieldLoadInfo(FieldInfo field, SerializeAttribute attr, string yamlName, Func<MiniYaml, object> loader = null)
{
Field = field;
Attribute = attr;
YamlName = yamlName;
Loader = loader;
}
}
public static IEnumerable<FieldLoadInfo> GetTypeLoadInfo(Type type)
public static IEnumerable<FieldLoadInfo> GetTypeLoadInfo(Type type, bool includePrivateByDefault = false)
{
return TypeLoadInfo[type].Where(fli => fli.Field.IsPublic || (fli.Attribute.Serialize && !fli.Attribute.IsDefault));
return TypeLoadInfo[type].Where(fli => includePrivateByDefault || fli.Field.IsPublic || (fli.Attribute.Serialize && !fli.Attribute.IsDefault));
}
static FieldLoadInfo[] BuildTypeLoadInfo(Type type)
@@ -841,9 +744,13 @@ namespace OpenRA
if (!sa.Serialize)
continue;
var loader = sa.GetLoader(type);
var yamlName = string.IsNullOrEmpty(sa.YamlName) ? field.Name : sa.YamlName;
var fli = new FieldLoadInfo(field, sa, loader);
var loader = sa.GetLoader(type);
if (loader == null && sa.FromYamlKey)
loader = yaml => GetValue(yamlName, field.FieldType, yaml, field);
var fli = new FieldLoadInfo(field, sa, yamlName, loader);
ret.Add(fli);
}
@@ -854,21 +761,31 @@ namespace OpenRA
public sealed class IgnoreAttribute : SerializeAttribute
{
public IgnoreAttribute()
: base(serialize: false) { }
: base(false) { }
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class RequireAttribute : SerializeAttribute
{
public RequireAttribute()
: base(serialize: true, required: true) { }
: base(true, true) { }
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class AllowEmptyEntriesAttribute : SerializeAttribute
{
public AllowEmptyEntriesAttribute()
: base(allowEmptyEntries: true) { }
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class LoadUsingAttribute : SerializeAttribute
{
public LoadUsingAttribute(string loader, bool required = false)
: base(serialize: true, required, loader) { }
{
Loader = loader;
Required = required;
}
}
[AttributeUsage(AttributeTargets.Field)]
@@ -879,14 +796,18 @@ namespace OpenRA
public bool IsDefault => this == Default;
public readonly bool Serialize;
public readonly bool Required;
public readonly string Loader;
public string YamlName;
public string Loader;
public bool FromYamlKey;
public bool DictionaryFromYamlKey;
public bool Required;
public bool AllowEmptyEntries;
protected SerializeAttribute(bool serialize = true, bool required = false, string loader = null)
public SerializeAttribute(bool serialize = true, bool required = false, bool allowEmptyEntries = false)
{
Serialize = serialize;
Required = required;
Loader = loader;
AllowEmptyEntries = allowEmptyEntries;
}
internal Func<MiniYaml, object> GetLoader(Type type)
@@ -906,4 +827,32 @@ namespace OpenRA
}
}
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class FieldFromYamlKeyAttribute : FieldLoader.SerializeAttribute
{
public FieldFromYamlKeyAttribute()
{
FromYamlKey = true;
}
}
// Special-cases FieldFromYamlKeyAttribute for use with Dictionary<K,V>.
[AttributeUsage(AttributeTargets.Field)]
public sealed class DictionaryFromYamlKeyAttribute : FieldLoader.SerializeAttribute
{
public DictionaryFromYamlKeyAttribute()
{
FromYamlKey = true;
DictionaryFromYamlKey = true;
}
}
// Mirrors DescriptionAttribute from System.ComponentModel but we don't want to have to use that everywhere.
[AttributeUsage(AttributeTargets.All)]
public sealed class DescAttribute : Attribute
{
public readonly string[] Lines;
public DescAttribute(params string[] lines) { Lines = lines; }
}
}

View File

@@ -10,9 +10,7 @@
#endregion
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
@@ -24,38 +22,39 @@ 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(
@@ -80,35 +79,11 @@ 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 dict = (System.Collections.IDictionary)v;
@@ -120,7 +95,7 @@ namespace OpenRA
var formattedKey = FormatValue(key);
var formattedValue = FormatValue(value);
result.Append(CultureInfo.InvariantCulture, $"{formattedKey}: {formattedValue}{Environment.NewLine}");
result.Append($"{formattedKey}: {formattedValue}{Environment.NewLine}");
}
return result.ToString();

View File

@@ -10,105 +10,29 @@
#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;
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,50 +43,43 @@ 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];
byte bitDepth = 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 = s.ReadASCII(4);
var content = s.ReadBytes(length);
s.ReadInt32(); // crc
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.");
using (var ms = new MemoryStream(content))
{
switch (type)
{
case ChunkIHDR:
case "IHDR":
{
if (headerParsed)
throw new InvalidDataException("Invalid PNG file - duplicate header.");
Width = IPAddress.NetworkToHostOrder(ms.ReadInt32());
Height = IPAddress.NetworkToHostOrder(ms.ReadInt32());
#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];
bitDepth = ms.ReadUInt8();
var colorType = (PngColorType)ms.ReadUInt8();
if (IsPaletted(bitDepth, colorType))
Type = SpriteFrameType.Indexed8;
else if (colorType == PngColorType.Color)
Type = SpriteFrameType.Rgb24;
var compression = buffer[10];
Data = new byte[Width * Height * PixelStride];
// filter = buffer[11]
var interlace = buffer[12];
var compression = ms.ReadUInt8();
ms.ReadUInt8(); // filter
var interlace = ms.ReadUInt8();
if (compression != 0)
throw new InvalidDataException("Compression method not supported");
@@ -170,156 +87,73 @@ namespace OpenRA.FileFormats
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:
case "PLTE":
{
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
Palette = new Color[length / 3];
for (var i = 0; i < Palette.Length; i++)
{
s.ReadBytes(buffer.AsSpan(0, length));
for (var i = 0; i < count; i++)
{
var offset = i * 3;
Palette[i] = Color.FromArgb(buffer[offset], buffer[offset + 1], buffer[offset + 2]);
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
var r = ms.ReadUInt8(); var g = ms.ReadUInt8(); var b = ms.ReadUInt8();
Palette[i] = Color.FromArgb(r, g, b);
}
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
break;
}
case ChunkTRNS:
case "tRNS":
{
if (Palette == null)
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
{
s.ReadBytes(buffer.AsSpan(0, length));
for (var i = 0; i < length && i < Palette.Length; i++)
Palette[i] = Color.FromArgb(buffer[i], Palette[i]);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
for (var i = 0; i < length; i++)
Palette[i] = Color.FromArgb(ms.ReadUInt8(), Palette[i]);
// 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:
case "IDAT":
{
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
{
var span = buffer.AsSpan(0, length);
s.ReadBytes(span);
data.AddRange(content);
// Find Null Terminator for ASCIIZ (Keyword).
var nullIndex = span.IndexOf((byte)0);
if (nullIndex == -1)
return;
var key = Encoding.ASCII.GetString(span[..nullIndex]);
var value = Encoding.ASCII.GetString(span[(nullIndex + 1)..]);
EmbeddedData[key] = value;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
break;
}
case ChunkIEND:
case "tEXt":
{
if (Type == SpriteFrameType.Indexed8 && Palette == null)
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
var key = ms.ReadASCIIZ();
EmbeddedData.Add(key, ms.ReadASCII(length - key.Length - 1));
// 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)
case "IEND":
{
using (var ns = new MemoryStream(data.ToArray()))
{
using (var ds = new InflaterInputStream(ns))
{
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
{
Span<byte> prevLine = new byte[rowStride];
for (var y = 0; y < Height; y++)
{
var filterByte = ds.ReadByte();
if (filterByte == -1)
break;
var filter = (PngFilter)filterByte;
var filter = (PngFilter)ds.ReadUInt8();
ds.ReadBytes(Data, y * rowStride, sourceRowStride);
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;
var mask = 0xFF >> (8 - bitDepth);
for (var i = sourceRowStride - 1; i >= 0; i--)
{
var packed = line[i];
@@ -327,7 +161,7 @@ namespace OpenRA.FileFormats
{
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);
line[dest] = (byte)(packed >> (8 - (j + 1) * bitDepth) & mask);
}
}
}
@@ -337,23 +171,23 @@ namespace OpenRA.FileFormats
case PngFilter.None:
break;
case PngFilter.Sub:
for (var i = pxStride; i < line.Length; i++)
for (var i = pxStride; i < rowStride; i++)
line[i] += line[i - pxStride];
break;
case PngFilter.Up:
for (var i = 0; i < line.Length; i++)
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 < line.Length; 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 < line.Length; i++)
for (var i = pxStride; i < rowStride; i++)
line[i] += Paeth(line[i - pxStride], prevLine[i], prevLine[i - pxStride]);
break;
default:
@@ -363,15 +197,6 @@ namespace OpenRA.FileFormats
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)
@@ -385,6 +210,17 @@ namespace OpenRA.FileFormats
(pb <= pc) ? b : c;
}
}
}
if (Type == SpriteFrameType.Indexed8 && Palette == null)
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
return;
}
}
}
}
}
public Png(byte[] data, SpriteFrameType type, int width, int height, Color[] palette = null,
Dictionary<string, string> embeddedData = null)
@@ -420,27 +256,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();
for (var i = 0; i < width * height; i++)
{
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)
{
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 + 3] = data[stride * i + 3];
}
break;
@@ -457,8 +280,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;
}
@@ -481,29 +303,24 @@ 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.Write(typeBytes);
ReadOnlySpan<byte> data = dataSegment.AsSpan(0, (int)input.Length);
output.Write(header);
var data = input.ReadAllBytes();
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())
{
@@ -522,7 +339,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 +355,7 @@ namespace OpenRA.FileFormats
alphaPalette |= c.A > 0;
}
WritePngChunk(output, ChunkPLTE, palette);
WritePngChunk(output, "PLTE", palette);
}
}
@@ -549,13 +366,13 @@ 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, new Deflater(Deflater.BEST_COMPRESSION)))
{
var rowStride = Width * PixelStride;
for (var y = 0; y < Height; y++)
@@ -565,9 +382,12 @@ namespace OpenRA.FileFormats
compressed.WriteByte(FilterType);
compressed.Write(Data, y * rowStride, rowStride);
}
}
WritePngChunk(output, ChunkIDAT, data);
compressed.Flush();
compressed.Finish();
WritePngChunk(output, "IDAT", data);
}
}
foreach (var kv in EmbeddedData)
@@ -575,18 +395,18 @@ namespace OpenRA.FileFormats
using (var text = new MemoryStream())
{
text.Write(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
WritePngChunk(output, ChunkTEXT, text);
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;
}

View File

@@ -29,16 +29,16 @@ namespace OpenRA.FileSystem
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)
{
@@ -178,7 +178,7 @@ 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()

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);
}
}

View File

@@ -98,11 +98,11 @@ namespace OpenRA.FileSystem
{
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)))
{
@@ -120,26 +120,8 @@ namespace OpenRA.FileSystem
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());
}

View File

@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using Linguini.Bundle;
@@ -49,58 +48,23 @@ namespace OpenRA
}
}
[AttributeUsage(AttributeTargets.Class)]
public sealed class IncludeStaticFluentReferencesAttribute : Attribute
{
public readonly Type[] Types;
public IncludeStaticFluentReferencesAttribute(params Type[] types)
{
Types = types;
}
}
[AttributeUsage(AttributeTargets.Class)]
public sealed class IncludeChromeLogicArgsFluentReferencesAttribute : Attribute
{
public readonly string[] MethodNames;
public IncludeChromeLogicArgsFluentReferencesAttribute(params string[] methodNames)
{
MethodNames = methodNames;
}
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class IncludeFluentReferencesAttribute : Attribute
{
public readonly LintDictionaryReference DictionaryReference;
public IncludeFluentReferencesAttribute() { }
public IncludeFluentReferencesAttribute(LintDictionaryReference dictionaryReference = LintDictionaryReference.None)
{
DictionaryReference = dictionaryReference;
}
}
public class FluentBundle
{
readonly Linguini.Bundle.FluentBundle bundle;
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem)
public FluentBundle(string culture, 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)
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, string text)
: this(culture, paths, fileSystem, text, error => Log.Write("debug", error.Message)) { }
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem, Action<ParseError> onError)
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, Action<ParseError> onError)
: this(culture, paths, fileSystem, null, onError) { }
public FluentBundle(string culture, string text, Action<ParseError> onError)
: this(culture, default, null, text, onError) { }
: this(culture, null, null, text, onError) { }
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem, string text, Action<ParseError> onError)
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, string text, Action<ParseError> onError)
{
bundle = LinguiniBuilder.Builder()
.CultureInfo(new CultureInfo(culture))
@@ -147,7 +111,8 @@ namespace OpenRA
public bool TryGetMessage(string key, out string value, object[] args = null)
{
ArgumentNullException.ThrowIfNull(key);
if (key == null)
throw new ArgumentNullException(nameof(key));
try
{
@@ -164,7 +129,7 @@ namespace OpenRA
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 = [];
fluentArgs = new Dictionary<string, IFluentType>();
for (var i = 0; i < args.Length; i += 2)
{
var argKey = args[i] as string;

View File

@@ -10,7 +10,6 @@
#endregion
using System;
using System.Collections.Immutable;
using System.Text;
using OpenRA.FileSystem;
@@ -23,16 +22,16 @@ namespace OpenRA
static FluentBundle modFluentBundle;
static FluentBundle mapFluentBundle;
public static void Initialize(Manifest manifest, IReadOnlyFileSystem fileSystem)
public static void Initialize(ModData modData, IReadOnlyFileSystem fileSystem)
{
lock (SyncObject)
{
modFluentBundle = new FluentBundle(manifest.FluentCulture, manifest.FluentMessages, fileSystem);
modFluentBundle = new FluentBundle(modData.Manifest.FluentCulture, modData.Manifest.FluentMessages, fileSystem);
if (fileSystem is Map map && map.FluentMessageDefinitions != null)
{
var files = ImmutableArray<string>.Empty;
var files = Array.Empty<string>();
if (map.FluentMessageDefinitions.Value != null)
files = FieldLoader.GetValue<ImmutableArray<string>>("value", map.FluentMessageDefinitions.Value);
files = FieldLoader.GetValue<string[]>("value", map.FluentMessageDefinitions.Value);
string text = null;
if (map.FluentMessageDefinitions.Nodes.Length > 0)
@@ -45,7 +44,7 @@ namespace OpenRA
text = builder.ToString();
}
mapFluentBundle = new FluentBundle(manifest.FluentCulture, files, fileSystem, text);
mapFluentBundle = new FluentBundle(modData.Manifest.FluentCulture, files, fileSystem, text);
}
}
}

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,7 +27,6 @@ using OpenRA.Widgets;
namespace OpenRA
{
[IncludeStaticFluentReferences(typeof(Server.Server), typeof(Player), typeof(UnitOrders), typeof(OrderManager))]
public static class Game
{
[FluentReference("filename")]
@@ -168,7 +166,6 @@ namespace OpenRA
{
return ModData.WidgetLoader.LoadWidget(new WidgetArgs(args)
{
{ "modData", ModData },
{ "world", world },
{ "orderManager", OrderManager },
{ "worldRenderer", worldRenderer },
@@ -184,16 +181,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 +190,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;
@@ -372,7 +344,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 +359,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)
{
@@ -439,7 +369,7 @@ namespace OpenRA
try
{
var platform = CreatePlatform(p);
Renderer = new Renderer(platform, Settings.Graphics, manifest.RendererConstants.VertexBatchSize);
Renderer = new Renderer(platform, Settings.Graphics);
Sound = new Sound(platform, Settings.Sound);
break;
@@ -455,23 +385,67 @@ 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.Version})");
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
ExternalMods = new ExternalMods();
if (modID != null && Mods.TryGetValue(modID, out _))
{
var launchPath = args.GetValue("Engine.LaunchPath", null);
var launchArgs = new List<string>();
// Sanitize input from platform-specific launchers
// Process.Start requires paths to not be quoted, even if they contain spaces
if (launchPath != null && launchPath[0] == '"' && launchPath.Last() == '"')
launchPath = launchPath[1..^1];
// Metadata registration requires an explicit launch path
if (launchPath != null)
ExternalMods.Register(Mods[modID], launchPath, launchArgs, ModRegistration.User);
ExternalMods.ClearInvalidRegistrations(ModRegistration.User);
}
Console.WriteLine("External mods:");
foreach (var mod in ExternalMods)
Console.WriteLine($"\t{mod.Key} ({mod.Value.Version})");
InitializeMod(modID, args);
}
public static IPlatform CreatePlatform(string platformName)
{
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + platformName + ".dll");
#if NET5_0_OR_GREATER
var loader = new AssemblyLoader(rendererPath);
var platformType = loader.LoadDefaultAssembly().GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
#else
// NOTE: This is currently the only use of System.Reflection in this file, so would give an unused using error if we import it above
var assembly = System.Reflection.Assembly.LoadFile(rendererPath);
var platformType = assembly.GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
#endif
if (platformType == null)
throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation.");
return (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null);
}
public static void InitializeMod(Manifest manifest, Arguments args)
public static void InitializeMod(string mod, Arguments args)
{
// Clear static state if we have switched mods
LobbyInfoChanged = () => { };
@@ -495,25 +469,34 @@ 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, ModData.Manifest.CursorSheetSize);
var metadata = ModData.Manifest.Metadata;
if (!string.IsNullOrEmpty(metadata.WindowTitleTranslated))
@@ -531,16 +514,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()
@@ -614,7 +591,7 @@ 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);
@@ -661,7 +638,7 @@ namespace OpenRA
world.Tick();
PerfHistory.Tick(!world.Paused);
PerfHistory.Tick();
}
// Wait until we have done our first world Tick before TickRendering
@@ -725,7 +702,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 +717,15 @@ namespace OpenRA
Ui.Draw();
if (ModData != null && ModData.CursorProvider != null)
{
if (HideCursor)
Cursor?.SetCursor(null);
Cursor.SetCursor(null);
else
{
Cursor?.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default");
Cursor?.Render(Renderer);
Cursor.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default");
Cursor.Render(Renderer);
}
}
}
@@ -759,13 +739,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()
@@ -862,13 +841,8 @@ 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;
// Pick the minimum allowed FPS (the lower between 'minReplayFPS'
@@ -879,13 +853,12 @@ namespace OpenRA
var maxRenderInterval = Math.Max(1000 / MinReplayFps, renderInterval);
forcedNextRender = now + maxRenderInterval;
RenderTick();
renderBeforeNextTick = false;
}
}
else
{
// Simulate a render tick if it was time to render but we skip actually rendering
if (isTimeToRender || forceRender)
if (Renderer.WindowIsSuspended && isTimeToRender)
{
// Make sure that nextUpdate is set to a proper minimum interval
nextRender = now + renderInterval;
@@ -896,12 +869,6 @@ namespace OpenRA
// Ensure that we still logic tick despite not rendering
renderBeforeNextTick = false;
}
else
{
// Avoid busy wait.
Thread.Sleep((int)(nextRender - now));
}
}
}
else
Thread.Sleep((int)(nextUpdate - now));
@@ -980,8 +947,7 @@ namespace OpenRA
{
Name = "Skirmish Game",
Map = map,
AdvertiseOnline = false,
AdvertiseOnLocalNetwork = !isSkirmish
AdvertiseOnline = false
};
// Always connect to local games using the same loopback connection
@@ -1019,7 +985,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.PackageName) == 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;
@@ -40,32 +39,17 @@ 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 = [];
}
public MapPreview GetMapPreview(ModData modData)
{
var preview = modData.MapCache[MapUid];
if (preview.Status != MapStatus.Available && MapGenerationArgs != null)
{
preview.UpdateFromGenerationArgs(MapGenerationArgs);
preview.Generate();
}
return preview;
Players = new List<Player>();
playersByRuntime = new Dictionary<OpenRA.Player, Player>();
}
public static GameInformation Deserialize(string data, string path)
@@ -88,10 +72,6 @@ namespace OpenRA
case "Player":
info.Players.Add(FieldLoader.Load<Player>(node.Value));
break;
case "MapGenerationArgs":
info.MapGenerationArgs = FieldLoader.Load<MapGenerationArgs>(node.Value);
break;
}
}
@@ -114,18 +94,17 @@ namespace OpenRA
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)

View File

@@ -32,7 +32,7 @@ namespace OpenRA
/// You can remove inherited traits by adding a - in front of them as in -TraitName: to inherit everything, but this trait.
/// </summary>
public readonly string Name;
readonly TypeDictionary traits = [];
readonly TypeDictionary traits = new();
TraitInfo[] constructOrderCache = null;
public ActorInfo(ObjectCreator creator, string name, MiniYaml node)

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;
@@ -269,7 +268,7 @@ namespace OpenRA
if (mapRules.Value != null)
{
var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", mapRules.Value);
var mapFiles = FieldLoader.GetValue<string[]>("value", mapRules.Value);
foreach (var f in mapFiles)
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
return true;

View File

@@ -10,38 +10,37 @@
#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 ret = new Dictionary<string, SoundPool>();
var classifiction = y.NodeWithKey(key);
var ret = new Dictionary<string, SoundPool>(classifiction.Value.Nodes.Length);
foreach (var t in classifiction.Value.Nodes)
{
var volumeModifier = 1f;
@@ -54,12 +53,12 @@ namespace OpenRA.GameRules
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,7 +139,7 @@ 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 = content.WithNodes(MiniYaml.Merge(new IReadOnlyCollection<MiniYamlNode>[] { content.Nodes }));
FieldLoader.Load(this, content);
}
@@ -171,7 +170,7 @@ namespace OpenRA.GameRules
retList.Add(ret);
}
return retList.ToImmutableArray();
return retList;
}
public bool IsValidTarget(BitSet<TargetableType> targetTypes)

View File

@@ -10,7 +10,6 @@
#endregion
using System.Collections.Generic;
using OpenRA.Traits;
namespace OpenRA
{
@@ -32,7 +31,6 @@ namespace OpenRA
[FieldLoader.Require]
public readonly string DefaultSpeed;
[IncludeFluentReferences(LintDictionaryReference.Values)]
[FieldLoader.LoadUsing(nameof(LoadSpeeds))]
public readonly Dictionary<string, GameSpeed> Speeds;

View File

@@ -10,7 +10,6 @@
#endregion
using System;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.Primitives;
using OpenRA.Support;
@@ -74,10 +73,10 @@ namespace OpenRA.Graphics
shadow, pos, offset - new WVec(0, 0, height), CurrentSequence.ShadowZOffset + zOffset + height, 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 +92,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 +250,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,11 +71,11 @@ 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
@@ -172,7 +170,7 @@ namespace OpenRA.Graphics
var sheetDensity = SheetForCollection(collection);
if (cachedCollection == null)
{
cachedCollection = [];
cachedCollection = new Dictionary<string, Sprite>();
cachedSprites.Add(collectionName, cachedCollection);
}
@@ -242,11 +240,11 @@ namespace OpenRA.Graphics
// PERF: We don't need to search for images if there are no definitions.
// PERF: It's more efficient to send an empty array rather than an array of 9 nulls.
if (collection.Regions.Count == 0)
return [];
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 +254,7 @@ namespace OpenRA.Graphics
TryGetImage(collectionName, "corner-bl"),
TryGetImage(collectionName, "border-b"),
TryGetImage(collectionName, "corner-br")
];
};
}
cachedPanelSprites.Add(collectionName, sprites);

View File

@@ -13,7 +13,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Graphics
{
@@ -30,8 +29,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 +39,19 @@ namespace OpenRA.Graphics
readonly bool hardwareCursorsDisabled = false;
bool hardwareCursorsDoubled = false;
public CursorManager(ModData modData)
public CursorManager(CursorProvider cursorProvider, int cursorSheetSize)
{
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);
sheetBuilder = new SheetBuilder(SheetType.BGRA, cursorSheetSize);
// Sort the cursors for better packing onto the sheet.
foreach (var kv in modData.Cursors)
foreach (var kv in cursorProvider.Cursors
.OrderBy(kvp => kvp.Value.Frames.Max(f => f.Size.Height)))
{
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 +82,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,13 +94,9 @@ namespace OpenRA.Graphics
cursors.Add(kv.Key, c);
}
// Allow the utility to create a cursor manager.
if (Game.Renderer != null)
{
CreateOrUpdateHardwareCursors();
Update();
}
}
void CreateOrUpdateHardwareCursors()
{
@@ -149,7 +128,7 @@ namespace OpenRA.Graphics
}
}
SheetBuilder.Current?.ReleaseBuffer();
sheetBuilder.Current.ReleaseBuffer();
hardwareCursorsDoubled = graphicSettings.CursorDouble;
}
@@ -254,7 +233,7 @@ namespace OpenRA.Graphics
var height = frame.Size.Height;
if (width == 0 || height == 0)
return [];
return Array.Empty<byte>();
var data = new byte[4 * width * height];
unsafe
@@ -300,18 +279,21 @@ namespace OpenRA.Graphics
{
for (var i = 0; i < c.Cursors.Length; i++)
{
c.Cursors[i]?.Dispose();
if (c.Cursors[i] != null)
{
c.Cursors[i].Dispose();
c.Cursors[i] = null;
}
}
}
}
public void Dispose()
{
ClearHardwareCursors();
cursors.Clear();
SheetBuilder.Dispose();
sheetBuilder.Dispose();
}
}
}

View File

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

View File

@@ -9,26 +9,50 @@
*/
#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.ParseInt32Invariant(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.ParseInt32Invariant(yaml.Value);
else if (d.TryGetValue("End", out yaml))
Length = Exts.ParseInt32Invariant(yaml.Value) - Start;
else
Length = 1;
Frames = Frames.Take(Length).ToArray();
if (Start > cursorSprites.Length)
throw new YamlException($"Cursor {name}: {nameof(Start)} is greater than the length of the sprite sequence.");
if (Length > cursorSprites.Length)
throw new YamlException($"Cursor {name}: {nameof(Length)} is greater than the length of the sprite sequence.");
if (d.TryGetValue("X", out yaml))
{
Exts.TryParseInt32Invariant(yaml.Value, out var x);
Hotspot = Hotspot.WithX(x);
@@ -39,14 +63,6 @@ namespace OpenRA.Graphics
Exts.TryParseInt32Invariant(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()
{

View File

@@ -31,13 +31,25 @@ namespace OpenRA.Graphics
public interface IModelWidget
{
string Palette { get; }
float Scale { get; }
void Setup(Func<bool> isVisible, Func<string> getPalette, Func<string> getPlayerPalette,
public string Palette { get; }
public float Scale { get; }
public void Setup(Func<bool> isVisible, Func<string> getPalette, Func<string> getPlayerPalette,
Func<float> getScale, Func<IModel> getVoxel, Func<WRot> getRotation);
}
public readonly record struct ModelRenderData(int Start, int Count, Sheet Sheet);
public readonly struct ModelRenderData
{
public readonly int Start;
public readonly int Count;
public readonly Sheet Sheet;
public ModelRenderData(int start, int count, Sheet sheet)
{
Start = start;
Count = count;
Sheet = sheet;
}
}
public interface IModelCacheInfo : ITraitInfoInterface { }

View File

@@ -43,11 +43,11 @@ namespace OpenRA.Graphics
: base("model")
{ }
public override ShaderVertexAttribute[] Attributes { get; } =
[
public override ShaderVertexAttribute[] Attributes { get; } = new[]
{
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
new ShaderVertexAttribute("aVertexTexMetadata", ShaderVertexAttributeType.Float, 2, 28),
];
};
}
}

View File

@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using OpenRA.Primitives;
@@ -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++)

View File

@@ -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,8 +83,7 @@ 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;
IVertexBuffer<T> CreateVertexBuffer<T>(int size) where T : struct;
T[] CreateVertices<T>(int size) where T : struct;
IIndexBuffer CreateIndexBuffer(uint[] indices);
ITexture CreateTexture();
@@ -136,7 +134,7 @@ namespace OpenRA
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();
@@ -181,6 +179,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

@@ -14,20 +14,39 @@ using System.Runtime.InteropServices;
namespace OpenRA.Graphics
{
[StructLayout(LayoutKind.Sequential)]
public readonly record struct RenderPostProcessPassVertex(float X, float Y);
public readonly struct RenderPostProcessPassVertex
{
public readonly float X, Y;
public RenderPostProcessPassVertex(float x, float y)
{
X = x; Y = y;
}
}
[StructLayout(LayoutKind.Sequential)]
public readonly record struct RenderPostProcessPassTexturedVertex(float X, float Y, float S, float T);
public readonly struct RenderPostProcessPassTexturedVertex
{
// 3d position
public readonly float X, Y;
public readonly float S, T;
public RenderPostProcessPassTexturedVertex(float x, float y, float s, float t)
{
X = x; Y = y;
S = s; T = t;
}
}
public sealed class RenderPostProcessPassShaderBindings : ShaderBindings
{
public RenderPostProcessPassShaderBindings(string name)
: base("postprocess", "postprocess_" + name) { }
public override ShaderVertexAttribute[] Attributes { get; } =
[
public override ShaderVertexAttribute[] Attributes { get; } = new[]
{
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0)
];
};
}
public sealed class RenderPostProcessPassTexturedShaderBindings : ShaderBindings
@@ -36,10 +55,10 @@ namespace OpenRA.Graphics
: base("postprocess_textured", "postprocess_textured_" + name)
{ }
public override ShaderVertexAttribute[] Attributes { get; } =
[
public override ShaderVertexAttribute[] Attributes { get; } = new[]
{
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0),
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 2, 8),
];
};
}
}

View File

@@ -191,7 +191,7 @@ 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 FillRect(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);
}
@@ -97,7 +97,7 @@ namespace OpenRA.Graphics
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix))
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

@@ -23,7 +23,21 @@ namespace OpenRA.Graphics
UInt = 0x1405 // GL_UNSIGNED_INT
}
public readonly record struct ShaderVertexAttribute(string Name, ShaderVertexAttributeType Type, int Components, int Offset);
public readonly struct ShaderVertexAttribute
{
public readonly string Name;
public readonly ShaderVertexAttributeType Type;
public readonly int Components;
public readonly int Offset;
public ShaderVertexAttribute(string name, ShaderVertexAttributeType type, int components, int offset)
{
Name = name;
Type = type;
Components = components;
Offset = offset;
}
}
public abstract class ShaderBindings : IShaderBindings
{

View File

@@ -149,7 +149,7 @@ namespace OpenRA.Graphics
var buffer = data;
ReleaseBuffer();
// We aren't committing data to the GPU, so let's not delete our data.
// We aren't commiting data to the GPU, so let's not delete our data.
if (Game.Renderer == null)
return false;

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,6 +76,8 @@ namespace OpenRA.Graphics
{
CurrentChannel = t == SheetType.Indexed ? TextureChannel.Red : TextureChannel.RGBA;
Type = t;
Current = allocateSheet();
sheets.Add(Current);
this.allocateSheet = allocateSheet;
this.margin = margin;
}
@@ -80,12 +86,6 @@ namespace OpenRA.Graphics
public Sprite Add(byte[] src, SpriteFrameType type, Size size, bool premultiplied = false) { return Add(src, type, size, 0, float3.Zero, premultiplied); }
public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset, bool premultiplied = false)
{
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);
@@ -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);

View File

@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using OpenRA.FileSystem;
@@ -28,12 +27,12 @@ namespace OpenRA.Graphics
readonly Dictionary<
int,
(ImmutableArray<int> Frames, MiniYamlNode.SourceLocation Location, AdjustFrame AdjustFrame, bool Premultiplied)> spriteReservations = [];
readonly Dictionary<string, List<int>> reservationsByFilename = [];
(int[] Frames, MiniYamlNode.SourceLocation Location, AdjustFrame AdjustFrame, bool Premultiplied)> spriteReservations = new();
readonly Dictionary<string, List<int>> reservationsByFilename = new();
readonly Dictionary<int, Sprite[]> resolvedSprites = [];
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;
@@ -50,12 +49,12 @@ namespace OpenRA.Graphics
this.loaders = loaders;
}
public int ReserveSprites(string filename, ImmutableArray<int> frames, MiniYamlNode.SourceLocation location,
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location,
AdjustFrame adjustFrame = null, bool premultiplied = false)
{
var token = nextReservationToken++;
spriteReservations[token] = (frames, location, adjustFrame, premultiplied);
reservationsByFilename.GetOrAdd(filename, _ => []).Add(token);
spriteReservations[token] = (frames?.ToArray(), location, adjustFrame, premultiplied);
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
return token;
}
@@ -104,8 +103,8 @@ namespace OpenRA.Graphics
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 frames = rs.Frames ?? Enumerable.Range(0, loadedFrames.Length);
var total = rs.Frames?.Length ?? loadedFrames.Length;
var j = 0;
foreach (var i in frames)
@@ -156,7 +155,7 @@ namespace OpenRA.Graphics
}
foreach (var sb in SheetBuilders.Values)
sb.Current?.ReleaseBuffer();
sb.Current.ReleaseBuffer();
}
public Sprite[] ResolveSprites(int token)

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

@@ -19,7 +19,7 @@ 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 = [];
static readonly ConditionalWeakTable<World, IndexBufferRc> IndexBuffers = new();
readonly IndexBufferRc indexBufferWrapper;
public readonly BlendMode BlendMode;
@@ -30,7 +30,7 @@ namespace OpenRA.Graphics
readonly IVertexBuffer<Vertex> vertexBuffer;
readonly Vertex[] vertices;
readonly bool[] ignoreTint;
readonly HashSet<int> dirtyRows = [];
readonly HashSet<int> dirtyRows = new();
readonly int indexRowStride;
readonly int vertexRowStride;
readonly bool restrictToBounds;
@@ -49,23 +49,23 @@ namespace OpenRA.Graphics
BlendMode = blendMode;
map = world.Map;
vertexRowStride = 4 * map.MapSize.Width;
vertices = new Vertex[vertexRowStride * map.MapSize.Height];
vertexBuffer = Game.Renderer.Context.CreateEmptyVertexBuffer<Vertex>(vertices.Length);
vertexRowStride = 4 * map.MapSize.X;
vertices = new Vertex[vertexRowStride * map.MapSize.Y];
vertexBuffer = Game.Renderer.Context.CreateVertexBuffer<Vertex>(vertices.Length);
indexRowStride = 6 * map.MapSize.Width;
indexRowStride = 6 * map.MapSize.X;
lock (IndexBuffers)
{
indexBufferWrapper = IndexBuffers.GetValue(world, world => new IndexBufferRc(world));
indexBufferWrapper.AddRef();
}
palettes = new PaletteReference[map.MapSize.Width * map.MapSize.Height];
palettes = new PaletteReference[map.MapSize.X * map.MapSize.Y];
wr.PaletteInvalidated += UpdatePaletteIndices;
if (wr.TerrainLighting != null)
{
ignoreTint = new bool[vertexRowStride * map.MapSize.Height];
ignoreTint = new bool[vertexRowStride * map.MapSize.Y];
wr.TerrainLighting.CellChanged += UpdateTint;
}
}
@@ -80,7 +80,7 @@ namespace OpenRA.Graphics
vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, c, v.R, v.G, v.B, v.A);
}
for (var row = 0; row < map.MapSize.Height; row++)
for (var row = 0; row < map.MapSize.Y; row++)
dirtyRows.Add(row);
}
@@ -193,7 +193,7 @@ namespace OpenRA.Graphics
var offset = vertexRowStride * uv.V + 4 * uv.U;
Util.FastCreateQuad(vertices, pos, sprite, samplers, palette?.TextureIndex ?? 0, offset, scale * sprite.Size, alpha * float3.Ones, alpha);
palettes[uv.V * map.MapSize.Width + uv.U] = palette;
palettes[uv.V * map.MapSize.X + uv.U] = palette;
if (worldRenderer.TerrainLighting != null)
{
@@ -209,8 +209,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();
@@ -250,8 +250,7 @@ namespace OpenRA.Graphics
public IndexBufferRc(World world)
{
Buffer = Game.Renderer.Context.CreateIndexBuffer(
Util.CreateQuadIndices(world.Map.MapSize.Width * world.Map.MapSize.Height));
Buffer = Game.Renderer.Context.CreateIndexBuffer(Util.CreateQuadIndices(world.Map.MapSize.X * world.Map.MapSize.Y));
}
public void AddRef() { count++; }

View File

@@ -19,12 +19,12 @@ 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];
ReadOnlySpan<uint> cornerVertexMap = stackalloc uint[] { 0, 1, 2, 2, 3, 0 };
for (var i = 0; i < indices.Length; i++)
indices[i] = cornerVertexMap[i % 6] + (uint)(4 * (i / 6));
@@ -277,13 +277,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>

View File

@@ -53,12 +53,12 @@ namespace OpenRA.Graphics
: base("combined")
{ }
public override ShaderVertexAttribute[] Attributes { get; } =
[
public override ShaderVertexAttribute[] Attributes { get; } = new[]
{
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
new ShaderVertexAttribute("aVertexAttributes", ShaderVertexAttributeType.UInt, 1, 28),
new ShaderVertexAttribute("aVertexTint", ShaderVertexAttributeType.Float, 4, 32)
];
};
}
}

View File

@@ -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,17 @@ 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);
Game.Renderer.NativeResolution.Height * viewportSizes.DefaultScale / viewportSizes.MaxZoomWindowHeight);
if (unlockMinZoom)
{
@@ -258,7 +243,7 @@ namespace OpenRA.Graphics
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 +256,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);
}
@@ -323,9 +308,9 @@ namespace OpenRA.Graphics
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)
{
@@ -345,17 +330,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,33 +31,32 @@ 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 List<IRenderable> renderablesBuffer = new();
readonly IRenderer[] renderers;
readonly IRenderPostProcessPass[] postProcessPasses;
long[] renderablesKeysBuffer = [];
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>())
@@ -72,7 +70,7 @@ namespace OpenRA.Graphics
renderers = world.WorldActor.TraitsImplementing<IRenderer>().ToArray();
terrainRenderer = world.WorldActor.TraitOrDefault<IRenderTerrain>();
debugVis = Exts.Lazy(world.WorldActor.TraitOrDefault<DebugVisualizations>);
debugVis = Exts.Lazy(() => world.WorldActor.TraitOrDefault<DebugVisualizations>());
postProcessPasses = world.WorldActor.TraitsImplementing<IRenderPostProcessPass>().ToArray();
}
@@ -159,14 +157,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.
@@ -380,8 +371,6 @@ namespace OpenRA.Graphics
}
}
ApplyPostProcessing(PostProcessPassType.AfterAnnotations);
Game.Renderer.Flush();
preparedRenderables.Clear();
@@ -395,22 +384,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 +429,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,8 +9,7 @@
*/
#endregion
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Linq;
namespace OpenRA
@@ -25,10 +24,10 @@ namespace OpenRA
[FluentReference]
public readonly string Description = "";
public readonly FrozenSet<string> Types = FrozenSet<string>.Empty;
public readonly HashSet<string> Types = new();
[FluentReference]
public readonly FrozenSet<string> Contexts = FrozenSet<string>.Empty;
public readonly HashSet<string> Contexts = new();
public readonly bool Readonly = false;
public bool HasDuplicates { get; internal set; }
@@ -46,11 +45,11 @@ namespace OpenRA
Description = descriptionYaml.Value;
if (nodeDict.TryGetValue("Types", out var typesYaml))
Types = FieldLoader.GetValue<FrozenSet<string>>("Types", typesYaml.Value);
Types = FieldLoader.GetValue<HashSet<string>>("Types", typesYaml.Value);
if (nodeDict.TryGetValue("Contexts", out var contextYaml))
Contexts = FieldLoader.GetValue<ImmutableArray<string>>("Contexts", contextYaml.Value)
.Select(c => ContextFluentPrefix + "." + c).ToFrozenSet();
Contexts = FieldLoader.GetValue<HashSet<string>>("Contexts", contextYaml.Value)
.Select(c => ContextFluentPrefix + "." + c).ToHashSet();
if (nodeDict.TryGetValue("Platform", out var platformYaml))
{

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))
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))
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;

View File

@@ -23,7 +23,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
@@ -47,7 +65,7 @@ namespace OpenRA
public static class ModifiersExts
{
[FluentReference]
const string Cmd = "keycode-modifier.cmd";
public const string Cmd = "keycode-modifier.cmd";
[FluentReference(Traits.LintDictionaryReference.Values)]
public static readonly IReadOnlyDictionary<Modifiers, string> ModifierFluentKeys = new Dictionary<Modifiers, string>()

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

@@ -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,27 +9,51 @@
*/
#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 interface IGlobalModData { }
public sealed class TerrainFormat : IGlobalModData
{
public readonly string Type;
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
public TerrainFormat(MiniYaml yaml)
{
Type = yaml.Value;
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
}
}
public sealed class SpriteSequenceFormat : IGlobalModData
{
public readonly string Type;
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
public SpriteSequenceFormat(MiniYaml yaml)
{
Type = yaml.Value;
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
}
}
public class ModMetadata
{
// FieldLoader used here, must matching naming in YAML.
#pragma warning disable IDE1006 // Naming Styles
[FluentReference]
public readonly string Title;
readonly string Title;
public readonly string Version;
public readonly string Website;
public readonly string WebIcon32;
[FluentReference(optional: true)]
public readonly string WindowTitle;
[FluentReference]
readonly string WindowTitle;
public readonly bool Hidden;
#pragma warning restore IDE1006 // Naming Styles
@@ -37,56 +61,48 @@ namespace OpenRA
public string WindowTitleTranslated => WindowTitle != null ? FluentProvider.GetMessage(WindowTitle) : null;
}
public class RendererConstants
{
public readonly int FontSheetSize = 512;
public readonly int CursorSheetSize = 512;
public readonly int MapPreviewSheetSize = 2048;
public readonly int SequenceBgraSheetSize = 2048;
public readonly int SequenceIndexedSheetSize = 2048;
public readonly int VertexBatchSize = 8192;
}
/// <summary>Describes what is to be loaded in order to run a mod.</summary>
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,
ChromeMetrics, MapCompatibility, Missions, Hotkeys;
public readonly FrozenDictionary<string, string> MapFolders;
public readonly IReadOnlyDictionary<string, string> MapFolders;
public readonly MiniYaml FileSystem;
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[] Assemblies = Array.Empty<string>();
public readonly string[] SoundFormats = Array.Empty<string>();
public readonly string[] SpriteFormats = Array.Empty<string>();
public readonly string[] PackageFormats = Array.Empty<string>();
public readonly string[] VideoFormats = Array.Empty<string>();
public readonly int FontSheetSize = 512;
public readonly int CursorSheetSize = 512;
// TODO: This should be controlled by a user-selected translation bundle!
public readonly string FluentCulture = "en";
public readonly bool AllowUnusedFluentMessagesInExternalPackages = true;
static readonly FrozenSet<string> ReservedModuleNames = new HashSet<string>
readonly string[] reservedModuleNames =
{
"Include", "Metadata", "FileSystem", "MapFolders", "Rules",
"Sequences", "ModelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons",
"Voices", "Notifications", "Music", "FluentMessages", "TileSets", "ChromeMetrics", "Missions", "Hotkeys",
"ServerTraits", "LoadScreen", "DefaultOrderGenerator", "SupportsMapsFrom", "SoundFormats", "SpriteFormats", "VideoFormats",
"SpriteSequenceFormat", "TerrainFormat", "RequiresMods", "PackageFormats", "AllowUnusedFluentMessagesInExternalPackages", "RendererConstants"
}.ToFrozenSet();
"RequiresMods", "PackageFormats", "AllowUnusedFluentMessagesInExternalPackages", "FontSheetSize", "CursorSheetSize"
};
public readonly FrozenDictionary<string, MiniYaml> GlobalModData;
readonly TypeDictionary modules = new();
readonly Dictionary<string, MiniYaml> yaml;
bool customDataLoaded;
public Manifest(string modId, IReadOnlyPackage package)
{
@@ -94,7 +110,7 @@ namespace OpenRA
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"), $"{package.Name}:mod.yaml", stringPool: stringPool);
for (var i = nodes.Count - 1; i >= 0; i--)
{
if (nodes[i].Key != "Include")
@@ -111,7 +127,7 @@ namespace OpenRA
}
// 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"]);
@@ -148,59 +164,144 @@ 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);
Assemblies = FieldLoader.GetValue<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);
if (yaml.TryGetValue("SpriteSequenceFormat", out entry))
SpriteSequenceFormat = entry.Value;
if (yaml.TryGetValue("TerrainFormat", out entry))
TerrainFormat = entry.Value;
VideoFormats = FieldLoader.GetValue<string[]>("VideoFormats", entry.Value);
if (yaml.TryGetValue("AllowUnusedFluentMessagesInExternalPackages", out entry))
AllowUnusedFluentMessagesInExternalPackages =
FieldLoader.GetValue<bool>("AllowUnusedFluentMessagesInExternalPackages", entry.Value);
if (yaml.TryGetValue("RendererConstants", out entry))
RendererConstants = FieldLoader.Load<RendererConstants>(entry);
if (yaml.TryGetValue("FontSheetSize", out entry))
FontSheetSize = FieldLoader.GetValue<int>("FontSheetSize", entry.Value);
if (yaml.TryGetValue("CursorSheetSize", out entry))
CursorSheetSize = FieldLoader.GetValue<int>("CursorSheetSize", entry.Value);
}
public void LoadCustomData(ObjectCreator oc)
{
foreach (var kv in yaml)
{
if (reservedModuleNames.Contains(kv.Key))
continue;
var t = oc.FindType(kv.Key);
if (t == null || !typeof(IGlobalModData).IsAssignableFrom(t))
throw new InvalidDataException($"`{kv.Key}` is not a valid mod manifest entry.");
IGlobalModData module;
var ctor = t.GetConstructor(new[] { typeof(MiniYaml) });
if (ctor != null)
{
// Class has opted-in to DIY initialization
module = (IGlobalModData)ctor.Invoke(new object[] { kv.Value });
}
else
RendererConstants = new RendererConstants();
GlobalModData = yaml.Where(n => !ReservedModuleNames.Contains(n.Key))
.ToFrozenDictionary(n => n.Key, n => n.Value);
{
// Automatically load the child nodes using FieldLoader
module = oc.CreateObject<IGlobalModData>(kv.Key);
FieldLoader.Load(module, kv.Value);
}
static ImmutableArray<string> YamlList(Dictionary<string, MiniYaml> yaml, string key)
modules.Add(module);
}
customDataLoaded = true;
}
static string[] YamlList(Dictionary<string, MiniYaml> yaml, string key)
{
if (!yaml.TryGetValue(key, out var value))
return [];
return Array.Empty<string>();
return value.Nodes.Select(n => n.Key).ToImmutableArray();
return value.Nodes.Select(n => n.Key).ToArray();
}
static FrozenDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
static IReadOnlyDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
{
if (!yaml.TryGetValue(key, out var value))
return FrozenDictionary<string, string>.Empty;
return new Dictionary<string, string>();
return value.ToDictionary(my => my.Value).ToFrozenDictionary();
return value.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
{
// Automatically load the child nodes using FieldLoader
module = oc.CreateObject<IGlobalModData>(t.Name);
FieldLoader.Load(module, data);
}
return (T)module;
}
public void Dispose()
{
foreach (var module in modules)
{
var disposableModule = module as IDisposable;
disposableModule?.Dispose();
}
}
}
}

View File

@@ -216,8 +216,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,15 +70,15 @@ 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;
}
@@ -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

View File

@@ -9,7 +9,6 @@
*/
#endregion
using System;
using System.Collections;
using System.Collections.Generic;
@@ -90,31 +89,6 @@ namespace OpenRA
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

@@ -33,12 +33,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)
@@ -112,7 +112,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 +122,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);
}
}
@@ -158,7 +158,7 @@ namespace OpenRA
/// <summary>Defines the order of the fields in map.yaml.</summary>
static readonly MapField[] YamlFields =
[
{
new("MapFormat"),
new("RequiresMod"),
new("Title"),
@@ -179,7 +179,7 @@ namespace OpenRA
new("Voices", nameof(VoiceDefinitions), required: false),
new("Music", nameof(MusicDefinitions), required: false),
new("Notifications", nameof(NotificationDefinitions), required: false),
];
};
// Format versions
public int MapFormat { get; private set; }
@@ -193,13 +193,13 @@ 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 IReadOnlyCollection<MiniYamlNode> PlayerDefinitions = ImmutableArray<MiniYamlNode>.Empty;
public IReadOnlyCollection<MiniYamlNode> ActorDefinitions = ImmutableArray<MiniYamlNode>.Empty;
// Custom map yaml. Public for access by the map importers and lint checks
public MiniYaml RuleDefinitions;
@@ -211,7 +211,7 @@ namespace OpenRA
public MiniYaml MusicDefinitions;
public MiniYaml NotificationDefinitions;
public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = [];
public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = new();
// Generated data
public readonly MapGrid Grid;
@@ -283,7 +283,7 @@ namespace OpenRA
// 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 +321,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)
@@ -367,15 +368,16 @@ namespace OpenRA
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 = yaml.NodeWithKeyOrDefault("Players")?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty;
ActorDefinitions = yaml.NodeWithKeyOrDefault("Actors")?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty;
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 +385,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 +404,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 +418,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 +455,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 +516,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 +532,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,7 +607,7 @@ 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)
@@ -682,13 +684,13 @@ 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 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)heightsOffset);
@@ -697,9 +699,9 @@ namespace OpenRA
// Tile data
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 +712,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 +758,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>();
@@ -1042,7 +1044,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 +1064,7 @@ namespace OpenRA
InitializeCellProjection();
if (!inverseCellProjection.Contains(uv))
return [];
return new List<MPos>();
return inverseCellProjection[uv];
}
@@ -1087,15 +1089,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 +1145,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 +1153,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 +1168,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)

View File

@@ -14,13 +14,13 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using OpenRA.FileSystem;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Support;
using FS = OpenRA.FileSystem.FileSystem;
namespace OpenRA
{
@@ -28,27 +28,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 HashSet<string> StringPool { get; } = new();
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 +67,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,21 +82,20 @@ 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('~');
@@ -110,7 +110,7 @@ namespace OpenRA
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && !File.Exists(resolved))
Directory.CreateDirectory(resolved);
package = modFiles.OpenPackage(name);
package = modData.ModFiles.OpenPackage(name);
}
catch
{
@@ -121,38 +121,42 @@ 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);
// Freeing the package to save memory if there is a lot of Maps
previews[uid].DisposePackage();
if (oldMap != uid)
{
@@ -177,8 +181,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;
@@ -196,7 +204,7 @@ namespace OpenRA
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && (!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,7 +224,7 @@ 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;
}
@@ -231,37 +239,67 @@ namespace OpenRA
.ToList();
foreach (var uid in queryUids)
previews[uid].BeginRemoteSearch();
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null, null);
Task.Run(async () =>
{
var client = HttpClientFactory.Create();
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var buffer = new StringBuilder();
// Limit each query to 50 maps at a time to avoid request size limits
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
{
var result = await client.GetStreamAsync(url);
foreach (var kv in MiniYaml.FromStream(result, url, stringPool: stringPool))
previews[kv.Key].CompleteRemoteSearch(kv.Value, mapDetailsReceived);
await using (var resultStream = await client.GetStreamAsync(url))
{
using (var resultReader = new StreamReader(resultStream))
{
while (true)
{
var line = await resultReader.ReadLineAsync();
if (line == null || !line.StartsWith('\t'))
{
var yaml = MiniYaml.FromString(buffer.ToString(), url, stringPool: stringPool);
buffer.Clear();
foreach (var kv in yaml)
previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, modData.Manifest.MapCompatibility, mapDetailsReceived);
if (line == null)
break;
}
buffer.Append(line);
buffer.Append('\n');
}
}
}
foreach (var uid in batchUids)
{
var p = previews[uid];
if (p.Status != MapStatus.DownloadAvailable)
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null);
}
}
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];
if (p.Status == MapStatus.Searching)
p.CompleteRemoteSearch(null, mapQueryFailed);
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null);
mapQueryFailed?.Invoke(p);
}
}
}
}
@@ -328,9 +366,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);
Log.Write("debug", "MapCache.LoadAsyncInternal ended");
}

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.PackageName == 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 =
[
public readonly WVec[] SubCellOffsets =
{
new(0, 0, 0), // full cell - index 0
new(-299, -256, 0), // top left - index 1
new(256, -256, 0), // top right - index 2
new(0, 0, 0), // center - index 3
new(-299, 256, 0), // bottom left - index 4
new(256, 256, 0), // bottom right - index 5
];
};
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;
@@ -72,12 +69,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 +82,6 @@ namespace OpenRA
public MapClassification Class;
public MapVisibility Visibility;
public DateTime ModifiedDate;
public MapGenerationArgs GenerationArgs;
public MiniYaml RuleDefinitions;
public MiniYaml WeaponDefinitions;
@@ -117,7 +113,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");
@@ -132,9 +128,9 @@ namespace OpenRA
{
if (FluentMessageDefinitions != null)
{
var files = ImmutableArray<string>.Empty;
var files = Array.Empty<string>();
if (FluentMessageDefinitions.Value != null)
files = FieldLoader.GetValue<ImmutableArray<string>>("value", FluentMessageDefinitions.Value);
files = FieldLoader.GetValue<string[]>("value", FluentMessageDefinitions.Value);
string text = null;
if (FluentMessageDefinitions.Nodes.Length > 0)
@@ -160,8 +156,8 @@ 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
@@ -199,26 +195,22 @@ 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()
public string PackageName { get; private set; }
IReadOnlyPackage package;
public IReadOnlyPackage Package
{
if (package == null && parentPackage != null)
package = parentPackage.OpenPackage(Path, modData.ModFiles);
get
{
package ??= parentPackage.OpenPackage(PackageName, modData.ModFiles);
return package;
}
public Map ToMap()
{
LoadPackage();
using (new PerfTimer("Map"))
return new Map(modData, package);
private set => package = value;
}
IReadOnlyPackage parentPackage;
@@ -227,12 +219,12 @@ namespace OpenRA
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,8 +240,6 @@ 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; }
@@ -327,12 +317,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,28 +332,57 @@ 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;
PackageName = map.Package.Name;
var mapPlayers = new MapPlayers(map.PlayerDefinitions);
var spawns = new List<CPos>();
foreach (var kv in map.ActorDefinitions.Where(d => d.Value.Value == "mpspawn"))
{
var s = new ActorReference(kv.Value.Value, kv.Value.ToDictionary());
spawns.Add(s.Get<LocationInit>().Value);
}
/// <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)
innerData = new InnerData
{
Path = p.Name;
package = p;
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 },
{ "FluentMessages", map.FluentMessageDefinitions },
{ "Weapons", map.WeaponDefinitions },
{ "Voices", map.VoiceDefinitions },
{ "Music", map.MusicDefinitions },
{ "Notifications", map.NotificationDefinitions },
{ "Sequences", map.SequenceDefinitions },
{ "ModelSequences", map.ModelSequenceDefinitions }
}, null);
}
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification,
string[] mapCompatibility, MapGridType gridType, IEnumerable<List<MiniYamlNode>> modDataRules)
{
Dictionary<string, MiniYaml> yaml;
using (var yamlStream = p.GetStream("map.yaml"))
{
@@ -373,9 +392,12 @@ namespace OpenRA
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, $"{p.Name}:map.yaml", stringPool: cache.StringPool)).ToDictionary();
}
PackageName = p.Name;
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 +410,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 +431,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 +442,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 +477,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;
}
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, string[] mapCompatibility, 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 +513,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)
{
@@ -610,37 +541,29 @@ namespace OpenRA
// 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))
if (!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)
// 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 +616,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 +636,34 @@ namespace OpenRA
public void Invalidate()
{
lock (syncRoot)
{
innerData.Class = MapClassification.Unknown;
innerData.Status = MapStatus.Unavailable;
}
}
public void Dispose()
{
package?.Dispose();
DisposePackage();
}
public void DisposePackage()
{
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 +679,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,8 +690,7 @@ 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);

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

@@ -55,9 +55,14 @@ namespace OpenRA
return Bounds.Contains(uv.U, uv.V);
}
public int IndexOf(T value, int startIndex)
{
return Array.IndexOf(Entries, value, startIndex);
}
public void SetAll(T value)
{
Entries.AsSpan().Fill(value);
Array.Fill(Entries, value);
}
}
}

View File

@@ -10,7 +10,6 @@
#endregion
using System.Collections.Generic;
using System.Collections.Immutable;
using OpenRA.FileSystem;
using OpenRA.Primitives;
using OpenRA.Support;
@@ -26,16 +25,14 @@ namespace OpenRA
public interface ITerrainInfo
{
string Id { get; }
string Name { get; }
Size TileSize { get; }
ImmutableArray<TerrainTypeInfo> TerrainTypes { get; }
TerrainTypeInfo[] TerrainTypes { get; }
TerrainTileInfo GetTerrainInfo(TerrainTile r);
bool TryGetTerrainInfo(TerrainTile r, out TerrainTileInfo info);
byte GetTerrainIndex(string type);
byte GetTerrainIndex(TerrainTile r);
TerrainTile DefaultTerrainTile { get; }
ImmutableArray<Color> HeightDebugColors { get; }
Color[] HeightDebugColors { get; }
IEnumerable<Color> RestrictedPlayerColors { get; }
float MinHeightColorBrightness { get; }
float MaxHeightColorBrightness { get; }
@@ -63,7 +60,7 @@ namespace OpenRA
{
public readonly string Type;
public readonly BitSet<TargetableType> TargetTypes;
public readonly ImmutableArray<string> AcceptsSmudgeType = [];
public readonly HashSet<string> AcceptsSmudgeType = new();
public readonly Color Color;
public readonly bool RestrictPlayerColor = false;

View File

@@ -11,35 +11,32 @@
namespace OpenRA
{
public readonly struct TerrainTile(ushort type, byte index)
public readonly struct TerrainTile
{
public readonly ushort Type = type;
public readonly byte Index = index;
public readonly ushort Type;
public readonly byte Index;
public TerrainTile(ushort type, byte index)
{
Type = type;
Index = index;
}
public override int GetHashCode() { return Type.GetHashCode() ^ Index.GetHashCode(); }
public override string ToString() { return Type + "," + Index; }
public static bool TryParse(string s, out TerrainTile tt)
{
var split = s.Split(',');
if (split.Length == 2 &&
Exts.TryParseUshortInvariant(split[0], out var type) &&
Exts.TryParseByteInvariant(split[1], out var index))
{
tt = new TerrainTile(type, index);
return true;
}
tt = default;
return false;
}
}
public readonly struct ResourceTile(byte type, byte index)
public readonly struct ResourceTile
{
public readonly byte Type = type;
public readonly byte Index = index;
public readonly byte Type;
public readonly byte Index;
public ResourceTile(byte type, byte index)
{
Type = type;
Index = index;
}
public override int GetHashCode() { return Type.GetHashCode() ^ Index.GetHashCode(); }
}

View File

@@ -59,10 +59,16 @@ namespace OpenRA
public sealed class MiniYamlNode
{
public readonly struct SourceLocation(string name, int line)
public readonly struct SourceLocation
{
public readonly string Name = name;
public readonly int Line = line;
public readonly string Name;
public readonly int Line;
public SourceLocation(string name, int line)
{
Name = name;
Line = line;
}
public override string ToString() { return $"{Name}:{Line}"; }
}
@@ -93,7 +99,7 @@ namespace OpenRA
}
public MiniYamlNode(string k, string v, string c = null)
: this(k, new MiniYaml(v, []), c) { }
: this(k, new MiniYaml(v, Enumerable.Empty<MiniYamlNode>()), c) { }
public MiniYamlNode(string k, string v, IEnumerable<MiniYamlNode> n)
: this(k, new MiniYaml(v, n), null) { }
@@ -109,7 +115,7 @@ namespace OpenRA
const int SpacesPerLevel = 4;
static readonly Func<string, string> StringIdentity = s => s;
static readonly Func<MiniYaml, MiniYaml> MiniYamlIdentity = my => my;
static readonly Dictionary<string, MiniYamlNode> ConflictScratch = [];
static readonly Dictionary<string, MiniYamlNode> ConflictScratch = new();
public readonly string Value;
public readonly ImmutableArray<MiniYamlNode> Nodes;
@@ -190,22 +196,21 @@ namespace OpenRA
}
public MiniYaml(string value)
: this(value, []) { }
: this(value, Enumerable.Empty<MiniYamlNode>()) { }
public MiniYaml(string value, IEnumerable<MiniYamlNode> nodes)
{
Value = value;
Nodes = nodes.ToImmutableArray();
Nodes = ImmutableArray.CreateRange(nodes);
}
static IEnumerable<MiniYamlNode> FromLines(
IEnumerable<ReadOnlyMemory<char>> lines, string name, bool discardCommentsAndWhitespace, HashSet<string> stringPool)
static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string name, bool discardCommentsAndWhitespace, HashSet<string> stringPool)
{
// YAML config often contains repeated strings for key, values, comments.
// Pool these strings so we only need one copy of each unique string.
// This saves on long-term memory usage as parsed values can often live a long time.
// A caller can also provide a pool as input, allowing de-duplication across multiple parses.
stringPool ??= [];
stringPool ??= new HashSet<string>();
var result = new List<List<MiniYamlNode>>
{
@@ -336,25 +341,18 @@ namespace OpenRA
parsedLines.Add((level, keyString, valueString, commentString, location));
}
foreach (var topLevelNode in result[0])
yield return topLevelNode;
result[0].Clear();
}
if (parsedLines.Count > 0)
{
BuildCompletedSubNode(0);
foreach (var topLevelNode in result[0])
yield return topLevelNode;
result[0].Clear();
}
return result[0];
void BuildCompletedSubNode(int level)
{
var lastLevel = parsedLines[^1].Level;
while (lastLevel >= result.Count)
result.Add([]);
result.Add(new List<MiniYamlNode>());
while (parsedLines.Count > 0 && parsedLines[^1].Level >= level)
{
@@ -383,30 +381,29 @@ namespace OpenRA
}
}
public static IEnumerable<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
public static List<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
{
return FromStream(File.OpenRead(path), path, discardCommentsAndWhitespace, stringPool);
}
public static IEnumerable<MiniYamlNode> FromStream(Stream s, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
public static List<MiniYamlNode> FromStream(Stream s, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
{
return FromLines(s.ReadAllLinesAsMemory(), name, discardCommentsAndWhitespace, stringPool);
}
public static IEnumerable<MiniYamlNode> FromString(string text, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
public static List<MiniYamlNode> FromString(string text, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
{
return FromLines(text.Split(["\r\n", "\n"], StringSplitOptions.None).Select(s => s.AsMemory()), name, discardCommentsAndWhitespace, stringPool);
return FromLines(text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(s => s.AsMemory()), name, discardCommentsAndWhitespace, stringPool);
}
public static List<MiniYamlNode> Merge(IEnumerable<IEnumerable<MiniYamlNode>> sources)
public static List<MiniYamlNode> Merge(IEnumerable<IReadOnlyCollection<MiniYamlNode>> sources)
{
var sourcesList = sources.ToList();
if (sourcesList.Count == 0)
return [];
return new List<MiniYamlNode>();
var tree = sourcesList
.Where(s => s != null)
.Select(s => s as IReadOnlyCollection<MiniYamlNode> ?? s.ToList())
.Select(MergeSelfPartial)
.Aggregate(MergePartial)
.Where(n => n.Key != null)
@@ -506,9 +503,6 @@ namespace OpenRA
var ret = new List<MiniYamlNode>(existingNodes.Count);
foreach (var n in existingNodes)
{
if (n.Key == null)
continue;
if (keys.Add(n.Key))
ret.Add(n);
else
@@ -523,60 +517,17 @@ namespace OpenRA
return ret;
}
static IReadOnlyList<MiniYamlNode> WeakResolveRemovals(IReadOnlyList<MiniYamlNode> nodes)
{
if (nodes == null || nodes.Count == 0)
return nodes;
List<MiniYamlNode> ret = null;
for (var i = 0; i < nodes.Count; i++)
{
var node = nodes[i];
if (node.Key == null)
continue;
if (node.Key.StartsWith('-'))
{
if (ret == null)
{
ret ??= new List<MiniYamlNode>(nodes.Count);
ret.AddRange(nodes.Take(i));
}
// Apply the removal node - but "weakly" - don't throw if there is no prior node to remove.
var removed = node.Key[1..];
ret.RemoveAll(r => r.Key == removed);
}
else
{
ret?.Add(node);
}
}
return ret ?? nodes;
}
static MiniYaml MergePartial(MiniYaml existingNodes, MiniYaml overrideNodes)
{
var resolvedExistingNodes = WeakResolveRemovals(existingNodes?.Nodes);
var resolvedOverrideNodes = WeakResolveRemovals(overrideNodes?.Nodes);
lock (ConflictScratch)
{
try
{
// PERF: Reuse ConflictScratch for all conflict checks to avoid allocations.
resolvedExistingNodes?.IntoDictionaryWithConflictLog(
existingNodes?.Nodes.IntoDictionaryWithConflictLog(
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
resolvedOverrideNodes?.IntoDictionaryWithConflictLog(
overrideNodes?.Nodes.IntoDictionaryWithConflictLog(
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
ConflictScratch.Clear();
}
catch (ArgumentException ex)
{
throw new YamlException(ex.Message);
}
}
if (existingNodes == null)
return overrideNodes;
@@ -605,9 +556,6 @@ namespace OpenRA
void MergeNode(MiniYamlNode node)
{
if (node.Key == null)
return;
// Append Removal nodes to the result.
// Therefore: we know the remainder of the method deals with a plain node.
if (node.Key.StartsWith('-'))
@@ -678,12 +626,12 @@ namespace OpenRA
{
if (mapRules != null && mapRules.Value != null)
{
var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", mapRules.Value);
files = files.Concat(mapFiles);
var mapFiles = FieldLoader.GetValue<string[]>("value", mapRules.Value);
files = files.Append(mapFiles);
}
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var yaml = files.Select(s => FromStream(fileSystem.Open(s), s, stringPool: stringPool));
IEnumerable<IReadOnlyCollection<MiniYamlNode>> yaml = files.Select(s => FromStream(fileSystem.Open(s), s, stringPool: stringPool));
if (mapRules != null && mapRules.Nodes.Length > 0)
yaml = yaml.Append(mapRules.Nodes);
@@ -748,7 +696,7 @@ namespace OpenRA
public MiniYamlBuilder(string value, List<MiniYamlNode> nodes)
{
Value = value;
Nodes = nodes == null ? [] : nodes.ConvertAll(x => new MiniYamlNodeBuilder(x));
Nodes = nodes == null ? new List<MiniYamlNodeBuilder>() : nodes.ConvertAll(x => new MiniYamlNodeBuilder(x));
}
public MiniYaml Build()
@@ -776,6 +724,7 @@ namespace OpenRA
}
}
[Serializable]
public class YamlException : Exception
{
public YamlException(string s)

View File

@@ -10,22 +10,18 @@
#endregion
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Video;
using OpenRA.Widgets;
using FS = OpenRA.FileSystem.FileSystem;
namespace OpenRA
{
public interface IGlobalModData { }
public sealed class ModData : IDisposable
{
public readonly Manifest Manifest;
@@ -35,13 +31,14 @@ namespace OpenRA
public readonly IPackageLoader[] PackageLoaders;
public readonly ISoundLoader[] SoundLoaders;
public readonly ISpriteLoader[] SpriteLoaders;
public readonly ITerrainLoader TerrainLoader;
public readonly ISpriteSequenceLoader SpriteSequenceLoader;
public readonly IVideoLoader[] VideoLoaders;
public readonly HotkeyManager Hotkeys;
public readonly IFileSystemLoader FileSystemLoader;
public readonly FrozenDictionary<string, CursorSequence> Cursors;
public ILoadScreen LoadScreen { get; }
public CursorProvider CursorProvider { get; private set; }
public FS ModFiles;
public IReadOnlyFileSystem DefaultFileSystem => ModFiles;
@@ -51,11 +48,9 @@ namespace OpenRA
readonly Lazy<IReadOnlyDictionary<string, ITerrainInfo>> defaultTerrainInfo;
public IReadOnlyDictionary<string, ITerrainInfo> DefaultTerrainInfo => defaultTerrainInfo.Value;
readonly TypeDictionary modules = [];
public ModData(Manifest mod, InstalledMods mods, bool useLoadScreen = false)
{
Languages = [];
Languages = Array.Empty<string>();
// Take a local copy of the manifest
Manifest = new Manifest(mod.Id, mod.Package);
@@ -65,64 +60,53 @@ namespace OpenRA
FileSystemLoader = ObjectCreator.GetLoader<IFileSystemLoader>(Manifest.FileSystem.Value, "filesystem");
FieldLoader.Load(FileSystemLoader, Manifest.FileSystem);
FileSystemLoader.Mount(Manifest, ModFiles, ObjectCreator);
FileSystemLoader.Mount(ModFiles, ObjectCreator);
ModFiles.TrimExcess();
foreach (var kv in Manifest.GlobalModData)
{
var t = ObjectCreator.FindType(kv.Key);
if (t == null || !typeof(IGlobalModData).IsAssignableFrom(t))
throw new InvalidDataException($"`{kv.Key}` is not a valid mod manifest entry.");
Manifest.LoadCustomData(ObjectCreator);
IGlobalModData module;
var ctor = t.GetConstructor([typeof(MiniYaml)]);
if (ctor != null)
{
// Class has opted-in to DIY initialization
module = (IGlobalModData)ctor.Invoke([kv.Value]);
}
else
{
// Automatically load the child nodes using FieldLoader
module = ObjectCreator.CreateObject<IGlobalModData>(kv.Key);
FieldLoader.Load(module, kv.Value);
}
modules.Add(module);
}
FluentProvider.Initialize(Manifest, DefaultFileSystem);
FluentProvider.Initialize(this, DefaultFileSystem);
if (useLoadScreen)
{
LoadScreen = ObjectCreator.CreateObject<ILoadScreen>(Manifest.LoadScreen.Value);
LoadScreen.Init(Manifest, DefaultFileSystem);
LoadScreen.Init(this, Manifest.LoadScreen.ToDictionary(my => my.Value));
LoadScreen.Display();
}
WidgetLoader = new WidgetLoader(Manifest, DefaultFileSystem);
MapCache = new MapCache(Manifest, ModFiles);
WidgetLoader = new WidgetLoader(this);
MapCache = new MapCache(this);
SoundLoaders = ObjectCreator.GetLoaders<ISoundLoader>(Manifest.SoundFormats, "sound");
SpriteLoaders = ObjectCreator.GetLoaders<ISpriteLoader>(Manifest.SpriteFormats, "sprite");
VideoLoaders = ObjectCreator.GetLoaders<IVideoLoader>(Manifest.VideoFormats, "video");
SpriteSequenceLoader = ObjectCreator.GetLoader<ISpriteSequenceLoader>(Manifest.SpriteSequenceFormat, "sequence");
Hotkeys = new HotkeyManager(ModFiles, ObjectCreator, Manifest);
Cursors = ParseCursors(Manifest, DefaultFileSystem);
var terrainFormat = Manifest.Get<TerrainFormat>();
var terrainLoader = ObjectCreator.FindType(terrainFormat.Type + "Loader");
var terrainCtor = terrainLoader?.GetConstructor(new[] { typeof(ModData) });
if (terrainLoader == null || !terrainLoader.GetInterfaces().Contains(typeof(ITerrainLoader)) || terrainCtor == null)
throw new InvalidOperationException($"Unable to find a terrain loader for type '{terrainFormat.Type}'.");
TerrainLoader = (ITerrainLoader)terrainCtor.Invoke(new[] { this });
var sequenceFormat = Manifest.Get<SpriteSequenceFormat>();
var sequenceLoader = ObjectCreator.FindType(sequenceFormat.Type + "Loader");
var sequenceCtor = sequenceLoader?.GetConstructor(new[] { typeof(ModData) });
if (sequenceLoader == null || !sequenceLoader.GetInterfaces().Contains(typeof(ISpriteSequenceLoader)) || sequenceCtor == null)
throw new InvalidOperationException($"Unable to find a sequence loader for type '{sequenceFormat.Type}'.");
SpriteSequenceLoader = (ISpriteSequenceLoader)sequenceCtor.Invoke(new[] { this });
Hotkeys = new HotkeyManager(ModFiles, Game.Settings.Keys, Manifest);
defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this));
defaultTerrainInfo = Exts.Lazy(() =>
{
var terrainType = ObjectCreator.FindType(Manifest.TerrainFormat + "Loader");
var terrainCtor = terrainType?.GetConstructor([typeof(ModData)]);
if (terrainType == null || !terrainType.GetInterfaces().Contains(typeof(ITerrainLoader)) || terrainCtor == null)
throw new InvalidOperationException($"Unable to find a terrain loader for type '{Manifest.TerrainFormat}'.");
var items = new Dictionary<string, ITerrainInfo>();
var terrainLoader = (ITerrainLoader)terrainCtor.Invoke([this]);
foreach (var file in Manifest.TileSets)
{
var t = terrainLoader.ParseTerrain(DefaultFileSystem, file);
var t = TerrainLoader.ParseTerrain(DefaultFileSystem, file);
items.Add(t.Id, t);
}
@@ -132,23 +116,6 @@ namespace OpenRA
initialThreadId = Environment.CurrentManagedThreadId;
}
static FrozenDictionary<string, CursorSequence> ParseCursors(Manifest manifest, IReadOnlyFileSystem fileSystem)
{
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var sequenceYaml = MiniYaml.Merge(manifest.Cursors.Select(
s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool)));
var cursors = new Dictionary<string, CursorSequence>();
foreach (var node in sequenceYaml)
if (node.Key == "Cursors")
foreach (var fileNode in node.Value.Nodes)
foreach (var sequenceNode in fileNode.Value.Nodes)
cursors.Add(sequenceNode.Key, new CursorSequence(
sequenceNode.Key, fileNode.Key, fileNode.Value.Value, sequenceNode.Value));
return cursors.ToFrozenDictionary();
}
// HACK: Only update the loading screen if we're in the main thread.
readonly int initialThreadId;
internal void HandleLoadingProgress()
@@ -165,18 +132,26 @@ namespace OpenRA
// horribly when you use ModData in unexpected ways.
ChromeMetrics.Initialize(this);
ChromeProvider.Initialize(this);
Ui.Initialize(this);
FluentProvider.Initialize(Manifest, fileSystem);
FluentProvider.Initialize(this, fileSystem);
Game.Sound.Initialize(SoundLoaders, fileSystem);
CursorProvider = new CursorProvider(this);
}
public IEnumerable<string> Languages { get; }
public void PrepareMap(Map map)
public Map PrepareMap(string uid)
{
LoadScreen?.Display();
if (MapCache[uid].Status != MapStatus.Available)
throw new InvalidDataException($"Invalid map uid: {uid}");
Map map;
using (new Support.PerfTimer("Map"))
map = new Map(this, MapCache[uid].Package);
// Reinitialize all our assets
InitializeLoaders(map);
map.Sequences.LoadSprites();
@@ -185,37 +160,14 @@ namespace OpenRA
using (new Support.PerfTimer("Map.Music"))
foreach (var entry in map.Rules.Music)
entry.Value.Load(map);
return map;
}
public MiniYamlNode[][] GetRulesYaml()
public List<MiniYamlNode>[] GetRulesYaml()
{
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s, stringPool: stringPool).ToArray()).ToArray();
}
/// <summary>Load a cached IGlobalModData instance.</summary>
public T GetOrCreate<T>() where T : IGlobalModData
{
var module = modules.GetOrDefault<T>();
// Lazily create the default values if not explicitly defined.
if (module == null)
{
module = (T)ObjectCreator.CreateBasic(typeof(T));
modules.Add(module);
}
return module;
}
public T GetOrNull<T>() where T : IGlobalModData
{
return modules.GetOrDefault<T>();
}
public T GetSettings<T>() where T : SettingsModule
{
return Game.Settings.GetOrCreate<T>(ObjectCreator, Manifest.Id);
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s, stringPool: stringPool)).ToArray();
}
public void Dispose()
@@ -225,18 +177,14 @@ namespace OpenRA
ObjectCreator?.Dispose();
foreach (var module in modules)
{
var disposableModule = module as IDisposable;
disposableModule?.Dispose();
}
Manifest.Dispose();
}
}
public interface ILoadScreen : IDisposable
{
/// <summary>Initializes the loadscreen with yaml data from the LoadScreen block in mod.yaml.</summary>
void Init(Manifest manifest, IReadOnlyFileSystem fileSystem);
void Init(ModData m, Dictionary<string, string> info);
/// <summary>Called at arbitrary times during mod load to rerender the loadscreen.</summary>
void Display();
@@ -245,7 +193,7 @@ namespace OpenRA
/// Called before loading the mod assets.
/// Returns false if mod loading should be aborted (e.g. switching to another mod instead).
/// </summary>
bool BeforeLoad(ModData modData);
bool BeforeLoad();
/// <summary>Called when the engine expects to connect to a server/replay or load the shellmap.</summary>
void StartGame(Arguments args);
@@ -253,6 +201,6 @@ namespace OpenRA
public interface IFileSystemLoader
{
void Mount(Manifest manifest, FS fileSystem, ObjectCreator objectCreator);
void Mount(FS fileSystem, ObjectCreator objectCreator);
}
}

View File

@@ -42,9 +42,9 @@ namespace OpenRA.Network
public sealed class EchoConnection : IConnection
{
const int LocalClientId = 1;
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sync = [];
readonly Queue<(int Frame, OrderPacket Orders)> orders = [];
readonly Queue<OrderPacket> immediateOrders = [];
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sync = new();
readonly Queue<(int Frame, OrderPacket Orders)> orders = new();
readonly Queue<OrderPacket> immediateOrders = new();
bool disposed;
int IConnection.LocalClientId => LocalClientId;
@@ -52,7 +52,7 @@ namespace OpenRA.Network
void IConnection.StartGame()
{
// Inject an empty frame to fill the gap we are making by projecting forward orders
orders.Enqueue((0, new OrderPacket([])));
orders.Enqueue((0, new OrderPacket(Array.Empty<Order>())));
}
void IConnection.Send(int frame, IEnumerable<Order> o)
@@ -100,12 +100,12 @@ namespace OpenRA.Network
{
public readonly ConnectionTarget Target;
internal ReplayRecorder Recorder { get; private set; }
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sentSync = [];
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> queuedSyncPackets = [];
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sentSync = new();
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> queuedSyncPackets = new();
readonly Queue<(int Frame, OrderPacket Orders)> sentOrders = [];
readonly Queue<OrderPacket> sentImmediateOrders = [];
readonly ConcurrentQueue<(int FromClient, byte[] Data)> receivedPackets = [];
readonly Queue<(int Frame, OrderPacket Orders)> sentOrders = new();
readonly Queue<OrderPacket> sentImmediateOrders = new();
readonly ConcurrentQueue<(int FromClient, byte[] Data)> receivedPackets = new();
TcpClient tcp;
volatile ConnectionState connectionState = ConnectionState.Connecting;
volatile int clientId;

View File

@@ -22,12 +22,12 @@ namespace OpenRA.Network
public ConnectionTarget()
{
endpoints = [new DnsEndPoint("invalid", 0)];
endpoints = new[] { new DnsEndPoint("invalid", 0) };
}
public ConnectionTarget(string host, int port)
{
endpoints = [new DnsEndPoint(host, port)];
endpoints = new[] { new DnsEndPoint(host, port) };
}
public ConnectionTarget(IEnumerable<DnsEndPoint> endpoints)
@@ -48,7 +48,7 @@ namespace OpenRA.Network
}
catch (Exception)
{
return [];
return Enumerable.Empty<IPEndPoint>();
}
}).ToList();
}

View File

@@ -11,7 +11,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Linguini.Shared.Types.Bundle;
namespace OpenRA.Network
@@ -56,16 +55,14 @@ namespace OpenRA.Network
public readonly string Key = string.Empty;
[FieldLoader.LoadUsing(nameof(LoadArguments))]
public readonly ImmutableArray<object> Arguments;
public readonly object[] Arguments;
static object LoadArguments(MiniYaml yaml)
{
var arguments = new List<object>();
var argumentsNode = yaml.NodeWithKeyOrDefault("Arguments");
if (argumentsNode == null)
return ImmutableArray<object>.Empty;
var arguments = new List<object>(argumentsNode.Value.Nodes.Length * 2);
if (argumentsNode != null)
{
foreach (var argumentNode in argumentsNode.Value.Nodes)
{
var argument = FieldLoader.Load<FluentArgument>(argumentNode.Value);
@@ -80,8 +77,9 @@ namespace OpenRA.Network
else
arguments.Add(argument.Value);
}
}
return arguments.ToImmutableArray();
return arguments.ToArray();
}
public FluentMessage(MiniYaml yaml)

View File

@@ -86,23 +86,22 @@ namespace OpenRA.Network
// Loaded from file and updated during gameplay
public int LastOrdersFrame { get; private set; }
public int LastSyncFrame { get; private set; }
byte[] lastSyncPacket = [];
byte[] lastSyncPacket = Array.Empty<byte>();
// Loaded from file or set on game start
public Session.Global GlobalSettings { get; private set; }
public Dictionary<string, Session.Slot> Slots { get; private set; }
public Dictionary<string, SlotClient> SlotClients { get; private set; }
public Dictionary<int, MiniYaml> TraitData = [];
public MapGenerationArgs MapGenerationArgs;
public Dictionary<int, MiniYaml> TraitData = new();
// Set on game start
int[] clientsBySlotIndex = [];
int[] clientsBySlotIndex = Array.Empty<int>();
int firstBotSlotIndex = -1;
public GameSave()
{
LastOrdersFrame = -1;
Slots = [];
Slots = new Dictionary<string, Session.Slot>();
}
public GameSave(string filepath)
@@ -124,10 +123,10 @@ namespace OpenRA.Network
lastSyncPacket = rs.ReadBytes(Order.SyncHashOrderLength);
var globalSettings = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:globalSettings");
GlobalSettings = Session.Global.Deserialize(globalSettings.First().Value);
GlobalSettings = Session.Global.Deserialize(globalSettings[0].Value);
var slots = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:slots");
Slots = [];
Slots = new Dictionary<string, Session.Slot>();
foreach (var s in slots)
{
var slot = Session.Slot.Deserialize(s.Value);
@@ -135,17 +134,13 @@ namespace OpenRA.Network
}
var slotClients = MiniYaml.FromString(rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength), $"{filepath}:slotClients");
SlotClients = [];
SlotClients = new Dictionary<string, SlotClient>();
foreach (var s in slotClients)
{
var slotClient = SlotClient.Deserialize(s.Value);
SlotClients.Add(slotClient.Slot, slotClient);
}
var mapGenerationArgs = rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength);
if (!string.IsNullOrEmpty(mapGenerationArgs))
MapGenerationArgs = FieldLoader.Load<MapGenerationArgs>(new MiniYaml("", MiniYaml.FromString(mapGenerationArgs, $"{filepath}:mapGenerationArgs")));
if (rs.Position != traitDataOffset || rs.ReadInt32() != TraitDataMarker)
throw new InvalidDataException("Invalid orasav file");
@@ -160,9 +155,6 @@ namespace OpenRA.Network
public void StartGame(Session lobbyInfo, MapPreview map)
{
if (map.Class == MapClassification.Generated)
MapGenerationArgs = map.GenerationArgs;
// Game orders are mapped from a client index to the slot that they occupy
// Orders from spectators are ignored, which is not a problem in practice
// because all immediate orders are also ignored
@@ -174,8 +166,8 @@ namespace OpenRA.Network
// Perform a deep clone by round-tripping the data
GlobalSettings = Session.Global.Deserialize(lobbyInfo.GlobalSettings.Serialize().Value);
Slots = [];
SlotClients = [];
Slots = new Dictionary<string, Session.Slot>();
SlotClients = new Dictionary<string, SlotClient>();
foreach (var s in lobbyInfo.Slots)
{
Slots[s.Key] = Session.Slot.Deserialize(s.Value.Serialize().Value);
@@ -314,8 +306,6 @@ namespace OpenRA.Network
.ToList();
file.WriteLengthPrefixedString(Encoding.UTF8, slotClientNodes.WriteToString());
file.WriteLengthPrefixedString(Encoding.UTF8, MapGenerationArgs?.Serialize().WriteToString() ?? string.Empty);
var traitDataOffset = file.Length;
file.Write(TraitDataMarker);

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 System.Text.RegularExpressions;
using OpenRA.Primitives;
@@ -49,9 +47,8 @@ namespace OpenRA.Network
public class GameServer
{
static readonly ImmutableArray<string> SerializeFields =
[
static readonly string[] SerializeFields =
{
// Server information
"Name", "Address",
@@ -60,7 +57,7 @@ namespace OpenRA.Network
// Current server state
"Map", "State", "MaxPlayers", "Protected", "Authentication", "DisabledSpawnPoints"
];
};
public const int ProtocolVersion = 2;
@@ -133,10 +130,10 @@ namespace OpenRA.Network
public readonly bool IsJoinable = false;
[FieldLoader.LoadUsing(nameof(LoadClients))]
public readonly ImmutableArray<GameClient> Clients;
public readonly GameClient[] Clients;
/// <summary>The list of spawnpoints that are disabled for this game.</summary>
public readonly FrozenSet<int> DisabledSpawnPoints = FrozenSet<int>.Empty;
public readonly int[] DisabledSpawnPoints = Array.Empty<int>();
public string ModLabel => $"{ModTitle} ({Version})";
@@ -152,7 +149,7 @@ namespace OpenRA.Network
clients.Add(FieldLoader.Load<GameClient>(client.Value));
}
return clients.ToImmutableArray();
return clients.ToArray();
}
public GameServer(MiniYaml yaml)
@@ -229,9 +226,9 @@ namespace OpenRA.Network
ModWebsite = manifest.Metadata.Website;
ModIcon32 = manifest.Metadata.WebIcon32;
Protected = !string.IsNullOrEmpty(server.Settings.Password);
Authentication = server.Settings.RequireAuthentication || server.Settings.ProfileIDWhitelist.Count > 0;
Clients = server.LobbyInfo.Clients.Select(c => new GameClient(c)).ToImmutableArray();
DisabledSpawnPoints = server.LobbyInfo.DisabledSpawnPoints?.ToFrozenSet() ?? FrozenSet<int>.Empty;
Authentication = server.Settings.RequireAuthentication || server.Settings.ProfileIDWhitelist.Length > 0;
Clients = server.LobbyInfo.Clients.Select(c => new GameClient(c)).ToArray();
DisabledSpawnPoints = server.LobbyInfo.DisabledSpawnPoints?.ToArray() ?? Array.Empty<int>();
}
public string ToPOSTData(bool lanGame)

View File

@@ -269,11 +269,6 @@ namespace OpenRA
return new Order(order, null, false) { IsImmediate = isImmediate, TargetString = targetString };
}
public static Order FromTargetString(string order, string targetString, bool isImmediate, uint extraData)
{
return new Order(order, null, false) { IsImmediate = isImmediate, TargetString = targetString, ExtraData = extraData };
}
public static Order FromGroupedOrder(Order grouped, Actor subject)
{
return new Order(

View File

@@ -78,7 +78,7 @@ namespace OpenRA.Network
public static class OrderIO
{
static readonly OrderPacket NoOrders = new([]);
static readonly OrderPacket NoOrders = new(Array.Empty<Order>());
public static byte[] SerializeSync((int Frame, int SyncHash, ulong DefeatState) data)
{

View File

@@ -10,7 +10,6 @@
#endregion
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -27,8 +26,8 @@ namespace OpenRA.Network
const string DesyncCompareLogs = "notification-desync-compare-logs";
readonly SyncReport syncReport;
readonly Dictionary<int, Queue<(int Frame, OrderPacket Orders)>> pendingOrders = [];
readonly Dictionary<int, (int SyncHash, ulong DefeatState)> syncForFrame = [];
readonly Dictionary<int, Queue<(int Frame, OrderPacket Orders)>> pendingOrders = new();
readonly Dictionary<int, (int SyncHash, ulong DefeatState)> syncForFrame = new();
public Session LobbyInfo = new();
@@ -41,7 +40,7 @@ namespace OpenRA.Network
public bool AuthenticationFailed = false;
// The default null means "no map restriction" while an empty set means "all maps restricted"
public FrozenSet<string> ServerMapPool = null;
public HashSet<string> ServerMapPool = null;
public int NetFrameNumber { get; private set; }
public int LocalFrameNumber;
@@ -54,11 +53,11 @@ namespace OpenRA.Network
internal int GameSaveLastFrame = -1;
internal int GameSaveLastSyncFrame = -1;
readonly List<Order> localOrders = [];
readonly List<Order> localImmediateOrders = [];
readonly List<Order> localOrders = new();
readonly List<Order> localImmediateOrders = new();
readonly List<ClientOrder> processClientOrders = [];
readonly List<int> processClientsToRemove = [];
readonly List<ClientOrder> processClientOrders = new();
readonly List<int> processClientsToRemove = new();
bool disposed;
bool generateSyncReport = false;

View File

@@ -24,8 +24,8 @@ namespace OpenRA.Network
public (int ClientId, byte[] Packet)[] Packets;
}
readonly Queue<Chunk> chunks = [];
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sync = [];
readonly Queue<Chunk> chunks = new();
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sync = new();
readonly int orderLatency;
public readonly int TickCount;
@@ -87,7 +87,7 @@ namespace OpenRA.Network
}
}
var gameSpeeds = Game.ModData.GetOrCreate<GameSpeeds>();
var gameSpeeds = Game.ModData.Manifest.Get<GameSpeeds>();
var gameSpeedName = LobbyInfo.GlobalSettings.OptionOrDefault("gamespeed", gameSpeeds.DefaultSpeed);
orderLatency = gameSpeeds.Speeds[gameSpeedName].OrderLatency;
}

View File

@@ -20,12 +20,12 @@ namespace OpenRA.Network
{
public class Session
{
public List<Client> Clients = [];
public List<Client> Clients = new();
// Keyed by the PlayerReference id that the slot corresponds to
public Dictionary<string, Slot> Slots = [];
public Dictionary<string, Slot> Slots = new();
public HashSet<int> DisabledSpawnPoints = [];
public HashSet<int> DisabledSpawnPoints = new();
public Global GlobalSettings = new();
@@ -213,16 +213,15 @@ namespace OpenRA.Network
public bool AllowSpectators = true;
public string GameUid;
public bool EnableSingleplayer;
public bool EnableMapGeneration;
public bool EnableGameSaves;
public bool EnableSyncReports;
public bool Dedicated;
public bool GameSavesEnabled;
// 120ms network frame interval for 40ms local tick
public int NetFrameInterval = 3;
[FieldLoader.Ignore]
public Dictionary<string, LobbyOptionState> LobbyOptions = [];
public Dictionary<string, LobbyOptionState> LobbyOptions = new();
public static Global Deserialize(MiniYaml data)
{
@@ -241,7 +240,7 @@ namespace OpenRA.Network
var data = new MiniYamlNode("GlobalSettings", FieldSaver.Save(this));
var options = LobbyOptions.Select(kv => new MiniYamlNode(kv.Key, FieldSaver.Save(kv.Value)));
data = data.WithValue(data.Value.WithNodesAppended(
[new MiniYamlNode("Options", new MiniYaml(null, options))]));
new[] { new MiniYamlNode("Options", new MiniYaml(null, options)) }));
return data;
}

View File

@@ -166,9 +166,9 @@ namespace OpenRA.Network
public int Frame;
public int SyncedRandom;
public int TotalCount;
public readonly List<TraitReport> Traits = [];
public readonly List<EffectReport> Effects = [];
public readonly List<OrderManager.ClientOrder> Orders = [];
public readonly List<TraitReport> Traits = new();
public readonly List<EffectReport> Effects = new();
public readonly List<OrderManager.ClientOrder> Orders = new();
}
struct TraitReport
@@ -202,10 +202,10 @@ namespace OpenRA.Network
{
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
var fields = type.GetFields(Flags)
.Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<VerifySyncAttribute>())
.Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>())
.ToList();
var properties = type.GetProperties(Flags)
.Where(pi => pi.HasAttribute<VerifySyncAttribute>())
.Where(pi => pi.HasAttribute<SyncAttribute>())
.ToList();
foreach (var prop in properties)
@@ -238,11 +238,11 @@ namespace OpenRA.Network
// PERF: If the member is a Boolean, we can also avoid the allocation caused by boxing it.
// Instead, we can just return the resulting strings directly.
var getBoolString = Expression.Condition(getMember, TrueString, FalseString);
return Expression.Lambda<Func<ISync, string>>(getBoolString, name, [SyncParam]).Compile();
return Expression.Lambda<Func<ISync, string>>(getBoolString, name, new[] { SyncParam }).Compile();
}
var boxedCopy = Expression.Convert(getMember, typeof(object));
return Expression.Lambda<Func<ISync, object>>(boxedCopy, name, [SyncParam]).Compile();
return Expression.Lambda<Func<ISync, object>>(boxedCopy, name, new[] { SyncParam }).Compile();
}
// For reference types, we have to call ToString right away to get a snapshot of the value. We cannot
@@ -266,13 +266,13 @@ namespace OpenRA.Network
// (ISync sync) => { var foo = ((TSync)sync).Foo; return foo == null ? null : foo.ToString(); }
var memberVariable = Expression.Variable(memberType, getMember.Member.Name);
var assignMemberVariable = Expression.Assign(memberVariable, getMember);
var member = Expression.Block([memberVariable], assignMemberVariable);
var member = Expression.Block(new[] { memberVariable }, assignMemberVariable);
getString = Expression.Call(member, toString);
var nullMember = Expression.Constant(null, memberType);
getString = Expression.Condition(Expression.Equal(member, nullMember), NullString, getString);
}
return Expression.Lambda<Func<ISync, string>>(getString, name, [SyncParam]).Compile();
return Expression.Lambda<Func<ISync, string>>(getString, name, new[] { SyncParam }).Compile();
}
}

View File

@@ -9,7 +9,6 @@
*/
#endregion
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Server;
@@ -30,6 +29,9 @@ namespace OpenRA.Network
[FluentReference]
const string GameStarted = "notification-game-has-started";
[FluentReference]
const string GameSaved = "notification-game-saved";
[FluentReference("player")]
const string GamePaused = "notification-game-paused";
@@ -65,11 +67,11 @@ namespace OpenRA.Network
{
var message = new FluentMessage(node.Value);
if (message.Key == Joined)
TextNotificationsManager.AddPlayerJoinedLine(message.Key, message.Arguments.ToArray());
TextNotificationsManager.AddPlayerJoinedLine(message.Key, message.Arguments);
else if (message.Key == Left)
TextNotificationsManager.AddPlayerLeftLine(message.Key, message.Arguments.ToArray());
TextNotificationsManager.AddPlayerLeftLine(message.Key, message.Arguments);
else
TextNotificationsManager.AddSystemLine(message.Key, message.Arguments.ToArray());
TextNotificationsManager.AddSystemLine(message.Key, message.Arguments);
}
break;
@@ -181,7 +183,7 @@ namespace OpenRA.Network
if (!string.IsNullOrEmpty(order.TargetString))
{
var data = MiniYaml.FromString(order.TargetString, order.OrderString).ToList();
var data = MiniYaml.FromString(order.TargetString, order.OrderString);
var saveLastOrdersFrame = data.FirstOrDefault(n => n.Key == "SaveLastOrdersFrame");
if (saveLastOrdersFrame != null)
orderManager.GameSaveLastFrame =
@@ -201,7 +203,7 @@ namespace OpenRA.Network
case "SaveTraitData":
{
var data = MiniYaml.FromString(order.TargetString, order.OrderString).First();
var data = MiniYaml.FromString(order.TargetString, order.OrderString)[0];
var traitIndex = Exts.ParseInt32Invariant(data.Key);
world?.AddGameSaveTraitData(traitIndex, data.Value);
@@ -210,8 +212,11 @@ namespace OpenRA.Network
}
case "GameSaved":
if (!orderManager.World.IsReplay)
TextNotificationsManager.AddSystemLine(GameSaved);
foreach (var nsr in orderManager.World.WorldActor.TraitsImplementing<INotifyGameSaved>())
nsr.GameSaved(orderManager.World, order.ExtraData != 0);
nsr.GameSaved(orderManager.World);
break;
case "PauseGame":
@@ -380,21 +385,7 @@ namespace OpenRA.Network
case "SyncMapPool":
{
orderManager.ServerMapPool = FieldLoader.GetValue<FrozenSet<string>>("SyncMapPool", order.TargetString);
break;
}
case "GenerateMap":
{
var yaml = new MiniYaml(order.OrderString, MiniYaml.FromString(order.TargetString, order.OrderString));
var args = FieldLoader.Load<MapGenerationArgs>(yaml);
var preview = Game.ModData.MapCache[args.Uid];
if (preview.Status != MapStatus.Available && preview.Class != MapClassification.Generated)
{
preview.UpdateFromGenerationArgs(args);
preview.Generate();
}
orderManager.ServerMapPool = FieldLoader.GetValue<HashSet<string>>("SyncMapPool", order.TargetString);
break;
}

View File

@@ -22,7 +22,7 @@ namespace OpenRA
{
// .NET does not support unloading assemblies, so mod libraries will leak across mod changes.
// This tracks the assemblies that have been loaded since game start so that we don't load multiple copies
static readonly Dictionary<string, Assembly> ResolvedAssemblies = [];
static readonly Dictionary<string, Assembly> ResolvedAssemblies = new();
readonly Cache<string, Type> typeCache;
readonly Cache<Type, ConstructorInfo> ctorCache;
@@ -55,9 +55,26 @@ namespace OpenRA
if (!ResolvedAssemblies.TryGetValue(hash, out var assembly))
{
#if NET5_0_OR_GREATER
var loader = new Support.AssemblyLoader(resolvedPath);
assembly = loader.LoadDefaultAssembly();
ResolvedAssemblies.Add(hash, assembly);
#else
assembly = Assembly.LoadFile(resolvedPath);
ResolvedAssemblies.Add(hash, assembly);
// Allow mods to use libraries.
var assemblyPath = Path.GetDirectoryName(resolvedPath);
if (assemblyPath != null)
{
foreach (var referencedAssembly in assembly.GetReferencedAssemblies())
{
var depedencyPath = Path.Combine(assemblyPath, referencedAssembly.Name + ".dll");
if (File.Exists(depedencyPath))
LoadAssembly(assemblyList, depedencyPath);
}
}
#endif
}
assemblyList.Add(assembly);
@@ -77,7 +94,7 @@ namespace OpenRA
public T CreateObject<T>(string className)
{
return CreateObject<T>(className, []);
return CreateObject<T>(className, new Dictionary<string, object>());
}
public T CreateObject<T>(string className, Dictionary<string, object> args)
@@ -119,7 +136,7 @@ namespace OpenRA
public object CreateBasic(Type type)
{
return type.GetConstructor([]).Invoke([]);
return type.GetConstructor(Array.Empty<Type>()).Invoke(Array.Empty<object>());
}
public object CreateUsingArgs(ConstructorInfo ctor, Dictionary<string, object> args)

View File

@@ -2,13 +2,18 @@
<PropertyGroup>
<RootNamespace>OpenRA</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="9.0.0" />
<ItemGroup Condition="'$(MSBuildRuntimeType)'!='Mono'">
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="6.0.2" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
</ItemGroup>
<ItemGroup Condition="'$(MSBuildRuntimeType)'=='Mono'">
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Linguini.Bundle" Version="0.8.1" />
<PackageReference Include="OpenRA-Eluant" Version="1.0.22" />
<PackageReference Include="Mono.NAT" Version="3.0.4" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="System.Threading.Channels" Version="9.0.0" />
<PackageReference Include="System.Threading.Channels" Version="6.0.0" />
</ItemGroup>
</Project>

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