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 # IDE0200 Remove unnecessary lambda expression
#csharp_style_prefer_method_group_conversion = true #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 # IDE0210 Convert to top-level statements/IDE0211 Convert to 'Program.Main' style program
csharp_style_prefer_top_level_statements = false csharp_style_prefer_top_level_statements = false
@@ -72,7 +72,7 @@ dotnet_diagnostic.IDE0211.severity = warning
# IDE0290 Use primary constructor # IDE0290 Use primary constructor
#csharp_style_prefer_primary_constructors = true #csharp_style_prefer_primary_constructors = true
dotnet_diagnostic.IDE0290.severity = silent dotnet_diagnostic.IDE0200.severity = silent # Requires C# 12
# IDE0330 Prefer 'System.Threading.Lock' # IDE0330 Prefer 'System.Threading.Lock'
#csharp_prefer_system_threading_lock = true #csharp_prefer_system_threading_lock = true
@@ -229,10 +229,6 @@ dotnet_diagnostic.IDE0100.severity = warning
# No options # No options
dotnet_diagnostic.IDE0120.severity = warning 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 # IDE0130 Namespace does not match folder structure
#dotnet_style_namespace_match_folder = true #dotnet_style_namespace_match_folder = true
# This rule doesn't appear to work (never reports violations) # 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 # IDE0230 Use UTF-8 string literal
#csharp_style_prefer_utf8_string_literals = true #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 # IDE0240 Nullable directive is redundant
# No options # No options
@@ -312,43 +308,27 @@ dotnet_diagnostic.IDE0241.severity = warning
# IDE0300 Use collection expression for array # IDE0300 Use collection expression for array
# From above, uses dotnet_style_prefer_collection_expression # 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 # IDE0301 Use collection expression for empty
# From above, uses dotnet_style_prefer_collection_expression # 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 # IDE0302 Use collection expression for stackalloc
# From above, uses dotnet_style_prefer_collection_expression # 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()' # IDE0303 Use collection expression for 'Create()'
# From above, uses dotnet_style_prefer_collection_expression # 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 # IDE0304 Use collection expression for builder
# From above, uses dotnet_style_prefer_collection_expression # 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 # IDE0305 Use collection expression for fluent
# From above, uses dotnet_style_prefer_collection_expression # From above, uses dotnet_style_prefer_collection_expression
dotnet_diagnostic.IDE0305.severity = silent dotnet_diagnostic.IDE0305.severity = silent # Requires C# 12
# 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
## Field preferences ## Field preferences
@@ -393,10 +373,6 @@ dotnet_diagnostic.IDE0251.severity = warning
#csharp_prefer_static_anonymous_function = true #csharp_prefer_static_anonymous_function = true
dotnet_diagnostic.IDE0320.severity = warning dotnet_diagnostic.IDE0320.severity = warning
# IDE0380 Remove unnecessary 'unsafe' modifier
# No options
dotnet_diagnostic.IDE0380.severity = warning
## Null-checking preferences ## Null-checking preferences
# IDE1005 Use conditional delegate call # IDE1005 Use conditional delegate call
@@ -411,7 +387,7 @@ dotnet_diagnostic.IDE0060.severity = warning
# IDE0280 Use 'nameof' # IDE0280 Use 'nameof'
# No options # No options
dotnet_diagnostic.IDE0280.severity = warning dotnet_diagnostic.IDE0280.severity = silent # Requires C# 11
## Parentheses preferences ## Parentheses preferences
@@ -450,7 +426,7 @@ dotnet_diagnostic.IDE0083.severity = warning
# IDE0170 Simplify property pattern # IDE0170 Simplify property pattern
#csharp_style_prefer_extended_property_pattern = true #csharp_style_prefer_extended_property_pattern = true
dotnet_diagnostic.IDE0170.severity = warning dotnet_diagnostic.IDE0170.severity = silent # Requires C# 10
## Suppression preferences ## Suppression preferences
@@ -458,10 +434,6 @@ dotnet_diagnostic.IDE0170.severity = warning
#dotnet_remove_unnecessary_suppression_exclusions = none #dotnet_remove_unnecessary_suppression_exclusions = none
dotnet_diagnostic.IDE0079.severity = warning dotnet_diagnostic.IDE0079.severity = warning
# IDE0370 Remove unnecessary suppression (null-forgiving operator)
# No options
dotnet_diagnostic.IDE0370.severity = warning
## 'this' and 'Me' preferences ## 'this' and 'Me' preferences
# IDE0003/IDE0009 Remove 'this' or 'Me' qualification/Add 'this' or 'Me' qualification # 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 dotnet_diagnostic.CA1310.severity = warning
# Specify a culture or use an invariant version. # 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 ### Portability and Interoperability Rules
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/interoperability-warnings ### 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 dotnet_diagnostic.CA1507.severity = warning
# Use ArgumentNullException throw helper. # 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. # 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. # 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. # 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. # Avoid redundant length argument.
dotnet_diagnostic.CA1514.severity = warning dotnet_diagnostic.CA1514.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
# Consider making public types internal.
dotnet_diagnostic.CA1515.severity = warning
# Use cross-platform intrinsics.
dotnet_diagnostic.CA1516.severity = warning
### Naming Rules ### Naming Rules
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/naming-warnings ### 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'. # Provide memory-based overrides of async methods when subclassing 'Stream'.
dotnet_diagnostic.CA1844.severity = warning dotnet_diagnostic.CA1844.severity = warning
# Use span-based 'string.Concat'. # Use span-based 'string.Concat'. (Not available on mono)
dotnet_diagnostic.CA1845.severity = warning dotnet_diagnostic.CA1845.severity = none
# Prefer AsSpan over Substring. # Prefer AsSpan over Substring.
dotnet_diagnostic.CA1846.severity = warning dotnet_diagnostic.CA1846.severity = warning
@@ -912,34 +878,34 @@ dotnet_diagnostic.CA1846.severity = warning
dotnet_diagnostic.CA1847.severity = warning dotnet_diagnostic.CA1847.severity = warning
# Call async methods when in an async method. # 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. # Prefer static HashData method over ComputeHash. (Not available on mono)
dotnet_diagnostic.CA1850.severity = warning 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. # Possible multiple enumerations of IEnumerable collection.
#dotnet_code_quality.CA1851.enumeration_methods = #dotnet_code_quality.CA1851.enumeration_methods =
dotnet_code_quality.CA1851.linq_chain_methods = M:OpenRA.Traits.IRenderModifier.Modify* dotnet_code_quality.CA1851.linq_chain_methods = M:OpenRA.Traits.IRenderModifier.Modify*
dotnet_code_quality.CA1851.assume_method_enumerates_parameters = true 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. # 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)'. # 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. # 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(). # 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. # 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. # 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. # Use StartsWith instead of IndexOf.
dotnet_diagnostic.CA1858.severity = warning dotnet_diagnostic.CA1858.severity = warning
@@ -951,24 +917,24 @@ dotnet_diagnostic.CA1860.severity = warning
dotnet_diagnostic.CA1862.severity = warning dotnet_diagnostic.CA1862.severity = warning
# Use 'CompositeFormat'. # 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. # 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. # Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char.
dotnet_diagnostic.CA1865.severity = warning dotnet_diagnostic.CA1865.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
dotnet_diagnostic.CA1866.severity = warning dotnet_diagnostic.CA1866.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
dotnet_diagnostic.CA1867.severity = warning dotnet_diagnostic.CA1867.severity = suggestion # TODO: Change to warning once using .NET 8 or later.
# Unnecessary call to 'Contains' for sets. # 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. # 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. # 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'. # Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull'.
dotnet_diagnostic.CA1871.severity = warning 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'. # Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'.
dotnet_diagnostic.CA1872.severity = warning 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 ### Reliability Rules
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/reliability-warnings ### 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 dotnet_diagnostic.CA2018.severity = warning
# ThreadStatic fields should not use inline initialization. # 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. # Don't call Enumerable.Cast<T> or Enumerable.OfType<T> with incompatible types.
dotnet_diagnostic.CA2021.severity = warning dotnet_diagnostic.CA2021.severity = warning
@@ -1018,18 +972,6 @@ dotnet_diagnostic.CA2021.severity = warning
# Avoid inexact read with Stream.Read. # Avoid inexact read with Stream.Read.
dotnet_diagnostic.CA2022.severity = warning 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 ### Security Rules
### https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/security-warnings ### 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 dotnet_diagnostic.CA2251.severity = warning
# Ensure ThreadStatic is only used with static fields. # 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. # 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'. # Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull'.
dotnet_diagnostic.CA2264.severity = warning dotnet_diagnostic.CA2264.severity = warning
@@ -1318,24 +1260,3 @@ dotnet_diagnostic.RCS1239.severity = warning
# Use element access. # Use element access.
dotnet_diagnostic.RCS1246.severity = warning 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 Version: ENGINE VERSION
- OpenRA Mod: GAME MOD - OpenRA Mod: GAME MOD
- Source: Official download package OR self-compiled OR third-party package - Source: Official download package OR self-compiled OR third-party package
- For self-compiled or third-party packages: Mono version
## Exception log ## 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: on:
push: push:
paths-ignore:
- '*.md'
pull_request: pull_request:
branches: [ bleed, 'prep-*' ] branches: [ bleed, 'prep-*' ]
paths-ignore:
- '*.md'
- '*.py'
permissions: permissions:
contents: read # to fetch code (actions/checkout) contents: read # to fetch code (actions/checkout)
jobs: jobs:
linux: linux:
name: Linux (.NET 8.0) name: Linux (.NET 6.0)
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Clone Repository - name: Remove System .NET
uses: actions/checkout@v6 run: sudo apt-get remove -y dotnet*
- name: Install .NET 8.0 - name: Clone Repository
uses: actions/setup-dotnet@v5 uses: actions/checkout@v4
- name: Install .NET 6.0
uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '8.0.x' dotnet-version: '6.0.x'
- name: Check Code - name: Check Code
run: | run: |
@@ -38,18 +36,36 @@ jobs:
make check-scripts make check-scripts
make TREAT_WARNINGS_AS_ERRORS=true test make TREAT_WARNINGS_AS_ERRORS=true test
windows: linux-mono:
name: Windows (.NET 8.0) name: Linux (mono)
runs-on: windows-2022 runs-on: ubuntu-22.04
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Install .NET 8.0 - name: Check Code
uses: actions/setup-dotnet@v5 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: with:
dotnet-version: '8.0.x' dotnet-version: '6.0.x'
- name: Check Code - name: Check Code
shell: powershell shell: powershell
@@ -61,7 +77,7 @@ jobs:
- name: Check Mods - name: Check Mods
run: | 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:Path = $ENV:Path + ";C:\Program Files (x86)\Lua\5.1\"
$ENV:TREAT_WARNINGS_AS_ERRORS = "true" $ENV:TREAT_WARNINGS_AS_ERRORS = "true"
.\make.ps1 check-scripts .\make.ps1 check-scripts

View File

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

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Prepare Environment - name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV} run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -40,12 +40,12 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Install .NET 8.0 - name: Install .NET 6.0
uses: actions/setup-dotnet@v5 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '8.0.x' dotnet-version: '6.0.x'
- name: Prepare Environment - name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV} run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -65,15 +65,15 @@ jobs:
macos: macos:
name: macOS Disk Image name: macOS Disk Image
runs-on: macos-14 runs-on: macos-13
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Install .NET 8.0 - name: Install .NET 6.0
uses: actions/setup-dotnet@v5 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '8.0.x' dotnet-version: '6.0.x'
- name: Prepare Environment - name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV} run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -101,12 +101,12 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Install .NET 8.0 - name: Install .NET 6.0
uses: actions/setup-dotnet@v5 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: '8.0.x' dotnet-version: '6.0.x'
- name: Prepare Environment - name: Prepare Environment
run: | run: |
@@ -119,37 +119,9 @@ jobs:
mkdir -p build/windows mkdir -p build/windows
./packaging/windows/buildpackage.sh "${GIT_TAG}" "${PWD}/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 - name: Upload Packages
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash shell: bash
run: | run: |
gh release upload "${{ github.ref_name }}" build/windows/*.zip gh release upload ${{ github.ref_name }} build/windows/*
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

4
.gitignore vendored
View File

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

View File

@@ -36,7 +36,6 @@ Also thanks to:
* Andreas Beck (baxtor) * Andreas Beck (baxtor)
* Ang Soon Li (asl97) * Ang Soon Li (asl97)
* Arik Lirette (Angusm3) * Arik Lirette (Angusm3)
* Ashley Newson
* Barnaby Smith (mvi) * Barnaby Smith (mvi)
* Bellator * Bellator
* Bernd Stellwag (burned42) * Bernd Stellwag (burned42)
@@ -55,7 +54,6 @@ Also thanks to:
* Constantin Helmig (CH4Code) * Constantin Helmig (CH4Code)
* D2k Sardaukar * D2k Sardaukar
* D'Arcy Rush (r34ch) * D'Arcy Rush (r34ch)
* Dan Stoian (stoiandan)
* Daniel Derejvanik (Harisson) * Daniel Derejvanik (Harisson)
* Danny Keary (Dan9550) * Danny Keary (Dan9550)
* David Jiménez (Rydra) * David Jiménez (Rydra)
@@ -143,7 +141,6 @@ Also thanks to:
* Riderr3 * Riderr3
* riiga * riiga
* Rikhardur Bjarni Einarsson (WolfGaming) * Rikhardur Bjarni Einarsson (WolfGaming)
* Robert Nordan (robpvn)
* Sascha Biedermann (bidifx) * Sascha Biedermann (bidifx)
* Sean Hunt (coppro) * Sean Hunt (coppro)
* Shawn Collins (UberWaffe) * 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. 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. 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> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Optimize>true</Optimize> <Optimize>true</Optimize>
<LangVersion>12</LangVersion> <LangVersion>9</LangVersion>
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
<EngineRootPath Condition="'$(EngineRootPath)' == ''">..</EngineRootPath> <EngineRootPath Condition="'$(EngineRootPath)' == ''">..</EngineRootPath>
<OutputPath>$(EngineRootPath)/bin</OutputPath> <OutputPath>$(EngineRootPath)/bin</OutputPath>
@@ -18,7 +18,8 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework Condition="'$(MSBuildRuntimeType)'!='Mono'">net6.0</TargetFramework>
<TargetFramework Condition="'$(MSBuildRuntimeType)'=='Mono'">netstandard2.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
@@ -52,8 +53,9 @@
<!-- StyleCop/Roslynator --> <!-- StyleCop/Roslynator -->
<ItemGroup> <ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
<PackageReference Include="Roslynator.Analyzers" Version="4.13.0" PrivateAssets="All" /> <!-- Roslynator analyzers fail to run under Mono (AD0001) -->
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.13.0" PrivateAssets="All" /> <PackageReference Include="Roslynator.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

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

View File

@@ -8,7 +8,7 @@ Windows
Compiling OpenRA requires the following dependencies: Compiling OpenRA requires the following dependencies:
* [Windows PowerShell >= 4.0](https://microsoft.com/powershell) (included by default in recent Windows 10 versions) * [Windows PowerShell >= 4.0](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. 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 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`. 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 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: # to compile, run:
# make # 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: # 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: # 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: # 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: # 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 # to compile and install Red Alert, Tiberian Dawn, and Dune 2000
# using system libraries for native dependencies, run: # using system libraries for native dependencies, run:
@@ -45,12 +48,15 @@ gameinstalldir ?= $(libdir)/openra
# Toolchain # Toolchain
CWD = $(shell pwd) CWD = $(shell pwd)
MSBUILD = msbuild -verbosity:m -nologo
DOTNET = dotnet DOTNET = dotnet
MONO = mono
RM = rm RM = rm
RM_R = $(RM) -r RM_R = $(RM) -r
RM_F = $(RM) -f RM_F = $(RM) -f
RM_RF = $(RM) -rf RM_RF = $(RM) -rf
RUNTIME ?= net6
CONFIGURATION ?= Release CONFIGURATION ?= Release
DOTNET_RID = $(shell ${DOTNET} --info | grep RID: | cut -w -f3) DOTNET_RID = $(shell ${DOTNET} --info | grep RID: | cut -w -f3)
ARCH_X64 = $(shell echo ${DOTNET_RID} | grep x64) ARCH_X64 = $(shell echo ${DOTNET_RID} | grep x64)
@@ -85,12 +91,18 @@ endif
# #
all: all:
@echo "Compiling in ${CONFIGURATION} mode..." @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) @$(DOTNET) build -c ${CONFIGURATION} -nologo -p:TargetPlatform=$(TARGETPLATFORM)
endif
ifeq ($(TARGETPLATFORM), unix-generic) ifeq ($(TARGETPLATFORM), unix-generic)
@./configure-system-libraries.sh @./configure-system-libraries.sh
endif endif
@./fetch-geoip.sh @./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 # Deleting the intermediate / output directories ensures the build directory is actually clean
clean: clean:
@-$(RM_RF) ./bin ./*/obj @-$(RM_RF) ./bin ./*/obj
@@ -99,8 +111,12 @@ clean:
check: check:
@echo @echo
@echo "Compiling in Debug mode..." @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) clean -c Debug --nologo --verbosity minimal
@$(DOTNET) build -c Debug -nologo -warnaserror -p:TargetPlatform=$(TARGETPLATFORM) @$(DOTNET) build -c Debug -nologo -warnaserror -p:TargetPlatform=$(TARGETPLATFORM)
endif
ifeq ($(TARGETPLATFORM), unix-generic) ifeq ($(TARGETPLATFORM), unix-generic)
@./configure-system-libraries.sh @./configure-system-libraries.sh
endif endif
@@ -119,19 +135,15 @@ check-scripts:
test: all test: all
@echo @echo
@echo "Testing Tiberian Sun mod MiniYAML..." @echo "Testing Tiberian Sun mod MiniYAML..."
@./utility.sh ts-content --check-yaml
@./utility.sh ts --check-yaml @./utility.sh ts --check-yaml
@echo @echo
@echo "Testing Dune 2000 mod MiniYAML..." @echo "Testing Dune 2000 mod MiniYAML..."
@./utility.sh d2k-content --check-yaml
@./utility.sh d2k --check-yaml @./utility.sh d2k --check-yaml
@echo @echo
@echo "Testing Tiberian Dawn mod MiniYAML..." @echo "Testing Tiberian Dawn mod MiniYAML..."
@./utility.sh cnc-content --check-yaml
@./utility.sh cnc --check-yaml @./utility.sh cnc --check-yaml
@echo @echo
@echo "Testing Red Alert mod MiniYAML..." @echo "Testing Red Alert mod MiniYAML..."
@./utility.sh ra-content --check-yaml
@./utility.sh ra --check-yaml @./utility.sh ra --check-yaml
tests: tests:
@@ -149,7 +161,7 @@ endif
@sh -c '. ./packaging/functions.sh; set_mod_version "$(VERSION)" mods/*/mod.yaml' @sh -c '. ./packaging/functions.sh; set_mod_version "$(VERSION)" mods/*/mod.yaml'
install: 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' @sh -c '. ./packaging/functions.sh; install_data $(CWD) $(DESTDIR)$(gameinstalldir) cnc d2k ra'
install-linux-shortcuts: install-linux-shortcuts:
@@ -166,21 +178,24 @@ help:
@echo 'to compile, run:' @echo 'to compile, run:'
@echo ' make' @echo ' make'
@echo @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 'to compile using system libraries for native dependencies, run:'
@echo ' make TARGETPLATFORM=unix-generic' @echo ' make [RUNTIME=net6] TARGETPLATFORM=unix-generic'
@echo @echo
@echo 'to check the official mods for erroneous yaml files, run:' @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
@echo 'to check the engine and official mod dlls for code style violations, run:' @echo 'to check the engine and official mod dlls for code style violations, run:'
@echo ' make check' @echo ' make [RUNTIME=net6] check'
@echo @echo
@echo 'to compile and install Red Alert, Tiberian Dawn, and Dune 2000 run:' @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
@echo 'to compile and install Red Alert, Tiberian Dawn, and Dune 2000' @echo 'to compile and install Red Alert, Tiberian Dawn, and Dune 2000'
@echo 'using system libraries for native dependencies, run:' @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
@echo 'to install FreeDesktop startup scripts, desktop files, icons, and MIME metadata' @echo 'to install FreeDesktop startup scripts, desktop files, icons, and MIME metadata'
@echo ' make install-linux-shortcuts' @echo ' make install-linux-shortcuts'

View File

@@ -89,21 +89,21 @@ namespace OpenRA
sealed class ConditionState sealed class ConditionState
{ {
/// <summary>Delegates that have registered to be notified when this condition changes.</summary> /// <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> /// <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> /// <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; int nextConditionToken = 1;
/// <summary>Cache of condition -> enabled state for quick evaluation of token counter conditions.</summary> /// <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> /// <summary>Read-only version of conditionCache that is passed to IConditionConsumers.</summary>
readonly IReadOnlyDictionary<string, int> readOnlyConditionCache; readonly IReadOnlyDictionary<string, int> readOnlyConditionCache;
@@ -123,9 +123,6 @@ namespace OpenRA
readonly IEnumerable<WPos> enabledTargetableWorldPositions; readonly IEnumerable<WPos> enabledTargetableWorldPositions;
bool created; bool created;
IEnumerable<IRenderable> renderables;
WorldRenderer lastWorldRenderer;
internal Actor(World world, string name, TypeDictionary initDict) internal Actor(World world, string name, TypeDictionary initDict)
{ {
var duplicateInit = initDict.WithInterface<ISingleInstanceInit>().GroupBy(i => i.GetType()) var duplicateInit = initDict.WithInterface<ISingleInstanceInit>().GroupBy(i => i.GetType())
@@ -290,18 +287,11 @@ namespace OpenRA
public IEnumerable<IRenderable> Render(WorldRenderer wr) public IEnumerable<IRenderable> Render(WorldRenderer wr)
{ {
if (lastWorldRenderer != wr)
{
// PERF: Cache the enumerable to reduce allocations.
lastWorldRenderer = wr;
renderables = Renderables(wr);
}
// PERF: Avoid LINQ. // PERF: Avoid LINQ.
var modifiedRenderables = renderables; var renderables = Renderables(wr);
foreach (var modifier in renderModifiers) foreach (var modifier in renderModifiers)
modifiedRenderables = modifier.ModifyRender(this, wr, modifiedRenderables); renderables = modifier.ModifyRender(this, wr, renderables);
return modifiedRenderables; return renderables;
} }
IEnumerable<IRenderable> Renderables(WorldRenderer wr) IEnumerable<IRenderable> Renderables(WorldRenderer wr)
@@ -514,7 +504,7 @@ namespace OpenRA
if (!visibilityModifier.IsVisible(this, player)) if (!visibilityModifier.IsVisible(this, player))
return false; return false;
return defaultVisibility?.IsVisible(this, player) ?? true; return defaultVisibility.IsVisible(this, player);
} }
public BitSet<TargetableType> GetAllTargetTypes() public BitSet<TargetableType> GetAllTargetTypes()
@@ -551,7 +541,7 @@ namespace OpenRA
if (EnabledTargetablePositions.Any()) if (EnabledTargetablePositions.Any())
return enabledTargetableWorldPositions; return enabledTargetableWorldPositions;
return [CenterPosition]; return new[] { CenterPosition };
} }
#region Conditions #region Conditions

View File

@@ -78,9 +78,17 @@ namespace OpenRA
return new MPos(X, Y); return new MPos(X, Y);
// Convert from RectangularIsometric cell (x, y) position to rectangular map position (u, v) // 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 v = X + Y;
var u = (v - (v & 1)) / 2 - Y;
return new MPos(u, v); return new MPos(u, v);
} }

View File

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

View File

@@ -20,7 +20,7 @@ namespace OpenRA
public static class CryptoUtil public static class CryptoUtil
{ {
// Fixed byte pattern for the OID header // Fixed byte pattern for the OID header
static readonly byte[] OIDHeader = [0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0]; static readonly byte[] OIDHeader = { 0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0 };
static readonly char[] HexUpperAlphabet = "0123456789ABCDEF".ToArray(); static readonly char[] HexUpperAlphabet = "0123456789ABCDEF".ToArray();
static readonly char[] HexLowerAlphabet = "0123456789abcdef".ToArray(); static readonly char[] HexLowerAlphabet = "0123456789abcdef".ToArray();
@@ -210,7 +210,8 @@ namespace OpenRA
using (var rsa = new RSACryptoServiceProvider()) using (var rsa = new RSACryptoServiceProvider())
{ {
rsa.ImportParameters(parameters); 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) catch (Exception e)
@@ -235,7 +236,8 @@ namespace OpenRA
using (var rsa = new RSACryptoServiceProvider()) using (var rsa = new RSACryptoServiceProvider())
{ {
rsa.ImportParameters(parameters); 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) catch (Exception e)
@@ -250,12 +252,14 @@ namespace OpenRA
public static string SHA1Hash(Stream data) 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) 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) public static string SHA1Hash(string data)

View File

@@ -12,7 +12,6 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using OpenRA.FileFormats; using OpenRA.FileFormats;
@@ -29,7 +28,7 @@ namespace OpenRA
public readonly string Id; public readonly string Id;
public readonly string Version; public readonly string Version;
public readonly string LaunchPath; public readonly string LaunchPath;
public readonly ImmutableArray<string> LaunchArgs; public readonly string[] LaunchArgs;
public Sprite Icon { get; internal set; } public Sprite Icon { get; internal set; }
public Sprite Icon2x { get; internal set; } public Sprite Icon2x { get; internal set; }
public Sprite Icon3x { get; internal set; } public Sprite Icon3x { get; internal set; }
@@ -41,7 +40,7 @@ namespace OpenRA
public class ExternalMods : IReadOnlyDictionary<string, ExternalMod> public class ExternalMods : IReadOnlyDictionary<string, ExternalMod>
{ {
readonly Dictionary<string, ExternalMod> mods = []; readonly Dictionary<string, ExternalMod> mods = new();
readonly SheetBuilder sheetBuilder; readonly SheetBuilder sheetBuilder;
Sheet CreateSheet() Sheet CreateSheet()
@@ -123,13 +122,13 @@ namespace OpenRA
return; return;
var key = ExternalMod.MakeKey(mod); 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("Id", mod.Id),
new MiniYamlNode("Version", mod.Metadata.Version), new MiniYamlNode("Version", mod.Metadata.Version),
new MiniYamlNode("LaunchPath", launchPath), new MiniYamlNode("LaunchPath", launchPath),
new MiniYamlNode("LaunchArgs", new[] { "Game.Mod=" + mod.Id }.Concat(launchArgs).JoinWith(", ")) new MiniYamlNode("LaunchArgs", new[] { "Game.Mod=" + mod.Id }.Concat(launchArgs).JoinWith(", "))
])); }));
var iconNodes = new List<MiniYamlNode>(); var iconNodes = new List<MiniYamlNode>();

View File

@@ -11,11 +11,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Support; 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)); 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; var windingNumber = 0;
@@ -121,11 +119,17 @@ namespace OpenRA
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, V v) 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. // 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) if (!exists)
value = v; value = v;
return value; 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) 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); 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>( public static Dictionary<TKey, TSource> ToDictionaryWithConflictLog<TSource, TKey>(
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
string debugName, Func<TKey, string> logKey, Func<TSource, string> logValue) string debugName, Func<TKey, string> logKey, Func<TSource, string> logValue)
@@ -456,14 +465,14 @@ namespace OpenRA
// Check for a key conflict: // Check for a key conflict:
if (!output.TryAdd(key, element)) if (!output.TryAdd(key, element))
{ {
dupKeys ??= []; dupKeys ??= new Dictionary<TKey, List<string>>();
if (!dupKeys.TryGetValue(key, out var dupKeyMessages)) if (!dupKeys.TryGetValue(key, out var dupKeyMessages))
{ {
// Log the initial conflicting value already inserted: // Log the initial conflicting value already inserted:
dupKeyMessages = dupKeyMessages = new List<string>
[ {
logValue(output[key]) logValue(output[key])
]; };
dupKeys.Add(key, dupKeyMessages); dupKeys.Add(key, dupKeyMessages);
} }
@@ -478,7 +487,7 @@ namespace OpenRA
var badKeysFormatted = new StringBuilder( var badKeysFormatted = new StringBuilder(
$"{debugName}, duplicate values found for the following keys: "); $"{debugName}, duplicate values found for the following keys: ");
foreach (var p in dupKeys) 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()); throw new ArgumentException(badKeysFormatted.ToString());
} }
} }
@@ -501,16 +510,44 @@ namespace OpenRA
return result; 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) public static byte ParseByteInvariant(string s)
{ {
return byte.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo); 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) public static short ParseInt16Invariant(string s)
{ {
return short.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo); return short.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
@@ -521,22 +558,6 @@ namespace OpenRA
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo); 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) public static bool TryParseInt32Invariant(string s, out int i)
{ {
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i); return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
@@ -547,17 +568,6 @@ namespace OpenRA
return long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i); return long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
} }
public static 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) public static string ToStringInvariant(this byte i)
{ {
return i.ToString(NumberFormatInfo.InvariantInfo); return i.ToString(NumberFormatInfo.InvariantInfo);
@@ -573,16 +583,6 @@ namespace OpenRA
return i.ToString(NumberFormatInfo.InvariantInfo); 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) public static string ToStringInvariant(this int i, string format)
{ {
return i.ToString(format, NumberFormatInfo.InvariantInfo); return i.ToString(format, NumberFormatInfo.InvariantInfo);
@@ -665,7 +665,7 @@ namespace OpenRA
if (index == -1) if (index == -1)
{ {
// The remaining string is an empty string // The remaining string is an empty string
str = []; str = ReadOnlySpan<char>.Empty;
Current = span; Current = span;
return true; return true;
} }
@@ -677,4 +677,27 @@ namespace OpenRA
public ReadOnlySpan<char> Current { get; private set; } 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 #endregion
using System; using System;
using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.Serialization;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Support; using OpenRA.Support;
@@ -25,8 +24,9 @@ namespace OpenRA
{ {
public static class FieldLoader public static class FieldLoader
{ {
const char Comma = ','; const char SplitComma = ',';
[Serializable]
public class MissingFieldsException : YamlException public class MissingFieldsException : YamlException
{ {
public readonly string[] Missing; public readonly string[] Missing;
@@ -46,10 +46,17 @@ namespace OpenRA
Header = missing.Length > 1 ? header : headerSingle ?? header; Header = missing.Length > 1 ? header : headerSingle ?? header;
Missing = missing; 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) => public static Func<string, Type, string, object> InvalidValueAction = (s, t, f) =>
throw new YamlException($"FieldLoader: Cannot parse `{s}` into `{f}.{t}`"); throw new YamlException($"FieldLoader: Cannot parse `{s}` into `{f}.{t}` ");
public static Action<string, Type> UnknownFieldAction = (s, f) => public static Action<string, Type> UnknownFieldAction = (s, f) =>
throw new NotImplementedException($"FieldLoader: Missing field `{s}` on `{f.Name}`"); throw new NotImplementedException($"FieldLoader: Missing field `{s}` on `{f.Name}`");
@@ -61,8 +68,8 @@ namespace OpenRA
static readonly ConcurrentCache<string, IntegerExpression> IntegerExpressionCache = static readonly ConcurrentCache<string, IntegerExpression> IntegerExpressionCache =
new(expression => new IntegerExpression(expression)); new(expression => new IntegerExpression(expression));
static readonly FrozenDictionary<Type, Func<string, Type, string, object>> TypeParsers = static readonly Dictionary<Type, Func<string, Type, string, MemberInfo, object>> TypeParsers =
new Dictionary<Type, Func<string, Type, string, object>> new()
{ {
{ typeof(int), ParseInt }, { typeof(int), ParseInt },
{ typeof(ushort), ParseUShort }, { typeof(ushort), ParseUShort },
@@ -85,6 +92,7 @@ namespace OpenRA
{ typeof(CVec[]), ParseCVecArray }, { typeof(CVec[]), ParseCVecArray },
{ typeof(BooleanExpression), ParseBooleanExpression }, { typeof(BooleanExpression), ParseBooleanExpression },
{ typeof(IntegerExpression), ParseIntegerExpression }, { typeof(IntegerExpression), ParseIntegerExpression },
{ typeof(Enum), ParseEnum },
{ typeof(bool), ParseBool }, { typeof(bool), ParseBool },
{ typeof(int2[]), ParseInt2Array }, { typeof(int2[]), ParseInt2Array },
{ typeof(Size), ParseSize }, { typeof(Size), ParseSize },
@@ -93,44 +101,23 @@ namespace OpenRA
{ typeof(float3), ParseFloat3 }, { typeof(float3), ParseFloat3 },
{ typeof(Rectangle), ParseRectangle }, { typeof(Rectangle), ParseRectangle },
{ typeof(DateTime), ParseDateTime } { typeof(DateTime), ParseDateTime }
}.ToFrozenDictionary(); };
static readonly FrozenDictionary<Type, Func<string, Type, string, MiniYaml, object>> GenericTypeParsers = static readonly Dictionary<Type, Func<string, Type, string, MiniYaml, MemberInfo, object>> GenericTypeParsers =
new Dictionary<Type, Func<string, Type, string, MiniYaml, object>> new()
{ {
{ typeof(HashSet<>), ParseHashSetOrList }, { typeof(HashSet<>), ParseHashSetOrList },
{ typeof(List<>), ParseHashSetOrList }, { typeof(List<>), ParseHashSetOrList },
{ typeof(Dictionary<,>), ParseDictionary }, { typeof(Dictionary<,>), ParseDictionary },
{ typeof(ImmutableArray<>), ParseImmutableArray },
{ typeof(FrozenSet<>), ParseFrozenSet },
{ typeof(FrozenDictionary<,>), ParseFrozenDictionary },
{ typeof(BitSet<>), ParseBitSet }, { typeof(BitSet<>), ParseBitSet },
{ typeof(Nullable<>), ParseNullable }, { typeof(Nullable<>), ParseNullable },
}.ToFrozenDictionary(); };
static readonly object BoxedTrue = true; static readonly object BoxedTrue = true;
static readonly object BoxedFalse = false; static readonly object BoxedFalse = false;
static readonly object[] BoxedInts = Exts.MakeArray(33, i => (object)i); static readonly object[] BoxedInts = Exts.MakeArray(33, i => (object)i);
static readonly MethodInfo ToImmutableArray = static object ParseInt(string fieldName, Type fieldType, string value, MemberInfo field)
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)
{ {
if (Exts.TryParseInt32Invariant(value, out var res)) if (Exts.TryParseInt32Invariant(value, out var res))
{ {
@@ -142,48 +129,48 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName); 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 res;
return InvalidValueAction(value, fieldType, fieldName); 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 res;
return InvalidValueAction(value, fieldType, fieldName); 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)) if (value != null && float.TryParse(value.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var res))
return res; return res * (value.Contains('%') ? 0.01f : 1f);
return InvalidValueAction(value, fieldType, fieldName); 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)) if (value != null && decimal.TryParse(value.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var res))
return res * (value.Contains('%') ? 0.01m : 1m); return res * (value.Contains('%') ? 0.01m : 1m);
return InvalidValueAction(value, fieldType, fieldName); 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; 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 color;
return InvalidValueAction(value, fieldType, fieldName); 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)) if (Hotkey.TryParse(value, out var res))
return res; return res;
@@ -191,12 +178,12 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName); 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]; 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)) if (WDist.TryParse(value, out var res))
return res; return res;
@@ -204,11 +191,11 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName); 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) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma);
if (parts.Length == 3 if (parts.Length == 3
&& WDist.TryParse(parts[0], out var rx) && WDist.TryParse(parts[0], out var rx)
&& WDist.TryParse(parts[1], out var ry) && WDist.TryParse(parts[1], out var ry)
@@ -219,11 +206,11 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName); 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) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma);
if (parts.Length % 3 != 0) if (parts.Length % 3 != 0)
return InvalidValueAction(value, fieldType, fieldName); 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 + 1], out var ry)
&& WDist.TryParse(parts[3 * i + 2], out var rz)) && WDist.TryParse(parts[3 * i + 2], out var rz))
vecs[i] = new WVec(rx, ry, rz); vecs[i] = new WVec(rx, ry, rz);
else
return InvalidValueAction(value, fieldType, fieldName);
} }
return vecs; return vecs;
@@ -246,11 +231,11 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName); 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) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma);
if (parts.Length == 3 if (parts.Length == 3
&& WDist.TryParse(parts[0], out var rx) && WDist.TryParse(parts[0], out var rx)
&& WDist.TryParse(parts[1], out var ry) && WDist.TryParse(parts[1], out var ry)
@@ -261,18 +246,18 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName); 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)) if (Exts.TryParseInt32Invariant(value, out var res))
return new WAngle(res); return new WAngle(res);
return InvalidValueAction(value, fieldType, fieldName); 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) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma);
if (parts.Length == 3 if (parts.Length == 3
&& Exts.TryParseInt32Invariant(parts[0], out var rr) && Exts.TryParseInt32Invariant(parts[0], out var rr)
&& Exts.TryParseInt32Invariant(parts[1], out var rp) && Exts.TryParseInt32Invariant(parts[1], out var rp)
@@ -283,31 +268,27 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName); 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) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 3 if (parts.Length == 3)
&& Exts.TryParseInt32Invariant(parts[0], out var x) return new CPos(
&& Exts.TryParseInt32Invariant(parts[1], out var y) Exts.ParseInt32Invariant(parts[0]),
&& Exts.TryParseByteInvariant(parts[2], out var layer)) Exts.ParseInt32Invariant(parts[1]),
return new CPos(x, y, layer); Exts.ParseByteInvariant(parts[2]));
return new CPos(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
if (parts.Length == 2
&& Exts.TryParseInt32Invariant(parts[0], out x)
&& Exts.TryParseInt32Invariant(parts[1], out y))
return new CPos(x, y);
} }
return InvalidValueAction(value, fieldType, fieldName); 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) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma);
if (parts.Length % 2 != 0) if (parts.Length % 2 != 0)
return InvalidValueAction(value, fieldType, fieldName); return InvalidValueAction(value, fieldType, fieldName);
@@ -315,11 +296,9 @@ namespace OpenRA
var vecs = new CPos[parts.Length / 2]; var vecs = new CPos[parts.Length / 2];
for (var i = 0; i < vecs.Length; i++) for (var i = 0; i < vecs.Length; i++)
{ {
if (Exts.TryParseInt32Invariant(parts[2 * i], out var rx) if (int.TryParse(parts[2 * i], out var rx)
&& Exts.TryParseInt32Invariant(parts[2 * i + 1], out var ry)) && int.TryParse(parts[2 * i + 1], out var ry))
vecs[i] = new CPos(rx, ry); vecs[i] = new CPos(rx, ry);
else
return InvalidValueAction(value, fieldType, fieldName);
} }
return vecs; return vecs;
@@ -328,25 +307,22 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName); 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) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2 return new CVec(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
&& Exts.TryParseInt32Invariant(parts[0], out var x)
&& Exts.TryParseInt32Invariant(parts[1], out var y))
return new CVec(x, y);
} }
return InvalidValueAction(value, fieldType, fieldName); 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) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma);
if (parts.Length % 2 != 0) if (parts.Length % 2 != 0)
return InvalidValueAction(value, fieldType, fieldName); return InvalidValueAction(value, fieldType, fieldName);
@@ -354,11 +330,9 @@ namespace OpenRA
var vecs = new CVec[parts.Length / 2]; var vecs = new CVec[parts.Length / 2];
for (var i = 0; i < vecs.Length; i++) for (var i = 0; i < vecs.Length; i++)
{ {
if (Exts.TryParseInt32Invariant(parts[2 * i], out var rx) if (int.TryParse(parts[2 * i], out var rx)
&& Exts.TryParseInt32Invariant(parts[2 * i + 1], out var ry)) && int.TryParse(parts[2 * i + 1], out var ry))
vecs[i] = new CVec(rx, ry); vecs[i] = new CVec(rx, ry);
else
return InvalidValueAction(value, fieldType, fieldName);
} }
return vecs; return vecs;
@@ -367,7 +341,7 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName); 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) if (value != null)
{ {
@@ -377,14 +351,14 @@ namespace OpenRA
} }
catch (InvalidDataException e) 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); 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) if (value != null)
{ {
@@ -394,50 +368,44 @@ namespace OpenRA
} }
catch (InvalidDataException e) 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); 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. try
if (Enum.TryParse(fieldType, value, true, out var enumValue))
{ {
return enumValue; return Enum.Parse(fieldType, value, true);
} }
catch (ArgumentException)
{
return InvalidValueAction(value, fieldType, fieldName); 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 result ? BoxedTrue : BoxedFalse;
return InvalidValueAction(value, fieldType, fieldName); 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) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length % 2 != 0) if (parts.Length % 2 != 0)
return InvalidValueAction(value, fieldType, fieldName); return InvalidValueAction(value, fieldType, fieldName);
var ints = new int2[parts.Length / 2]; var ints = new int2[parts.Length / 2];
for (var i = 0; i < ints.Length; i++) for (var i = 0; i < ints.Length; i++)
{ ints[i] = new int2(Exts.ParseInt32Invariant(parts[2 * i]), Exts.ParseInt32Invariant(parts[2 * i + 1]));
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);
}
return ints; return ints;
} }
@@ -445,130 +413,109 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName); 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) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2 return new Size(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
&& Exts.TryParseInt32Invariant(parts[0], out var width)
&& Exts.TryParseInt32Invariant(parts[1], out var height))
return new Size(width, height);
} }
return InvalidValueAction(value, fieldType, fieldName); 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) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2 if (parts.Length != 2)
&& Exts.TryParseInt32Invariant(parts[0], out var x) return InvalidValueAction(value, fieldType, fieldName);
&& Exts.TryParseInt32Invariant(parts[1], out var y))
return new int2(x, y); return new int2(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
} }
return InvalidValueAction(value, fieldType, fieldName); 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) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2 float xx = 0;
&& Exts.TryParseFloatOrPercentInvariant(parts[0], out var x) float yy = 0;
&& Exts.TryParseFloatOrPercentInvariant(parts[1], out var y)) if (float.TryParse(parts[0].Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var res))
return new float2(x, y); 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); 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) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 3 float.TryParse(parts[0], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var x);
&& Exts.TryParseFloatOrPercentInvariant(parts[0], out var x) float.TryParse(parts[1], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var y);
&& Exts.TryParseFloatOrPercentInvariant(parts[1], out var y)
&& Exts.TryParseFloatOrPercentInvariant(parts[2], out var z))
return new float3(x, y, z);
// z component is optional for compatibility with older float2 definitions // z component is optional for compatibility with older float2 definitions
if (parts.Length == 2 float z = 0;
&& Exts.TryParseFloatOrPercentInvariant(parts[0], out x) if (parts.Length > 2)
&& Exts.TryParseFloatOrPercentInvariant(parts[1], out y)) float.TryParse(parts[2], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out z);
return new float3(x, y, 0);
return new float3(x, y, z);
} }
return InvalidValueAction(value, fieldType, fieldName); 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) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 4 return new Rectangle(
&& Exts.TryParseInt32Invariant(parts[0], out var x) Exts.ParseInt32Invariant(parts[0]),
&& Exts.TryParseInt32Invariant(parts[1], out var y) Exts.ParseInt32Invariant(parts[1]),
&& Exts.TryParseInt32Invariant(parts[2], out var width) Exts.ParseInt32Invariant(parts[2]),
&& Exts.TryParseInt32Invariant(parts[3], out var height)) Exts.ParseInt32Invariant(parts[3]));
return new Rectangle(x, y, width, height);
} }
return InvalidValueAction(value, fieldType, fieldName); 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)) if (DateTime.TryParseExact(value, "yyyy-MM-dd HH-mm-ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dt))
return dt; return dt;
return InvalidValueAction(value, fieldType, fieldName); return InvalidValueAction(value, fieldType, fieldName);
} }
static object ParseArray(string fieldName, Type fieldType, string value) static object ParseHashSetOrList(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
{
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)
{ {
if (value == null) if (value == null)
return Activator.CreateInstance(fieldType); 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 set = Activator.CreateInstance(fieldType, parts.Length);
var arguments = fieldType.GetGenericArguments(); var arguments = fieldType.GetGenericArguments();
var addMethod = fieldType.GetMethod(nameof(List<object>.Add), arguments); var addMethod = fieldType.GetMethod(nameof(List<object>.Add), arguments);
var addArgs = new object[1]; var addArgs = new object[1];
for (var i = 0; i < parts.Length; i++) 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); addMethod.Invoke(set, addArgs);
} }
return set; 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) if (yaml == null)
return Activator.CreateInstance(fieldType); return Activator.CreateInstance(fieldType);
@@ -579,97 +526,34 @@ namespace OpenRA
var addArgs = new object[2]; var addArgs = new object[2];
foreach (var node in yaml.Nodes) foreach (var node in yaml.Nodes)
{ {
addArgs[0] = GetValue(fieldName, arguments[0], node.Key); addArgs[0] = GetValue(fieldName, arguments[0], node.Key, field);
addArgs[1] = GetValue(fieldName, arguments[1], node.Value); addArgs[1] = GetValue(fieldName, arguments[1], node.Value, field);
addMethod.Invoke(dict, addArgs); addMethod.Invoke(dict, addArgs);
} }
return dict; return dict;
} }
static object ParseImmutableArray(string fieldName, Type fieldType, string value, MiniYaml yaml) static object ParseBitSet(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
{
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)
{ {
if (value != null) if (value != null)
{ {
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
var ctor = fieldType.GetConstructor([typeof(string[])]); var ctor = fieldType.GetConstructor(new[] { typeof(string[]) });
return ctor.Invoke([parts]); return ctor.Invoke(new object[] { parts.Select(p => p.Trim()).ToArray() });
}
else
{
var ctor = fieldType.GetConstructor([typeof(string[])]);
return ctor.Invoke([Array.Empty<string>()]);
}
} }
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)) if (string.IsNullOrEmpty(value))
return null; return null;
var innerType = fieldType.GetGenericArguments()[0]; var innerType = fieldType.GetGenericArguments()[0];
var innerValue = GetValue("Nullable<T>", innerType, value); var innerValue = GetValue("Nullable<T>", innerType, value, field);
return fieldType.GetConstructor([innerType]).Invoke([innerValue]); return fieldType.GetConstructor(new[] { innerType }).Invoke(new[] { innerValue });
} }
public static void Load(object self, MiniYaml my) public static void Load(object self, MiniYaml my)
@@ -690,7 +574,7 @@ namespace OpenRA
val = fli.Loader(my); val = fli.Loader(my);
else else
{ {
missing ??= []; missing ??= new List<string>();
missing.Add(fli.YamlName); missing.Add(fli.YamlName);
continue; continue;
} }
@@ -701,7 +585,7 @@ namespace OpenRA
{ {
if (fli.Attribute.Required) if (fli.Attribute.Required)
{ {
missing ??= []; missing ??= new List<string>();
missing.Add(fli.YamlName); missing.Add(fli.YamlName);
} }
@@ -723,7 +607,7 @@ namespace OpenRA
if (!md.TryGetValue(yamlName, out var yaml)) if (!md.TryGetValue(yamlName, out var yaml))
return false; return false;
ret = GetValue(field.Name, field.FieldType, yaml); ret = GetValue(field.Name, field.FieldType, yaml, field);
return true; return true;
} }
@@ -734,7 +618,7 @@ namespace OpenRA
return t; 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; const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
@@ -743,14 +627,18 @@ namespace OpenRA
var field = target.GetType().GetField(key, Flags); var field = target.GetType().GetField(key, Flags);
if (field != null) 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; return;
} }
var prop = target.GetType().GetProperty(key, Flags); var prop = target.GetType().GetProperty(key, Flags);
if (prop != null) 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; return;
} }
@@ -762,34 +650,48 @@ namespace OpenRA
return (T)GetValue(field, typeof(T), value, null); 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); 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(); value = value?.Trim();
if (fieldType.IsGenericType) if (fieldType.IsGenericType)
{ {
if (GenericTypeParsers.TryGetValue(fieldType.GetGenericTypeDefinition(), out var parseFuncGeneric)) if (GenericTypeParsers.TryGetValue(fieldType.GetGenericTypeDefinition(), out var parseFuncGeneric))
return parseFuncGeneric(fieldName, fieldType, value, yaml); return parseFuncGeneric(fieldName, fieldType, value, yaml, field);
} }
else else
{ {
if (TypeParsers.TryGetValue(fieldType, out var parseFunc)) if (TypeParsers.TryGetValue(fieldType, out var parseFunc))
return parseFunc(fieldName, fieldType, value); return parseFunc(fieldName, fieldType, value, field);
if (fieldType.IsArray && fieldType.GetArrayRank() == 1) if (fieldType.IsArray && fieldType.GetArrayRank() == 1)
return ParseArray(fieldName, fieldType, value); {
if (value == null)
return Array.CreateInstance(fieldType.GetElementType(), 0);
if (fieldType.IsEnum) var options = field != null && field.HasAttribute<AllowEmptyEntriesAttribute>() ?
return ParseEnum(fieldName, fieldType, value); 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); var conv = TypeDescriptor.GetConverter(fieldType);
@@ -813,20 +715,21 @@ namespace OpenRA
{ {
public readonly FieldInfo Field; public readonly FieldInfo Field;
public readonly SerializeAttribute Attribute; public readonly SerializeAttribute Attribute;
public readonly string YamlName;
public readonly Func<MiniYaml, object> Loader; 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; Field = field;
Attribute = attr; Attribute = attr;
YamlName = yamlName;
Loader = loader; 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) static FieldLoadInfo[] BuildTypeLoadInfo(Type type)
@@ -841,9 +744,13 @@ namespace OpenRA
if (!sa.Serialize) if (!sa.Serialize)
continue; 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); ret.Add(fli);
} }
@@ -854,21 +761,31 @@ namespace OpenRA
public sealed class IgnoreAttribute : SerializeAttribute public sealed class IgnoreAttribute : SerializeAttribute
{ {
public IgnoreAttribute() public IgnoreAttribute()
: base(serialize: false) { } : base(false) { }
} }
[AttributeUsage(AttributeTargets.Field)] [AttributeUsage(AttributeTargets.Field)]
public sealed class RequireAttribute : SerializeAttribute public sealed class RequireAttribute : SerializeAttribute
{ {
public RequireAttribute() 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)] [AttributeUsage(AttributeTargets.Field)]
public sealed class LoadUsingAttribute : SerializeAttribute public sealed class LoadUsingAttribute : SerializeAttribute
{ {
public LoadUsingAttribute(string loader, bool required = false) public LoadUsingAttribute(string loader, bool required = false)
: base(serialize: true, required, loader) { } {
Loader = loader;
Required = required;
}
} }
[AttributeUsage(AttributeTargets.Field)] [AttributeUsage(AttributeTargets.Field)]
@@ -879,14 +796,18 @@ namespace OpenRA
public bool IsDefault => this == Default; public bool IsDefault => this == Default;
public readonly bool Serialize; public readonly bool Serialize;
public readonly bool Required; public string YamlName;
public readonly string Loader; 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; Serialize = serialize;
Required = required; Required = required;
Loader = loader; AllowEmptyEntries = allowEmptyEntries;
} }
internal Func<MiniYaml, object> GetLoader(Type type) 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 #endregion
using System; using System;
using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@@ -24,38 +22,39 @@ namespace OpenRA
{ {
public static class FieldSaver public static class FieldSaver
{ {
public static MiniYaml Save(object o) public static MiniYaml Save(object o, bool includePrivateByDefault = false)
{ {
var nodes = new List<MiniYamlNode>(); 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 dict = (System.Collections.IDictionary)info.Field.GetValue(o);
var dictNodes = new List<MiniYamlNode>();
foreach (var kvp in dict) foreach (var kvp in dict)
{ {
var key = ((System.Collections.DictionaryEntry)kvp).Key; var key = ((System.Collections.DictionaryEntry)kvp).Key;
var value = ((System.Collections.DictionaryEntry)kvp).Value; 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 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()) 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)); .Where(info => FormatValue(o, info.Field) != FormatValue(from, info.Field));
return new MiniYaml( return new MiniYaml(
@@ -80,35 +79,11 @@ namespace OpenRA
if (t.IsArray && t.GetArrayRank() == 1) if (t.IsArray && t.GetArrayRank() == 1)
return ((Array)v).Cast<object>().Select(FormatValue).JoinWith(", "); return ((Array)v).Cast<object>().Select(FormatValue).JoinWith(", ");
if (t.IsGenericType && if (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(HashSet<>) || t.GetGenericTypeDefinition() == typeof(List<>)))
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<>))))
return ((System.Collections.IEnumerable)v).Cast<object>().Select(FormatValue).JoinWith(", "); return ((System.Collections.IEnumerable)v).Cast<object>().Select(FormatValue).JoinWith(", ");
// This is only for documentation generation // This is only for documentation generation
if (t.IsGenericType && if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
(t.GetGenericTypeDefinition() == typeof(Dictionary<,>) ||
t.GetGenericTypeDefinition()
.BaseTypes()
.Select(bt => bt.IsGenericType ? bt.GetGenericTypeDefinition() : null)
.Any(bt => bt == typeof(FrozenDictionary<,>))))
{ {
var result = new StringBuilder(); var result = new StringBuilder();
var dict = (System.Collections.IDictionary)v; var dict = (System.Collections.IDictionary)v;
@@ -120,7 +95,7 @@ namespace OpenRA
var formattedKey = FormatValue(key); var formattedKey = FormatValue(key);
var formattedValue = FormatValue(value); var formattedValue = FormatValue(value);
result.Append(CultureInfo.InvariantCulture, $"{formattedKey}: {formattedValue}{Environment.NewLine}"); result.Append($"{formattedKey}: {formattedValue}{Environment.NewLine}");
} }
return result.ToString(); return result.ToString();

View File

@@ -10,105 +10,29 @@
#endregion #endregion
using System; using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Text; using System.Text;
using ICSharpCode.SharpZipLib.Checksum;
using ICSharpCode.SharpZipLib.Zip.Compression;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Primitives; using OpenRA.Primitives;
namespace OpenRA.FileFormats 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 class Png
{ {
public const uint ChunkIHDR = 0x49484452; static readonly byte[] Signature = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a };
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];
public int Width { get; } public int Width { get; }
public int Height { get; } public int Height { get; }
public Color[] Palette { get; } public Color[] Palette { get; }
public byte[] Data { get; } public byte[] Data { get; }
public SpriteFrameType Type { 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; public int PixelStride => Type == SpriteFrameType.Indexed8 ? 1 : Type == SpriteFrameType.Rgb24 ? 3 : 4;
@@ -119,50 +43,43 @@ namespace OpenRA.FileFormats
s.Position += 8; s.Position += 8;
var headerParsed = false; var headerParsed = false;
var dataParsed = false; var data = new List<byte>();
byte bitDepth = 8;
Type = SpriteFrameType.Rgba32; Type = SpriteFrameType.Rgba32;
// Use a reusable 8-byte buffer for Length + Type. byte bitDepth = 8;
Span<byte> chunkHeader = stackalloc byte[8];
while (true) while (true)
{ {
if (s.Read(chunkHeader) < 8) var length = IPAddress.NetworkToHostOrder(s.ReadInt32());
throw new EndOfStreamException("Invalid PNG file - no end chunk found."); var type = s.ReadASCII(4);
var content = s.ReadBytes(length);
s.ReadInt32(); // crc
var length = BinaryPrimitives.ReadInt32BigEndian(chunkHeader[..4]); if (!headerParsed && type != "IHDR")
var type = BinaryPrimitives.ReadUInt32BigEndian(chunkHeader[4..]);
if (!headerParsed && type != ChunkIHDR)
throw new InvalidDataException("Invalid PNG file - header does not appear first."); throw new InvalidDataException("Invalid PNG file - header does not appear first.");
using (var ms = new MemoryStream(content))
{
switch (type) switch (type)
{ {
case ChunkIHDR: case "IHDR":
{ {
if (headerParsed) if (headerParsed)
throw new InvalidDataException("Invalid PNG file - duplicate header."); 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 bitDepth = ms.ReadUInt8();
Span<byte> buffer = stackalloc byte[13]; var colorType = (PngColorType)ms.ReadUInt8();
#pragma warning restore CA2014 // Do not use stackalloc in loops
s.ReadBytes(buffer);
Width = BinaryPrimitives.ReadInt32BigEndian(buffer[..4]);
Height = BinaryPrimitives.ReadInt32BigEndian(buffer[4..8]);
bitDepth = buffer[8];
var colorType = (PngColorType)buffer[9];
if (IsPaletted(bitDepth, colorType)) if (IsPaletted(bitDepth, colorType))
Type = SpriteFrameType.Indexed8; Type = SpriteFrameType.Indexed8;
else if (colorType == PngColorType.Color) else if (colorType == PngColorType.Color)
Type = SpriteFrameType.Rgb24; Type = SpriteFrameType.Rgb24;
var compression = buffer[10]; Data = new byte[Width * Height * PixelStride];
// filter = buffer[11] var compression = ms.ReadUInt8();
var interlace = buffer[12]; ms.ReadUInt8(); // filter
var interlace = ms.ReadUInt8();
if (compression != 0) if (compression != 0)
throw new InvalidDataException("Compression method not supported"); throw new InvalidDataException("Compression method not supported");
@@ -170,156 +87,73 @@ namespace OpenRA.FileFormats
if (interlace != 0) if (interlace != 0)
throw new InvalidDataException("Interlacing not supported"); throw new InvalidDataException("Interlacing not supported");
Data = new byte[Width * Height * PixelStride];
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
headerParsed = true; headerParsed = true;
break; break;
} }
case ChunkPLTE: case "PLTE":
{ {
if (length % 3 != 0) Palette = new Color[length / 3];
throw new InvalidDataException("Invalid PLTE chunk length."); for (var i = 0; i < Palette.Length; i++)
var count = length / 3;
Palette = new Color[count];
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
{ {
s.ReadBytes(buffer.AsSpan(0, length)); var r = ms.ReadUInt8(); var g = ms.ReadUInt8(); var b = ms.ReadUInt8();
for (var i = 0; i < count; i++) Palette[i] = Color.FromArgb(r, g, b);
{
var offset = i * 3;
Palette[i] = Color.FromArgb(buffer[offset], buffer[offset + 1], buffer[offset + 2]);
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
} }
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
break; break;
} }
case ChunkTRNS: case "tRNS":
{ {
if (Palette == null) if (Palette == null)
throw new InvalidDataException("Non-Palette indexed PNG are not supported."); throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
var buffer = ArrayPool<byte>.Shared.Rent(length); for (var i = 0; i < length; i++)
try Palette[i] = Color.FromArgb(ms.ReadUInt8(), Palette[i]);
{
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);
}
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
break; break;
} }
case ChunkIDAT: case "IDAT":
if (dataParsed)
throw new InvalidDataException("Invalid PNG file - discontinuous IDAT chunks.");
dataParsed = true;
ProcessIDAT(s, length, bitDepth);
break;
case ChunkTEXT:
{ {
var buffer = ArrayPool<byte>.Shared.Rent(length); data.AddRange(content);
try
{
var span = buffer.AsSpan(0, length);
s.ReadBytes(span);
// 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; break;
} }
case ChunkIEND: case "tEXt":
{ {
if (Type == SpriteFrameType.Indexed8 && Palette == null) var key = ms.ReadASCIIZ();
throw new InvalidDataException("Non-Palette indexed PNG are not supported."); 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; 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 pxStride = PixelStride;
var rowStride = Width * pxStride; var rowStride = Width * pxStride;
var pixelsPerByte = 8 / bitDepth; var pixelsPerByte = 8 / bitDepth;
var sourceRowStride = Exts.IntegerDivisionRoundingAwayFromZero(rowStride, pixelsPerByte); var sourceRowStride = Exts.IntegerDivisionRoundingAwayFromZero(rowStride, pixelsPerByte);
// Custom stream that stitches IDAT chunks together without copying. Span<byte> prevLine = new byte[rowStride];
using var idatStream = new PngIdatStream(s, firstChunkLength);
using var ds = new ZLibStream(idatStream, CompressionMode.Decompress);
// Rent buffer from pool to avoid allocation churn.
var prevLineBuffer = ArrayPool<byte>.Shared.Rent(rowStride);
var prevLine = prevLineBuffer.AsSpan(0, rowStride);
prevLine.Clear();
try
{
for (var y = 0; y < Height; y++) for (var y = 0; y < Height; y++)
{ {
var filterByte = ds.ReadByte(); var filter = (PngFilter)ds.ReadUInt8();
if (filterByte == -1) ds.ReadBytes(Data, y * rowStride, sourceRowStride);
break;
var filter = (PngFilter)filterByte;
var line = Data.AsSpan(y * rowStride, rowStride); 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. // 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. // Unpack to bit depth of 8, yielding 1 pixel per byte.
// This makes life easier for consumers of palleted data. // This makes life easier for consumers of palleted data.
if (bitDepth < 8) if (bitDepth < 8)
{ {
var mask = (1 << bitDepth) - 1; var mask = 0xFF >> (8 - bitDepth);
for (var i = sourceRowStride - 1; i >= 0; i--) for (var i = sourceRowStride - 1; i >= 0; i--)
{ {
var packed = line[i]; var packed = line[i];
@@ -327,7 +161,7 @@ namespace OpenRA.FileFormats
{ {
var dest = i * pixelsPerByte + j; var dest = i * pixelsPerByte + j;
if (dest < line.Length) // Guard against last byte being only partially packed 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: case PngFilter.None:
break; break;
case PngFilter.Sub: case PngFilter.Sub:
for (var i = pxStride; i < line.Length; i++) for (var i = pxStride; i < rowStride; i++)
line[i] += line[i - pxStride]; line[i] += line[i - pxStride];
break; break;
case PngFilter.Up: case PngFilter.Up:
for (var i = 0; i < line.Length; i++) for (var i = 0; i < rowStride; i++)
line[i] += prevLine[i]; line[i] += prevLine[i];
break; break;
case PngFilter.Average: case PngFilter.Average:
for (var i = 0; i < pxStride; i++) for (var i = 0; i < pxStride; i++)
line[i] += Average(0, prevLine[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]); line[i] += Average(line[i - pxStride], prevLine[i]);
break; break;
case PngFilter.Paeth: case PngFilter.Paeth:
for (var i = 0; i < pxStride; i++) for (var i = 0; i < pxStride; i++)
line[i] += Paeth(0, prevLine[i], 0); 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]); line[i] += Paeth(line[i - pxStride], prevLine[i], prevLine[i - pxStride]);
break; break;
default: default:
@@ -363,15 +197,6 @@ namespace OpenRA.FileFormats
prevLine = line; 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 Average(byte a, byte b) => (byte)((a + b) / 2);
static byte Paeth(byte a, byte b, byte c) static byte Paeth(byte a, byte b, byte c)
@@ -385,6 +210,17 @@ namespace OpenRA.FileFormats
(pb <= pc) ? b : c; (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, public Png(byte[] data, SpriteFrameType type, int width, int height, Color[] palette = null,
Dictionary<string, string> embeddedData = null) Dictionary<string, string> embeddedData = null)
@@ -420,27 +256,14 @@ namespace OpenRA.FileFormats
// Convert to big endian // Convert to big endian
Data = new byte[data.Length]; Data = new byte[data.Length];
var stride = PixelStride; var stride = PixelStride;
var elements = width * height * stride; for (var i = 0; i < width * height; i++)
var src = data.AsSpan(); {
var dst = Data.AsSpan(); 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) if (type == SpriteFrameType.Bgra32)
{ Data[stride * i + 3] = data[stride * i + 3];
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];
}
} }
break; break;
@@ -457,8 +280,7 @@ namespace OpenRA.FileFormats
public static bool Verify(Stream s) public static bool Verify(Stream s)
{ {
var pos = s.Position; var pos = s.Position;
Span<byte> sigBuffer = stackalloc byte[8]; var isPng = Signature.Aggregate(true, (current, t) => current && s.ReadUInt8() == t);
var isPng = s.Read(sigBuffer) == 8 && sigBuffer.SequenceEqual(Signature);
s.Position = pos; s.Position = pos;
return isPng; return isPng;
} }
@@ -481,29 +303,24 @@ namespace OpenRA.FileFormats
throw new InvalidDataException("Unknown pixel format"); 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]; input.Position = 0;
BinaryPrimitives.WriteInt32BigEndian(header[..4], (int)input.Length);
BinaryPrimitives.WriteUInt32BigEndian(header[4..], type);
if (!input.TryGetBuffer(out var dataSegment)) var typeBytes = Encoding.ASCII.GetBytes(type);
dataSegment = new ArraySegment<byte>(input.ToArray()); output.Write(IPAddress.HostToNetworkOrder((int)input.Length));
output.Write(typeBytes);
ReadOnlySpan<byte> data = dataSegment.AsSpan(0, (int)input.Length); var data = input.ReadAllBytes();
output.Write(header);
output.Write(data); output.Write(data);
var crc = 0xFFFFFFFF; var crc32 = new Crc32();
crc = CRC32.Update(crc, header[4..]); crc32.Update(typeBytes);
crc = CRC32.Update(crc, data); crc32.Update(data);
var finalCrc = CRC32.Finish(crc); output.Write(IPAddress.NetworkToHostOrder((int)crc32.Value));
output.Write(IPAddress.NetworkToHostOrder((int)finalCrc));
} }
public byte[] Save(CompressionLevel compression = CompressionLevel.SmallestSize) public byte[] Save()
{ {
using (var output = new MemoryStream()) using (var output = new MemoryStream())
{ {
@@ -522,7 +339,7 @@ namespace OpenRA.FileFormats
header.WriteByte(0); // Filter header.WriteByte(0); // Filter
header.WriteByte(0); // Interlacing header.WriteByte(0); // Interlacing
WritePngChunk(output, ChunkIHDR, header); WritePngChunk(output, "IHDR", header);
} }
var alphaPalette = false; var alphaPalette = false;
@@ -538,7 +355,7 @@ namespace OpenRA.FileFormats
alphaPalette |= c.A > 0; alphaPalette |= c.A > 0;
} }
WritePngChunk(output, ChunkPLTE, palette); WritePngChunk(output, "PLTE", palette);
} }
} }
@@ -549,13 +366,13 @@ namespace OpenRA.FileFormats
foreach (var c in Palette) foreach (var c in Palette)
alpha.WriteByte(c.A); alpha.WriteByte(c.A);
WritePngChunk(output, ChunkTRNS, alpha); WritePngChunk(output, "tRNS", alpha);
} }
} }
using (var data = new MemoryStream()) 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; var rowStride = Width * PixelStride;
for (var y = 0; y < Height; y++) for (var y = 0; y < Height; y++)
@@ -565,9 +382,12 @@ namespace OpenRA.FileFormats
compressed.WriteByte(FilterType); compressed.WriteByte(FilterType);
compressed.Write(Data, y * rowStride, rowStride); compressed.Write(Data, y * rowStride, rowStride);
} }
}
WritePngChunk(output, ChunkIDAT, data); compressed.Flush();
compressed.Finish();
WritePngChunk(output, "IDAT", data);
}
} }
foreach (var kv in EmbeddedData) foreach (var kv in EmbeddedData)
@@ -575,18 +395,18 @@ namespace OpenRA.FileFormats
using (var text = new MemoryStream()) using (var text = new MemoryStream())
{ {
text.Write(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value)); text.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(); 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) public ReplayMetadata(GameInformation info)
{ {
ArgumentNullException.ThrowIfNull(info); if (info == null)
throw new ArgumentNullException(nameof(info));
GameInfo = info; GameInfo = info;
} }

View File

@@ -29,16 +29,16 @@ namespace OpenRA.FileSystem
public class FileSystem : IReadOnlyFileSystem public class FileSystem : IReadOnlyFileSystem
{ {
public IEnumerable<IReadOnlyPackage> MountedPackages => mountedPackages.Keys; public IEnumerable<IReadOnlyPackage> MountedPackages => mountedPackages.Keys;
readonly Dictionary<IReadOnlyPackage, int> mountedPackages = []; readonly Dictionary<IReadOnlyPackage, int> mountedPackages = new();
readonly Dictionary<string, IReadOnlyPackage> explicitMounts = []; readonly Dictionary<string, IReadOnlyPackage> explicitMounts = new();
readonly string modID; readonly string modID;
// Mod packages that should not be disposed // Mod packages that should not be disposed
readonly List<IReadOnlyPackage> modPackages = []; readonly List<IReadOnlyPackage> modPackages = new();
readonly IReadOnlyDictionary<string, Manifest> installedMods; readonly IReadOnlyDictionary<string, Manifest> installedMods;
readonly IPackageLoader[] packageLoaders; 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) public FileSystem(string modID, IReadOnlyDictionary<string, Manifest> installedMods, IPackageLoader[] packageLoaders)
{ {
@@ -178,7 +178,7 @@ namespace OpenRA.FileSystem
explicitMounts.Clear(); explicitMounts.Clear();
modPackages.Clear(); modPackages.Clear();
fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => []); fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
} }
public void TrimExcess() public void TrimExcess()

View File

@@ -35,7 +35,7 @@ namespace OpenRA.FileSystem
return Directory.GetFiles(Name, "*", SearchOption.TopDirectoryOnly) return Directory.GetFiles(Name, "*", SearchOption.TopDirectoryOnly)
.Concat(Directory.GetDirectories(Name)) .Concat(Directory.GetDirectories(Name))
.Select(Path.GetFileName) .Select(Path.GetFileName)
.Order(); .OrderBy(f => f);
} }
} }

View File

@@ -98,11 +98,11 @@ namespace OpenRA.FileSystem
{ {
readonly MemoryStream pkgStream = new(); 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 // 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 // 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))) using (var copy = new MemoryStream(File.ReadAllBytes(filename)))
{ {
@@ -120,26 +120,8 @@ namespace OpenRA.FileSystem
entry.ExtraData = null; 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() void Commit()
{ {
if (!string.IsNullOrEmpty(Name))
File.WriteAllBytes(Name, pkgStream.ToArray()); File.WriteAllBytes(Name, pkgStream.ToArray());
} }

View File

@@ -11,7 +11,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using Linguini.Bundle; 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 public class FluentBundle
{ {
readonly Linguini.Bundle.FluentBundle bundle; 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)) { } : 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)) { } : 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) { } : this(culture, paths, fileSystem, null, onError) { }
public FluentBundle(string culture, string text, Action<ParseError> 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() bundle = LinguiniBuilder.Builder()
.CultureInfo(new CultureInfo(culture)) .CultureInfo(new CultureInfo(culture))
@@ -147,7 +111,8 @@ namespace OpenRA
public bool TryGetMessage(string key, out string value, object[] args = null) public bool TryGetMessage(string key, out string value, object[] args = null)
{ {
ArgumentNullException.ThrowIfNull(key); if (key == null)
throw new ArgumentNullException(nameof(key));
try try
{ {
@@ -164,7 +129,7 @@ namespace OpenRA
throw new ArgumentException("Expected a comma separated list of name, value arguments " + 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)); "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) for (var i = 0; i < args.Length; i += 2)
{ {
var argKey = args[i] as string; var argKey = args[i] as string;

View File

@@ -10,7 +10,6 @@
#endregion #endregion
using System; using System;
using System.Collections.Immutable;
using System.Text; using System.Text;
using OpenRA.FileSystem; using OpenRA.FileSystem;
@@ -23,16 +22,16 @@ namespace OpenRA
static FluentBundle modFluentBundle; static FluentBundle modFluentBundle;
static FluentBundle mapFluentBundle; static FluentBundle mapFluentBundle;
public static void Initialize(Manifest manifest, IReadOnlyFileSystem fileSystem) public static void Initialize(ModData modData, IReadOnlyFileSystem fileSystem)
{ {
lock (SyncObject) 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) if (fileSystem is Map map && map.FluentMessageDefinitions != null)
{ {
var files = ImmutableArray<string>.Empty; var files = Array.Empty<string>();
if (map.FluentMessageDefinitions.Value != null) 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; string text = null;
if (map.FluentMessageDefinitions.Nodes.Length > 0) if (map.FluentMessageDefinitions.Nodes.Length > 0)
@@ -45,7 +44,7 @@ namespace OpenRA
text = builder.ToString(); 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 #endregion
using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
namespace OpenRA namespace OpenRA
@@ -24,15 +23,15 @@ namespace OpenRA
public class Fonts : IGlobalModData public class Fonts : IGlobalModData
{ {
[FieldLoader.LoadUsing(nameof(LoadFonts))] [FieldLoader.LoadUsing(nameof(LoadFonts))]
public readonly FrozenDictionary<string, FontData> FontList; public readonly Dictionary<string, FontData> FontList;
static object LoadFonts(MiniYaml y) 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) foreach (var node in y.Nodes)
ret.Add(node.Key, FieldLoader.Load<FontData>(node.Value)); ret.Add(node.Key, FieldLoader.Load<FontData>(node.Value));
return ret.ToFrozenDictionary(); return ret;
} }
} }
} }

View File

@@ -11,7 +11,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@@ -28,7 +27,6 @@ using OpenRA.Widgets;
namespace OpenRA namespace OpenRA
{ {
[IncludeStaticFluentReferences(typeof(Server.Server), typeof(Player), typeof(UnitOrders), typeof(OrderManager))]
public static class Game public static class Game
{ {
[FluentReference("filename")] [FluentReference("filename")]
@@ -168,7 +166,6 @@ namespace OpenRA
{ {
return ModData.WidgetLoader.LoadWidget(new WidgetArgs(args) return ModData.WidgetLoader.LoadWidget(new WidgetArgs(args)
{ {
{ "modData", ModData },
{ "world", world }, { "world", world },
{ "orderManager", OrderManager }, { "orderManager", OrderManager },
{ "worldRenderer", worldRenderer }, { "worldRenderer", worldRenderer },
@@ -184,16 +181,7 @@ namespace OpenRA
public static event Action BeforeGameStart = () => { }; public static event Action BeforeGameStart = () => { };
public static event Action AfterGameStart = () => { }; public static event Action AfterGameStart = () => { };
internal static void StartGame(string uid, WorldType type) internal static void StartGame(string mapUID, 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)
{ {
// Dispose of the old world before creating a new one. // Dispose of the old world before creating a new one.
worldRenderer?.Dispose(); worldRenderer?.Dispose();
@@ -202,23 +190,7 @@ namespace OpenRA
BeforeGameStart(); BeforeGameStart();
using (new PerfTimer("NewWorld")) using (new PerfTimer("NewWorld"))
{ OrderManager.World = new World(mapUID, ModData, OrderManager, type);
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.GameOver += FinishBenchmark; OrderManager.World.GameOver += FinishBenchmark;
@@ -372,7 +344,7 @@ namespace OpenRA
var explicitModPaths = Array.Empty<string>(); var explicitModPaths = Array.Empty<string>();
if (modID != null && (File.Exists(modID) || Directory.Exists(modID))) if (modID != null && (File.Exists(modID) || Directory.Exists(modID)))
{ {
explicitModPaths = [modID]; explicitModPaths = new[] { modID };
modID = Path.GetFileNameWithoutExtension(modID); modID = Path.GetFileNameWithoutExtension(modID);
} }
@@ -387,48 +359,6 @@ namespace OpenRA
Log.AddChannel("nat", "nat.log"); Log.AddChannel("nat", "nat.log");
Log.AddChannel("client", "client.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 }; var platforms = new[] { Settings.Game.Platform, "Default", null };
foreach (var p in platforms) foreach (var p in platforms)
{ {
@@ -439,7 +369,7 @@ namespace OpenRA
try try
{ {
var platform = CreatePlatform(p); 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); Sound = new Sound(platform, Settings.Sound);
break; 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) public static IPlatform CreatePlatform(string platformName)
{ {
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + platformName + ".dll"); var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + platformName + ".dll");
#if NET5_0_OR_GREATER
var loader = new AssemblyLoader(rendererPath); var loader = new AssemblyLoader(rendererPath);
var platformType = loader.LoadDefaultAssembly().GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t)); 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) if (platformType == null)
throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation."); throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation.");
return (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null); 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 // Clear static state if we have switched mods
LobbyInfoChanged = () => { }; LobbyInfoChanged = () => { };
@@ -495,25 +469,34 @@ namespace OpenRA
ModData = null; 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(); 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; return;
ModData.InitializeLoaders(ModData.DefaultFileSystem); ModData.InitializeLoaders(ModData.DefaultFileSystem);
Renderer.InitializeFonts(ModData); Renderer.InitializeFonts(ModData);
using (new PerfTimer("LoadMaps")) 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?.Dispose();
Cursor = new CursorManager(ModData); Cursor = new CursorManager(ModData.CursorProvider, ModData.Manifest.CursorSheetSize);
var metadata = ModData.Manifest.Metadata; var metadata = ModData.Manifest.Metadata;
if (!string.IsNullOrEmpty(metadata.WindowTitleTranslated)) if (!string.IsNullOrEmpty(metadata.WindowTitleTranslated))
@@ -531,16 +514,10 @@ namespace OpenRA
ModData.LoadScreen.StartGame(args); ModData.LoadScreen.StartGame(args);
} }
public static void LoadEditor(string uid) public static void LoadEditor(string mapUid)
{ {
JoinLocal(); JoinLocal();
StartGame(uid, WorldType.Editor); StartGame(mapUid, WorldType.Editor);
}
public static void LoadEditor(Map map)
{
JoinLocal();
StartGame(map, WorldType.Editor);
} }
public static void LoadShellMap() public static void LoadShellMap()
@@ -614,7 +591,7 @@ namespace OpenRA
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
var filename = TimestampedFilename(true); 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); Log.Write("debug", "Taking screenshot " + path);
Renderer.SaveScreenshot(path); Renderer.SaveScreenshot(path);
@@ -661,7 +638,7 @@ namespace OpenRA
world.Tick(); world.Tick();
PerfHistory.Tick(!world.Paused); PerfHistory.Tick();
} }
// Wait until we have done our first world Tick before TickRendering // 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 // Use worldRenderer.World instead of OrderManager.World to avoid a rendering mismatch while processing orders
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave) if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
{ {
Renderer.BeginWorld(worldRenderer.Viewport.CenterLocation, worldRenderer.Viewport.ViewportSize); Renderer.BeginWorld(worldRenderer.Viewport.Rectangle);
Sound.SetListenerPosition(worldRenderer.Viewport.CenterPosition); Sound.SetListenerPosition(worldRenderer.Viewport.CenterPosition);
using (new PerfSample("render_world")) using (new PerfSample("render_world"))
worldRenderer.Draw(); worldRenderer.Draw();
@@ -740,12 +717,15 @@ namespace OpenRA
Ui.Draw(); Ui.Draw();
if (ModData != null && ModData.CursorProvider != null)
{
if (HideCursor) if (HideCursor)
Cursor?.SetCursor(null); Cursor.SetCursor(null);
else else
{ {
Cursor?.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default"); Cursor.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default");
Cursor?.Render(Renderer); Cursor.Render(Renderer);
}
} }
} }
@@ -759,13 +739,12 @@ namespace OpenRA
} }
} }
var isActive = !(worldRenderer?.World.Paused ?? true); PerfHistory.Items["render"].Tick();
PerfHistory.Items["render"].Tick(isActive); PerfHistory.Items["batches"].Tick();
PerfHistory.Items["batches"].Tick(isActive); PerfHistory.Items["render_world"].Tick();
PerfHistory.Items["render_world"].Tick(isActive); PerfHistory.Items["render_widgets"].Tick();
PerfHistory.Items["render_widgets"].Tick(isActive); PerfHistory.Items["render_flip"].Tick();
PerfHistory.Items["render_flip"].Tick(isActive); PerfHistory.Items["terrain_lighting"].Tick();
PerfHistory.Items["terrain_lighting"].Tick(isActive);
} }
static void Loop() static void Loop()
@@ -862,13 +841,8 @@ namespace OpenRA
var haveSomeTimeUntilNextLogic = now < nextLogic; var haveSomeTimeUntilNextLogic = now < nextLogic;
var isTimeToRender = now >= nextRender; var isTimeToRender = now >= nextRender;
if (!Renderer.WindowIsSuspended) if (!Renderer.WindowIsSuspended && ((isTimeToRender && haveSomeTimeUntilNextLogic) || forceRender))
{ {
if (isTimeToRender || forceRender)
{
if (haveSomeTimeUntilNextLogic || forceRender)
RenderTick();
nextRender = now + renderInterval; nextRender = now + renderInterval;
// Pick the minimum allowed FPS (the lower between 'minReplayFPS' // Pick the minimum allowed FPS (the lower between 'minReplayFPS'
@@ -879,13 +853,12 @@ namespace OpenRA
var maxRenderInterval = Math.Max(1000 / MinReplayFps, renderInterval); var maxRenderInterval = Math.Max(1000 / MinReplayFps, renderInterval);
forcedNextRender = now + maxRenderInterval; forcedNextRender = now + maxRenderInterval;
RenderTick();
renderBeforeNextTick = false; renderBeforeNextTick = false;
} }
}
else
{
// Simulate a render tick if it was time to render but we skip actually rendering // 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 // Make sure that nextUpdate is set to a proper minimum interval
nextRender = now + renderInterval; nextRender = now + renderInterval;
@@ -896,12 +869,6 @@ namespace OpenRA
// Ensure that we still logic tick despite not rendering // Ensure that we still logic tick despite not rendering
renderBeforeNextTick = false; renderBeforeNextTick = false;
} }
else
{
// Avoid busy wait.
Thread.Sleep((int)(nextRender - now));
}
}
} }
else else
Thread.Sleep((int)(nextUpdate - now)); Thread.Sleep((int)(nextUpdate - now));
@@ -980,8 +947,7 @@ namespace OpenRA
{ {
Name = "Skirmish Game", Name = "Skirmish Game",
Map = map, Map = map,
AdvertiseOnline = false, AdvertiseOnline = false
AdvertiseOnLocalNetwork = !isSkirmish
}; };
// Always connect to local games using the same loopback connection // Always connect to local games using the same loopback connection
@@ -1019,7 +985,7 @@ namespace OpenRA
Order.Command($"state {Session.ClientState.Ready}") Order.Command($"state {Session.ClientState.Ready}")
}; };
var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.Path) == launchMap); var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.PackageName) == launchMap);
if (map == null) if (map == null)
throw new ArgumentException($"Could not find map '{launchMap}'."); throw new ArgumentException($"Could not find map '{launchMap}'.");

View File

@@ -10,7 +10,6 @@
#endregion #endregion
using System; using System;
using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using OpenRA.Network; using OpenRA.Network;
@@ -40,32 +39,17 @@ namespace OpenRA
public TimeSpan Duration => EndTimeUtc > StartTimeUtc ? EndTimeUtc - StartTimeUtc : TimeSpan.Zero; public TimeSpan Duration => EndTimeUtc > StartTimeUtc ? EndTimeUtc - StartTimeUtc : TimeSpan.Zero;
public IList<Player> Players { get; } 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 IEnumerable<Player> HumanPlayers { get { return Players.Where(p => p.IsHuman); } }
public bool IsSinglePlayer => HumanPlayers.Count() == 1; public bool IsSinglePlayer => HumanPlayers.Count() == 1;
[FieldLoader.Ignore]
public MapGenerationArgs MapGenerationArgs;
readonly Dictionary<OpenRA.Player, Player> playersByRuntime; readonly Dictionary<OpenRA.Player, Player> playersByRuntime;
public GameInformation() public GameInformation()
{ {
Players = []; Players = new List<Player>();
playersByRuntime = []; playersByRuntime = new Dictionary<OpenRA.Player, Player>();
}
public MapPreview GetMapPreview(ModData modData)
{
var preview = modData.MapCache[MapUid];
if (preview.Status != MapStatus.Available && MapGenerationArgs != null)
{
preview.UpdateFromGenerationArgs(MapGenerationArgs);
preview.Generate();
}
return preview;
} }
public static GameInformation Deserialize(string data, string path) public static GameInformation Deserialize(string data, string path)
@@ -88,10 +72,6 @@ namespace OpenRA
case "Player": case "Player":
info.Players.Add(FieldLoader.Load<Player>(node.Value)); info.Players.Add(FieldLoader.Load<Player>(node.Value));
break; 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++) for (var i = 0; i < Players.Count; i++)
nodes.Add(new MiniYamlNode($"Player@{i}", FieldSaver.Save(Players[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(); return nodes.WriteToString();
} }
/// <summary>Adds the player information at start-up.</summary> /// <summary>Adds the player information at start-up.</summary>
public void AddPlayer(OpenRA.Player runtimePlayer, Session lobbyInfo) 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 // We don't care about spectators and map players
if (runtimePlayer.NonCombatant || !runtimePlayer.Playable) 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. /// You can remove inherited traits by adding a - in front of them as in -TraitName: to inherit everything, but this trait.
/// </summary> /// </summary>
public readonly string Name; public readonly string Name;
readonly TypeDictionary traits = []; readonly TypeDictionary traits = new();
TraitInfo[] constructOrderCache = null; TraitInfo[] constructOrderCache = null;
public ActorInfo(ObjectCreator creator, string name, MiniYaml node) public ActorInfo(ObjectCreator creator, string name, MiniYaml node)

View File

@@ -11,7 +11,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using OpenRA.FileSystem; using OpenRA.FileSystem;
@@ -269,7 +268,7 @@ namespace OpenRA
if (mapRules.Value != null) 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) foreach (var f in mapFiles)
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f))) if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
return true; return true;

View File

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

View File

@@ -11,7 +11,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using OpenRA.Effects; using OpenRA.Effects;
using OpenRA.Primitives; using OpenRA.Primitives;
@@ -37,7 +36,7 @@ namespace OpenRA.GameRules
public class WarheadArgs public class WarheadArgs
{ {
public WeaponInfo Weapon; public WeaponInfo Weapon;
public int[] DamageModifiers = []; public int[] DamageModifiers = Array.Empty<int>();
public WPos? Source; public WPos? Source;
public WRot ImpactOrientation; public WRot ImpactOrientation;
public WPos ImpactPosition; public WPos ImpactPosition;
@@ -83,13 +82,13 @@ namespace OpenRA.GameRules
public readonly WVec FollowingBurstTargetOffset = WVec.Zero; public readonly WVec FollowingBurstTargetOffset = WVec.Zero;
[Desc("The sound played each time the weapon is fired.")] [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.")] [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.")] [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.")] [Desc("Delay in ticks to play reloading sound.")]
public readonly int AfterFireSoundDelay = 0; 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.", [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.")] "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.")] [Desc("The minimum range the weapon can fire.")]
public readonly WDist MinRange = WDist.Zero; public readonly WDist MinRange = WDist.Zero;
@@ -129,7 +128,7 @@ namespace OpenRA.GameRules
public readonly IProjectileInfo Projectile; public readonly IProjectileInfo Projectile;
[FieldLoader.LoadUsing(nameof(LoadWarheads))] [FieldLoader.LoadUsing(nameof(LoadWarheads))]
public readonly ImmutableArray<IWarhead> Warheads = []; public readonly List<IWarhead> Warheads = new();
/// <summary> /// <summary>
/// This constructor is used solely for documentation generation. /// This constructor is used solely for documentation generation.
@@ -140,7 +139,7 @@ namespace OpenRA.GameRules
{ {
// Resolve any weapon-level yaml inheritance or removals // Resolve any weapon-level yaml inheritance or removals
// HACK: The "Defaults" sequence syntax prevents us from doing this generally during yaml parsing // HACK: The "Defaults" sequence syntax prevents us from doing this generally during yaml parsing
content = content.WithNodes(MiniYaml.Merge([content.Nodes])); content = content.WithNodes(MiniYaml.Merge(new IReadOnlyCollection<MiniYamlNode>[] { content.Nodes }));
FieldLoader.Load(this, content); FieldLoader.Load(this, content);
} }
@@ -171,7 +170,7 @@ namespace OpenRA.GameRules
retList.Add(ret); retList.Add(ret);
} }
return retList.ToImmutableArray(); return retList;
} }
public bool IsValidTarget(BitSet<TargetableType> targetTypes) public bool IsValidTarget(BitSet<TargetableType> targetTypes)

View File

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

View File

@@ -10,7 +10,6 @@
#endregion #endregion
using System; using System;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Support; using OpenRA.Support;
@@ -74,10 +73,10 @@ namespace OpenRA.Graphics
shadow, pos, offset - new WVec(0, 0, height), CurrentSequence.ShadowZOffset + zOffset + height, palette, shadow, pos, offset - new WVec(0, 0, height), CurrentSequence.ShadowZOffset + zOffset + height, palette,
CurrentSequence.Scale, 1f, float3.Ones, tintModifiers, CurrentSequence.Scale, 1f, float3.Ones, tintModifiers,
true, rotation); 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) 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 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); 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) public Rectangle ScreenBounds(WorldRenderer wr, WPos pos, in WVec offset)
@@ -251,9 +250,9 @@ namespace OpenRA.Graphics
return sequences.GetSequence(Name, sequenceName); 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 #endregion
using System; using System;
using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using OpenRA.FileSystem; using OpenRA.FileSystem;
using OpenRA.Primitives; using OpenRA.Primitives;
@@ -49,9 +47,9 @@ namespace OpenRA.Graphics
public readonly string Image2x = null; public readonly string Image2x = null;
public readonly string Image3x = 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 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; public static IReadOnlyDictionary<string, Collection> Collections => collections;
@@ -73,11 +71,11 @@ namespace OpenRA.Graphics
dpiScale = Game.Renderer.WindowScale; dpiScale = Game.Renderer.WindowScale;
fileSystem = modData.DefaultFileSystem; fileSystem = modData.DefaultFileSystem;
collections = []; collections = new Dictionary<string, Collection>();
cachedSheets = []; cachedSheets = new Dictionary<string, (Sheet, int)>();
cachedSprites = []; cachedSprites = new Dictionary<string, Dictionary<string, Sprite>>();
cachedPanelSprites = []; cachedPanelSprites = new Dictionary<string, Sprite[]>();
cachedCollectionSheets = []; cachedCollectionSheets = new Dictionary<Collection, (Sheet, int)>();
var stringPool = new HashSet<string>(); // Reuse common strings in YAML var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var chrome = MiniYaml.Merge(modData.Manifest.Chrome var chrome = MiniYaml.Merge(modData.Manifest.Chrome
@@ -172,7 +170,7 @@ namespace OpenRA.Graphics
var sheetDensity = SheetForCollection(collection); var sheetDensity = SheetForCollection(collection);
if (cachedCollection == null) if (cachedCollection == null)
{ {
cachedCollection = []; cachedCollection = new Dictionary<string, Sprite>();
cachedSprites.Add(collectionName, cachedCollection); 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: We don't need to search for images if there are no definitions.
// PERF: It's more efficient to send an empty array rather than an array of 9 nulls. // PERF: It's more efficient to send an empty array rather than an array of 9 nulls.
if (collection.Regions.Count == 0) if (collection.Regions.Count == 0)
return []; return Array.Empty<Sprite>();
// Support manual definitions for unusual dialog layouts // Support manual definitions for unusual dialog layouts
sprites = sprites = new[]
[ {
TryGetImage(collectionName, "corner-tl"), TryGetImage(collectionName, "corner-tl"),
TryGetImage(collectionName, "border-t"), TryGetImage(collectionName, "border-t"),
TryGetImage(collectionName, "corner-tr"), TryGetImage(collectionName, "corner-tr"),
@@ -256,7 +254,7 @@ namespace OpenRA.Graphics
TryGetImage(collectionName, "corner-bl"), TryGetImage(collectionName, "corner-bl"),
TryGetImage(collectionName, "border-b"), TryGetImage(collectionName, "border-b"),
TryGetImage(collectionName, "corner-br") TryGetImage(collectionName, "corner-br")
]; };
} }
cachedPanelSprites.Add(collectionName, sprites); cachedPanelSprites.Add(collectionName, sprites);

View File

@@ -13,7 +13,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Graphics namespace OpenRA.Graphics
{ {
@@ -30,8 +29,8 @@ namespace OpenRA.Graphics
public IHardwareCursor[] Cursors; public IHardwareCursor[] Cursors;
} }
readonly Dictionary<string, Cursor> cursors = []; readonly Dictionary<string, Cursor> cursors = new();
public readonly SheetBuilder SheetBuilder; readonly SheetBuilder sheetBuilder;
readonly GraphicSettings graphicSettings; readonly GraphicSettings graphicSettings;
Cursor cursor; Cursor cursor;
@@ -40,35 +39,19 @@ namespace OpenRA.Graphics
readonly bool hardwareCursorsDisabled = false; readonly bool hardwareCursorsDisabled = false;
bool hardwareCursorsDoubled = false; bool hardwareCursorsDoubled = false;
public CursorManager(ModData modData) public CursorManager(CursorProvider cursorProvider, int cursorSheetSize)
{ {
hardwareCursorsDisabled = Game.Settings.Graphics.DisableHardwareCursors;
graphicSettings = Game.Settings.Graphics; graphicSettings = Game.Settings.Graphics;
hardwareCursorsDisabled = graphicSettings.DisableHardwareCursors; sheetBuilder = new SheetBuilder(SheetType.BGRA, cursorSheetSize);
SheetBuilder = new SheetBuilder(SheetType.BGRA, modData.Manifest.RendererConstants.CursorSheetSize);
// Overwrite previous definitions if there are duplicates
var pals = new Dictionary<string, IProvidesCursorPaletteInfo>();
foreach (var p in modData.DefaultRules.Actors[SystemActors.World].TraitInfos<IProvidesCursorPaletteInfo>())
if (p.Palette != null)
pals[p.Palette] = p;
var paletteCache = new Cache<string, ImmutablePalette>(p => pals[p].ReadPalette(modData.DefaultFileSystem));
var frameCache = new FrameCache(modData.DefaultFileSystem, modData.SpriteLoaders);
// Sort the cursors for better packing onto the sheet. // 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 frames = kv.Value.Frames;
var length = kv.Value.Length ?? cursorSprites.Length - kv.Value.Start; var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? cursorProvider.Palettes[kv.Value.Palette] : null;
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 c = new Cursor var c = new Cursor
{ {
@@ -99,7 +82,7 @@ namespace OpenRA.Graphics
type = SpriteFrameType.Bgra32; 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 // Bounds relative to the hotspot
c.Bounds = Rectangle.Union(c.Bounds, new Rectangle(hotspot, f.Size)); c.Bounds = Rectangle.Union(c.Bounds, new Rectangle(hotspot, f.Size));
@@ -111,13 +94,9 @@ namespace OpenRA.Graphics
cursors.Add(kv.Key, c); cursors.Add(kv.Key, c);
} }
// Allow the utility to create a cursor manager.
if (Game.Renderer != null)
{
CreateOrUpdateHardwareCursors(); CreateOrUpdateHardwareCursors();
Update(); Update();
} }
}
void CreateOrUpdateHardwareCursors() void CreateOrUpdateHardwareCursors()
{ {
@@ -149,7 +128,7 @@ namespace OpenRA.Graphics
} }
} }
SheetBuilder.Current?.ReleaseBuffer(); sheetBuilder.Current.ReleaseBuffer();
hardwareCursorsDoubled = graphicSettings.CursorDouble; hardwareCursorsDoubled = graphicSettings.CursorDouble;
} }
@@ -254,7 +233,7 @@ namespace OpenRA.Graphics
var height = frame.Size.Height; var height = frame.Size.Height;
if (width == 0 || height == 0) if (width == 0 || height == 0)
return []; return Array.Empty<byte>();
var data = new byte[4 * width * height]; var data = new byte[4 * width * height];
unsafe unsafe
@@ -300,18 +279,21 @@ namespace OpenRA.Graphics
{ {
for (var i = 0; i < c.Cursors.Length; i++) 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; c.Cursors[i] = null;
} }
} }
} }
}
public void Dispose() public void Dispose()
{ {
ClearHardwareCursors(); ClearHardwareCursors();
cursors.Clear(); 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 #endregion
using System.Linq;
namespace OpenRA.Graphics namespace OpenRA.Graphics
{ {
public class CursorSequence public class CursorSequence
{ {
public readonly string Name; public readonly string Name;
public readonly string Src;
public readonly int Start; public readonly int Start;
public readonly int? Length; public readonly int Length;
public readonly string Palette; public readonly string Palette;
public readonly int2 Hotspot; 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(); var d = info.ToDictionary();
Start = Exts.ParseInt32Invariant(d["Start"].Value);
Palette = palette; Palette = palette;
Name = name; 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); Exts.TryParseInt32Invariant(yaml.Value, out var x);
Hotspot = Hotspot.WithX(x); Hotspot = Hotspot.WithX(x);
@@ -39,14 +63,6 @@ namespace OpenRA.Graphics
Exts.TryParseInt32Invariant(yaml.Value, out var y); Exts.TryParseInt32Invariant(yaml.Value, out var y);
Hotspot = Hotspot.WithY(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 ITexture ColorShifts { get; }
public int Height { get; private set; } public int Height { get; private set; }
readonly Dictionary<string, ImmutablePalette> palettes = []; readonly Dictionary<string, ImmutablePalette> palettes = new();
readonly Dictionary<string, MutablePalette> mutablePalettes = []; readonly Dictionary<string, MutablePalette> mutablePalettes = new();
readonly Dictionary<string, int> indices = []; readonly Dictionary<string, int> indices = new();
byte[] buffer = []; byte[] buffer = Array.Empty<byte>();
float[] colorShiftBuffer = []; float[] colorShiftBuffer = Array.Empty<float>();
public HardwarePalette() public HardwarePalette()
{ {

View File

@@ -31,13 +31,25 @@ namespace OpenRA.Graphics
public interface IModelWidget public interface IModelWidget
{ {
string Palette { get; } public string Palette { get; }
float Scale { get; } public float Scale { get; }
void Setup(Func<bool> isVisible, Func<string> getPalette, Func<string> getPlayerPalette, public void Setup(Func<bool> isVisible, Func<string> getPalette, Func<string> getPlayerPalette,
Func<float> getScale, Func<IModel> getVoxel, Func<WRot> getRotation); 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 { } public interface IModelCacheInfo : ITraitInfoInterface { }

View File

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

View File

@@ -11,7 +11,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO; using System.IO;
using OpenRA.Primitives; using OpenRA.Primitives;
@@ -65,18 +64,18 @@ namespace OpenRA.Graphics
Buffer.BlockCopy(colors, 0, destination, destinationOffset * 4, Palette.Size * 4); 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)) using (var s = File.OpenRead(filename))
LoadFromStream(s, remapTransparent, remap); 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); 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)) using (var reader = new BinaryReader(s))
for (var i = 0; i < Palette.Size; i++) for (var i = 0; i < Palette.Size; i++)

View File

@@ -66,7 +66,6 @@ namespace OpenRA
void PumpInput(IInputHandler inputHandler); void PumpInput(IInputHandler inputHandler);
string GetClipboardText(); string GetClipboardText();
bool SetClipboardText(string text); bool SetClipboardText(string text);
bool TryOpenUrl(string url);
void GrabWindowMouseFocus(); void GrabWindowMouseFocus();
void ReleaseWindowMouseFocus(); void ReleaseWindowMouseFocus();
@@ -84,8 +83,7 @@ namespace OpenRA
public interface IGraphicsContext : IDisposable public interface IGraphicsContext : IDisposable
{ {
IVertexBuffer<T> CreateEmptyVertexBuffer<T>(int size) where T : struct; IVertexBuffer<T> CreateVertexBuffer<T>(int size) where T : struct;
IVertexBuffer<T> CreateVertexBuffer<T>(T[] data, bool dynamic = true) where T : struct;
T[] CreateVertices<T>(int size) where T : struct; T[] CreateVertices<T>(int size) where T : struct;
IIndexBuffer CreateIndexBuffer(uint[] indices); IIndexBuffer CreateIndexBuffer(uint[] indices);
ITexture CreateTexture(); ITexture CreateTexture();
@@ -136,7 +134,7 @@ namespace OpenRA
void SetVec(string name, float x); void SetVec(string name, float x);
void SetVec(string name, float x, float y); void SetVec(string name, float x, float y);
void SetVec(string name, float x, float y, float z); 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 SetTexture(string param, ITexture texture);
void SetMatrix(string param, float[] mtx); void SetMatrix(string param, float[] mtx);
void PrepareRender(); void PrepareRender();
@@ -181,6 +179,12 @@ namespace OpenRA
TriangleList, TriangleList,
} }
public readonly struct Range<T>
{
public readonly T Start, End;
public Range(T start, T end) { Start = start; End = end; }
}
public enum WindowMode public enum WindowMode
{ {
Windowed, Windowed,

View File

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

View File

@@ -14,20 +14,39 @@ using System.Runtime.InteropServices;
namespace OpenRA.Graphics namespace OpenRA.Graphics
{ {
[StructLayout(LayoutKind.Sequential)] [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)] [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 sealed class RenderPostProcessPassShaderBindings : ShaderBindings
{ {
public RenderPostProcessPassShaderBindings(string name) public RenderPostProcessPassShaderBindings(string name)
: base("postprocess", "postprocess_" + name) { } : base("postprocess", "postprocess_" + name) { }
public override ShaderVertexAttribute[] Attributes { get; } = public override ShaderVertexAttribute[] Attributes { get; } = new[]
[ {
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0) new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0)
]; };
} }
public sealed class RenderPostProcessPassTexturedShaderBindings : ShaderBindings public sealed class RenderPostProcessPassTexturedShaderBindings : ShaderBindings
@@ -36,10 +55,10 @@ namespace OpenRA.Graphics
: base("postprocess_textured", "postprocess_textured_" + name) : 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("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0),
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 2, 8), 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 tr = new float3(br.X, tl.Y, tl.Z);
var bl = new float3(tl.X, br.Y, br.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) 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 public interface ISpriteSequenceLoader
{ {
int BgraSheetSize { get; }
int IndexedSheetSize { get; }
IReadOnlyDictionary<string, ISpriteSequence> ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode node); IReadOnlyDictionary<string, ISpriteSequence> ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode node);
} }
public sealed class SequenceSet : IDisposable public sealed class SequenceSet : IDisposable
{ {
public readonly string TileSet;
readonly ModData modData; readonly ModData modData;
readonly string tileSet;
readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, ISpriteSequence>> images; readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, ISpriteSequence>> images;
public SpriteCache SpriteCache { get; } public SpriteCache SpriteCache { get; }
public SequenceSet(IReadOnlyFileSystem fileSystem, ModData modData, string tileSet, MiniYaml additionalSequences) public SequenceSet(IReadOnlyFileSystem fileSystem, ModData modData, string tileSet, MiniYaml additionalSequences)
{ {
this.modData = modData; this.modData = modData;
TileSet = tileSet; this.tileSet = tileSet;
SpriteCache = new SpriteCache(fileSystem, modData.SpriteLoaders, modData.SpriteSequenceLoader.BgraSheetSize, modData.SpriteSequenceLoader.IndexedSheetSize);
var rc = modData.Manifest.RendererConstants;
SpriteCache = new SpriteCache(fileSystem, modData.SpriteLoaders, rc.SequenceBgraSheetSize, rc.SequenceIndexedSheetSize);
using (new Support.PerfTimer("LoadSequences")) using (new Support.PerfTimer("LoadSequences"))
images = Load(fileSystem, additionalSequences); images = Load(fileSystem, additionalSequences);
} }
@@ -97,7 +97,7 @@ namespace OpenRA.Graphics
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix)) if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix))
continue; continue;
images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, TileSet, SpriteCache, node); images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node);
} }
return images; return images;

View File

@@ -23,7 +23,21 @@ namespace OpenRA.Graphics
UInt = 0x1405 // GL_UNSIGNED_INT 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 public abstract class ShaderBindings : IShaderBindings
{ {

View File

@@ -149,7 +149,7 @@ namespace OpenRA.Graphics
var buffer = data; var buffer = data;
ReleaseBuffer(); 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) if (Game.Renderer == null)
return false; return false;

View File

@@ -16,6 +16,7 @@ using OpenRA.Primitives;
namespace OpenRA.Graphics namespace OpenRA.Graphics
{ {
[Serializable]
public class SheetOverflowException : Exception public class SheetOverflowException : Exception
{ {
public SheetOverflowException(string message) public SheetOverflowException(string message)
@@ -33,7 +34,7 @@ namespace OpenRA.Graphics
public sealed class SheetBuilder : IDisposable public sealed class SheetBuilder : IDisposable
{ {
public readonly SheetType Type; public readonly SheetType Type;
readonly List<Sheet> sheets = []; readonly List<Sheet> sheets = new();
readonly Func<Sheet> allocateSheet; readonly Func<Sheet> allocateSheet;
readonly int margin; readonly int margin;
int rowHeight = 0; 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) public SheetBuilder(SheetType t, int sheetSize, int margin = 1)
: this(t, () => AllocateSheet(t, sheetSize), margin) { } : this(t, () => AllocateSheet(t, sheetSize), margin) { }
@@ -72,6 +76,8 @@ namespace OpenRA.Graphics
{ {
CurrentChannel = t == SheetType.Indexed ? TextureChannel.Red : TextureChannel.RGBA; CurrentChannel = t == SheetType.Indexed ? TextureChannel.Red : TextureChannel.RGBA;
Type = t; Type = t;
Current = allocateSheet();
sheets.Add(Current);
this.allocateSheet = allocateSheet; this.allocateSheet = allocateSheet;
this.margin = margin; 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, bool premultiplied = false) { return Add(src, type, size, 0, float3.Zero, premultiplied); }
public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset, bool premultiplied = false) public Sprite Add(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 // Don't bother allocating empty sprites
if (size.Width == 0 || size.Height == 0) if (size.Width == 0 || size.Height == 0)
return new Sprite(Current, Rectangle.Empty, 0, spriteOffset, CurrentChannel, BlendMode.Alpha); return new Sprite(Current, Rectangle.Empty, 0, spriteOffset, CurrentChannel, BlendMode.Alpha);
@@ -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 scale = 1f) { return Allocate(imageSize, 0, float3.Zero, scale); }
public Sprite Allocate(Size imageSize, float zRamp, in float3 spriteOffset, float scale = 1f) 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) if (imageSize.Width + p.X + margin > Current.Size.Width)
{ {
p = new int2(0, p.Y + rowHeight + margin); p = new int2(0, p.Y + rowHeight + margin);

View File

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

View File

@@ -9,6 +9,7 @@
*/ */
#endregion #endregion
using System;
using System.Collections.Generic; using System.Collections.Generic;
using OpenRA.Primitives; using OpenRA.Primitives;
@@ -16,7 +17,7 @@ namespace OpenRA.Graphics
{ {
public class SpriteRenderable : IPalettedRenderable, IModifyableRenderable, IFinalizedRenderable 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 Sprite sprite;
readonly WPos pos; readonly WPos pos;

View File

@@ -19,7 +19,7 @@ namespace OpenRA.Graphics
public sealed class TerrainSpriteLayer : IDisposable public sealed class TerrainSpriteLayer : IDisposable
{ {
// PERF: we can reuse the IndexBuffer as all layers have the same size. // 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; readonly IndexBufferRc indexBufferWrapper;
public readonly BlendMode BlendMode; public readonly BlendMode BlendMode;
@@ -30,7 +30,7 @@ namespace OpenRA.Graphics
readonly IVertexBuffer<Vertex> vertexBuffer; readonly IVertexBuffer<Vertex> vertexBuffer;
readonly Vertex[] vertices; readonly Vertex[] vertices;
readonly bool[] ignoreTint; readonly bool[] ignoreTint;
readonly HashSet<int> dirtyRows = []; readonly HashSet<int> dirtyRows = new();
readonly int indexRowStride; readonly int indexRowStride;
readonly int vertexRowStride; readonly int vertexRowStride;
readonly bool restrictToBounds; readonly bool restrictToBounds;
@@ -49,23 +49,23 @@ namespace OpenRA.Graphics
BlendMode = blendMode; BlendMode = blendMode;
map = world.Map; map = world.Map;
vertexRowStride = 4 * map.MapSize.Width; vertexRowStride = 4 * map.MapSize.X;
vertices = new Vertex[vertexRowStride * map.MapSize.Height]; vertices = new Vertex[vertexRowStride * map.MapSize.Y];
vertexBuffer = Game.Renderer.Context.CreateEmptyVertexBuffer<Vertex>(vertices.Length); vertexBuffer = Game.Renderer.Context.CreateVertexBuffer<Vertex>(vertices.Length);
indexRowStride = 6 * map.MapSize.Width; indexRowStride = 6 * map.MapSize.X;
lock (IndexBuffers) lock (IndexBuffers)
{ {
indexBufferWrapper = IndexBuffers.GetValue(world, world => new IndexBufferRc(world)); indexBufferWrapper = IndexBuffers.GetValue(world, world => new IndexBufferRc(world));
indexBufferWrapper.AddRef(); indexBufferWrapper.AddRef();
} }
palettes = new PaletteReference[map.MapSize.Width * map.MapSize.Height]; palettes = new PaletteReference[map.MapSize.X * map.MapSize.Y];
wr.PaletteInvalidated += UpdatePaletteIndices; wr.PaletteInvalidated += UpdatePaletteIndices;
if (wr.TerrainLighting != null) if (wr.TerrainLighting != null)
{ {
ignoreTint = new bool[vertexRowStride * map.MapSize.Height]; ignoreTint = new bool[vertexRowStride * map.MapSize.Y];
wr.TerrainLighting.CellChanged += UpdateTint; 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); 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); dirtyRows.Add(row);
} }
@@ -193,7 +193,7 @@ namespace OpenRA.Graphics
var offset = vertexRowStride * uv.V + 4 * uv.U; var offset = vertexRowStride * uv.V + 4 * uv.U;
Util.FastCreateQuad(vertices, pos, sprite, samplers, palette?.TextureIndex ?? 0, offset, scale * sprite.Size, alpha * float3.Ones, alpha); 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) if (worldRenderer.TerrainLighting != null)
{ {
@@ -209,8 +209,8 @@ namespace OpenRA.Graphics
var cells = restrictToBounds ? viewport.VisibleCellsInsideBounds : viewport.AllVisibleCells; var cells = restrictToBounds ? viewport.VisibleCellsInsideBounds : viewport.AllVisibleCells;
// Only draw the rows that are visible. // Only draw the rows that are visible.
var firstRow = cells.CandidateMapCoords.TopLeft.V.Clamp(0, 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.Height); var lastRow = (cells.CandidateMapCoords.BottomRight.V + 1).Clamp(firstRow, map.MapSize.Y);
Game.Renderer.Flush(); Game.Renderer.Flush();
@@ -250,8 +250,7 @@ namespace OpenRA.Graphics
public IndexBufferRc(World world) public IndexBufferRc(World world)
{ {
Buffer = Game.Renderer.Context.CreateIndexBuffer( Buffer = Game.Renderer.Context.CreateIndexBuffer(Util.CreateQuadIndices(world.Map.MapSize.X * world.Map.MapSize.Y));
Util.CreateQuadIndices(world.Map.MapSize.Width * world.Map.MapSize.Height));
} }
public void AddRef() { count++; } public void AddRef() { count++; }

View File

@@ -19,12 +19,12 @@ namespace OpenRA.Graphics
public static class Util public static class Util
{ {
// yes, our channel order is nuts. // yes, our channel order is nuts.
static readonly int[] ChannelMasks = [2, 1, 0, 3]; static readonly int[] ChannelMasks = { 2, 1, 0, 3 };
public static uint[] CreateQuadIndices(int quads) public static uint[] CreateQuadIndices(int quads)
{ {
var indices = new uint[quads * 6]; 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++) for (var i = 0; i < indices.Length; i++)
indices[i] = cornerVertexMap[i % 6] + (uint)(4 * (i / 6)); 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.X * angleSin - size.Y * angleCos) * size.Z / size.Y); (size.X * angleSin - size.Y * angleCos) * size.Z / size.Y);
return return new float3[]
[ {
center - ra, center - ra,
center + rb, center + rb,
center + ra, center + ra,
center - rb center - rb
]; };
} }
/// <summary> /// <summary>

View File

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

View File

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

View File

@@ -12,7 +12,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using OpenRA.Effects; using OpenRA.Effects;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Traits; using OpenRA.Traits;
@@ -32,33 +31,32 @@ namespace OpenRA.Graphics
public event Action PaletteInvalidated = null; public event Action PaletteInvalidated = null;
readonly HashSet<Actor> onScreenActors = []; readonly HashSet<Actor> onScreenActors = new();
readonly HardwarePalette palette = new(); readonly HardwarePalette palette = new();
readonly Dictionary<string, PaletteReference> palettes = []; readonly Dictionary<string, PaletteReference> palettes = new();
readonly IRenderTerrain terrainRenderer; readonly IRenderTerrain terrainRenderer;
readonly Lazy<DebugVisualizations> debugVis; readonly Lazy<DebugVisualizations> debugVis;
readonly Func<string, PaletteReference> createPaletteReference; readonly Func<string, PaletteReference> createPaletteReference;
readonly bool enableDepthBuffer; readonly bool enableDepthBuffer;
readonly List<IFinalizedRenderable> preparedRenderables = []; readonly List<IFinalizedRenderable> preparedRenderables = new();
readonly List<IFinalizedRenderable> preparedOverlayRenderables = []; readonly List<IFinalizedRenderable> preparedOverlayRenderables = new();
readonly List<IFinalizedRenderable> preparedAnnotationRenderables = []; readonly List<IFinalizedRenderable> preparedAnnotationRenderables = new();
readonly List<IRenderable> renderablesBuffer = []; readonly List<IRenderable> renderablesBuffer = new();
readonly IRenderer[] renderers; readonly IRenderer[] renderers;
readonly IRenderPostProcessPass[] postProcessPasses; readonly IRenderPostProcessPass[] postProcessPasses;
long[] renderablesKeysBuffer = [];
internal WorldRenderer(ModData modData, World world) internal WorldRenderer(ModData modData, World world)
{ {
World = world; World = world;
TileSize = World.Map.Rules.TerrainInfo.TileSize; TileSize = World.Map.Grid.TileSize;
TileScale = World.Map.Grid.TileScale; TileScale = World.Map.Grid.TileScale;
Viewport = new Viewport(this, world.Map); Viewport = new Viewport(this, world.Map);
createPaletteReference = CreatePaletteReference; createPaletteReference = CreatePaletteReference;
var mapGrid = modData.GetOrCreate<MapGrid>(); var mapGrid = modData.Manifest.Get<MapGrid>();
enableDepthBuffer = mapGrid.EnableDepthBuffer; enableDepthBuffer = mapGrid.EnableDepthBuffer;
foreach (var pal in world.TraitDict.ActorsWithTrait<ILoadsPalettes>()) foreach (var pal in world.TraitDict.ActorsWithTrait<ILoadsPalettes>())
@@ -72,7 +70,7 @@ namespace OpenRA.Graphics
renderers = world.WorldActor.TraitsImplementing<IRenderer>().ToArray(); renderers = world.WorldActor.TraitsImplementing<IRenderer>().ToArray();
terrainRenderer = world.WorldActor.TraitOrDefault<IRenderTerrain>(); terrainRenderer = world.WorldActor.TraitOrDefault<IRenderTerrain>();
debugVis = Exts.Lazy(world.WorldActor.TraitOrDefault<DebugVisualizations>); debugVis = Exts.Lazy(() => world.WorldActor.TraitOrDefault<DebugVisualizations>());
postProcessPasses = world.WorldActor.TraitsImplementing<IRenderPostProcessPass>().ToArray(); postProcessPasses = world.WorldActor.TraitsImplementing<IRenderPostProcessPass>().ToArray();
} }
@@ -159,14 +157,7 @@ namespace OpenRA.Graphics
renderablesBuffer.AddRange(e.Render(this)); renderablesBuffer.AddRange(e.Render(this));
// Renderables must be ordered using a stable sorting algorithm to avoid flickering artefacts // Renderables must be ordered using a stable sorting algorithm to avoid flickering artefacts
if (renderablesKeysBuffer.Length < renderablesBuffer.Count) foreach (var renderable in renderablesBuffer.OrderBy(RenderableZPositionComparisonKey))
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)
preparedRenderables.Add(renderable.PrepareRender(this)); preparedRenderables.Add(renderable.PrepareRender(this));
// PERF: Reuse collection to avoid allocations. // PERF: Reuse collection to avoid allocations.
@@ -380,8 +371,6 @@ namespace OpenRA.Graphics
} }
} }
ApplyPostProcessing(PostProcessPassType.AfterAnnotations);
Game.Renderer.Flush(); Game.Renderer.Flush();
preparedRenderables.Clear(); preparedRenderables.Clear();
@@ -395,22 +384,12 @@ namespace OpenRA.Graphics
Game.Renderer.SetPalette(palette); Game.Renderer.SetPalette(palette);
} }
/// <summary> // Conversion between world and screen coordinates
/// Converts a world position to a screen position.
/// </summary>
public float2 ScreenPosition(WPos pos) public float2 ScreenPosition(WPos pos)
{ {
return new float2((float)TileSize.Width * pos.X / TileScale, (float)TileSize.Height * (pos.Y - pos.Z) / TileScale); 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) public float3 Screen3DPosition(WPos pos)
{ {
// The projection from world coordinates to screen coordinates has // The projection from world coordinates to screen coordinates has
@@ -450,7 +429,7 @@ namespace OpenRA.Graphics
public float[] ScreenVector(in WVec vec) public float[] ScreenVector(in WVec vec)
{ {
var xyz = ScreenVectorComponents(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) public int2 ScreenPxOffset(in WVec vec)

View File

@@ -9,8 +9,7 @@
*/ */
#endregion #endregion
using System.Collections.Frozen; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
namespace OpenRA namespace OpenRA
@@ -25,10 +24,10 @@ namespace OpenRA
[FluentReference] [FluentReference]
public readonly string Description = ""; public readonly string Description = "";
public readonly FrozenSet<string> Types = FrozenSet<string>.Empty; public readonly HashSet<string> Types = new();
[FluentReference] [FluentReference]
public readonly FrozenSet<string> Contexts = FrozenSet<string>.Empty; public readonly HashSet<string> Contexts = new();
public readonly bool Readonly = false; public readonly bool Readonly = false;
public bool HasDuplicates { get; internal set; } public bool HasDuplicates { get; internal set; }
@@ -46,11 +45,11 @@ namespace OpenRA
Description = descriptionYaml.Value; Description = descriptionYaml.Value;
if (nodeDict.TryGetValue("Types", out var typesYaml)) 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)) if (nodeDict.TryGetValue("Contexts", out var contextYaml))
Contexts = FieldLoader.GetValue<ImmutableArray<string>>("Contexts", contextYaml.Value) Contexts = FieldLoader.GetValue<HashSet<string>>("Contexts", contextYaml.Value)
.Select(c => ContextFluentPrefix + "." + c).ToFrozenSet(); .Select(c => ContextFluentPrefix + "." + c).ToHashSet();
if (nodeDict.TryGetValue("Platform", out var platformYaml)) if (nodeDict.TryGetValue("Platform", out var platformYaml))
{ {

View File

@@ -17,16 +17,13 @@ namespace OpenRA
{ {
public sealed class HotkeyManager public sealed class HotkeyManager
{ {
[YamlNode("Keys", shared: true)] readonly Dictionary<string, Hotkey> settings;
sealed class HotkeySettings : SettingsModule { } readonly Dictionary<string, HotkeyDefinition> definitions = new();
readonly Dictionary<string, Hotkey> keys = new();
readonly Dictionary<string, HotkeyDefinition> definitions = []; public HotkeyManager(IReadOnlyFileSystem fileSystem, Dictionary<string, Hotkey> settings, Manifest manifest)
readonly Dictionary<string, Hotkey> keys = [];
readonly HotkeySettings hotkeySettings;
public HotkeyManager(IReadOnlyFileSystem fileSystem, ObjectCreator objectCreator, Manifest manifest)
{ {
hotkeySettings = Game.Settings.GetOrCreate<HotkeySettings>(objectCreator, manifest.Id); this.settings = settings;
var keyDefinitions = MiniYaml.Load(fileSystem, manifest.Hotkeys, null); var keyDefinitions = MiniYaml.Load(fileSystem, manifest.Hotkeys, null);
foreach (var kd in keyDefinitions) foreach (var kd in keyDefinitions)
@@ -36,9 +33,11 @@ namespace OpenRA
keys[kd.Key] = definition.Default; keys[kd.Key] = definition.Default;
} }
foreach (var node in hotkeySettings.Yaml.Nodes) foreach (var kv in settings)
if (definitions.TryGetValue(node.Key, out var definition) && !definition.Readonly) {
keys[node.Key] = FieldLoader.GetValue<Hotkey>(node.Key, node.Value.Value); if (definitions.TryGetValue(kv.Key, out var definition) && !definition.Readonly)
keys[kv.Key] = kv.Value;
}
foreach (var hd in definitions) foreach (var hd in definitions)
hd.Value.HasDuplicates = GetFirstDuplicate(hd.Value, this[hd.Value.Name].GetValue()) != null; hd.Value.HasDuplicates = GetFirstDuplicate(hd.Value, this[hd.Value.Name].GetValue()) != null;
@@ -69,9 +68,10 @@ namespace OpenRA
return; return;
keys[name] = value; keys[name] = value;
hotkeySettings.Yaml.Nodes.RemoveAll(n => n.Key == name);
if (value != definition.Default) 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; var hadDuplicates = definition.HasDuplicates;
definition.HasDuplicates = GetFirstDuplicate(definition, this[definition.Name].GetValue()) != null; definition.HasDuplicates = GetFirstDuplicate(definition, this[definition.Name].GetValue()) != null;
@@ -108,10 +108,5 @@ namespace OpenRA
public HotkeyReference this[string name] => new(GetHotkeyReference(name)); public HotkeyReference this[string name] => new(GetHotkeyReference(name));
public IEnumerable<HotkeyDefinition> Definitions => definitions.Values; 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 total = response.Content.Headers.ContentLength ?? -1;
var canReportProgress = total > 0; 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 totalRead = 0L;
var buffer = new byte[8192]; var buffer = new byte[8192];

View File

@@ -27,21 +27,25 @@ namespace OpenRA
public static bool TryParse(string s, out Hotkey result) public static bool TryParse(string s, out Hotkey result)
{ {
result = Invalid; result = Invalid;
if (s == null) if (string.IsNullOrWhiteSpace(s))
return false; return false;
Span<Range> ranges = stackalloc Range[2]; var parts = s.Split(' ');
var span = s.AsSpan();
var count = span.Split(ranges, ' ');
if (count == 0)
return false;
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; return false;
key = (Keycode)c;
}
var mods = Modifiers.None; 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; return false;
}
result = new Hotkey(key, mods); result = new Hotkey(key, mods);
return true; return true;

View File

@@ -23,7 +23,25 @@ namespace OpenRA
} }
public enum MouseInputEvent { Down, Move, Up, Scroll } 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] [Flags]
public enum MouseButton public enum MouseButton
@@ -47,7 +65,7 @@ namespace OpenRA
public static class ModifiersExts public static class ModifiersExts
{ {
[FluentReference] [FluentReference]
const string Cmd = "keycode-modifier.cmd"; public const string Cmd = "keycode-modifier.cmd";
[FluentReference(Traits.LintDictionaryReference.Values)] [FluentReference(Traits.LintDictionaryReference.Values)]
public static readonly IReadOnlyDictionary<Modifiers, string> ModifierFluentKeys = new Dictionary<Modifiers, string>() 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)); 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: // (b) Therefore:
// - au + 2bv adds (a + b) to (x, y) // - au + 2bv adds (a + b) to (x, y)
// - a correction factor is added if v is odd // - 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; var x = V - y;
return new CPos(x, y); return new CPos(x, y);
} }

View File

@@ -9,27 +9,51 @@
*/ */
#endregion #endregion
using System.Collections.Frozen; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using OpenRA.FileSystem; using OpenRA.FileSystem;
using OpenRA.Primitives;
namespace OpenRA 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 public class ModMetadata
{ {
// FieldLoader used here, must matching naming in YAML. // FieldLoader used here, must matching naming in YAML.
#pragma warning disable IDE1006 // Naming Styles #pragma warning disable IDE1006 // Naming Styles
[FluentReference] [FluentReference]
public readonly string Title; readonly string Title;
public readonly string Version; public readonly string Version;
public readonly string Website; public readonly string Website;
public readonly string WebIcon32; public readonly string WebIcon32;
[FluentReference]
[FluentReference(optional: true)] readonly string WindowTitle;
public readonly string WindowTitle;
public readonly bool Hidden; public readonly bool Hidden;
#pragma warning restore IDE1006 // Naming Styles #pragma warning restore IDE1006 // Naming Styles
@@ -37,56 +61,48 @@ namespace OpenRA
public string WindowTitleTranslated => WindowTitle != null ? FluentProvider.GetMessage(WindowTitle) : null; 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> /// <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 string Id;
public readonly IReadOnlyPackage Package; public readonly IReadOnlyPackage Package;
public readonly ModMetadata Metadata; public readonly ModMetadata Metadata;
public readonly ImmutableArray<string> public readonly string[]
Rules, ServerTraits, Rules, ServerTraits,
Sequences, ModelSequences, Cursors, Chrome, ChromeLayout, Sequences, ModelSequences, Cursors, Chrome, ChromeLayout,
Weapons, Voices, Notifications, Music, FluentMessages, TileSets, Weapons, Voices, Notifications, Music, FluentMessages, TileSets,
ChromeMetrics, MapCompatibility, Missions, Hotkeys; ChromeMetrics, MapCompatibility, Missions, Hotkeys;
public readonly FrozenDictionary<string, string> MapFolders; public readonly IReadOnlyDictionary<string, string> MapFolders;
public readonly MiniYaml FileSystem; public readonly MiniYaml FileSystem;
public readonly MiniYaml LoadScreen; public readonly MiniYaml LoadScreen;
public readonly string DefaultOrderGenerator; public readonly string DefaultOrderGenerator;
public readonly RendererConstants RendererConstants;
public readonly ImmutableArray<string> Assemblies = []; public readonly string[] Assemblies = Array.Empty<string>();
public readonly ImmutableArray<string> SoundFormats = []; public readonly string[] SoundFormats = Array.Empty<string>();
public readonly ImmutableArray<string> SpriteFormats = []; public readonly string[] SpriteFormats = Array.Empty<string>();
public readonly ImmutableArray<string> PackageFormats = []; public readonly string[] PackageFormats = Array.Empty<string>();
public readonly ImmutableArray<string> VideoFormats = []; public readonly string[] VideoFormats = Array.Empty<string>();
public readonly string SpriteSequenceFormat; public readonly int FontSheetSize = 512;
public readonly string TerrainFormat; public readonly int CursorSheetSize = 512;
// TODO: This should be controlled by a user-selected translation bundle! // TODO: This should be controlled by a user-selected translation bundle!
public readonly string FluentCulture = "en"; public readonly string FluentCulture = "en";
public readonly bool AllowUnusedFluentMessagesInExternalPackages = true; public readonly bool AllowUnusedFluentMessagesInExternalPackages = true;
static readonly FrozenSet<string> ReservedModuleNames = new HashSet<string> readonly string[] reservedModuleNames =
{ {
"Include", "Metadata", "FileSystem", "MapFolders", "Rules", "Include", "Metadata", "FileSystem", "MapFolders", "Rules",
"Sequences", "ModelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons", "Sequences", "ModelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons",
"Voices", "Notifications", "Music", "FluentMessages", "TileSets", "ChromeMetrics", "Missions", "Hotkeys", "Voices", "Notifications", "Music", "FluentMessages", "TileSets", "ChromeMetrics", "Missions", "Hotkeys",
"ServerTraits", "LoadScreen", "DefaultOrderGenerator", "SupportsMapsFrom", "SoundFormats", "SpriteFormats", "VideoFormats", "ServerTraits", "LoadScreen", "DefaultOrderGenerator", "SupportsMapsFrom", "SoundFormats", "SpriteFormats", "VideoFormats",
"SpriteSequenceFormat", "TerrainFormat", "RequiresMods", "PackageFormats", "AllowUnusedFluentMessagesInExternalPackages", "RendererConstants" "RequiresMods", "PackageFormats", "AllowUnusedFluentMessagesInExternalPackages", "FontSheetSize", "CursorSheetSize"
}.ToFrozenSet(); };
public readonly FrozenDictionary<string, MiniYaml> GlobalModData; readonly TypeDictionary modules = new();
readonly Dictionary<string, MiniYaml> yaml;
bool customDataLoaded;
public Manifest(string modId, IReadOnlyPackage package) public Manifest(string modId, IReadOnlyPackage package)
{ {
@@ -94,7 +110,7 @@ namespace OpenRA
Package = package; Package = package;
var stringPool = new HashSet<string>(); // Reuse common strings in YAML var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var nodes = MiniYaml.FromStream(package.GetStream("mod.yaml"), $"{package.Name}:mod.yaml", stringPool: stringPool).ToList(); var nodes = MiniYaml.FromStream(package.GetStream("mod.yaml"), $"{package.Name}:mod.yaml", stringPool: stringPool);
for (var i = nodes.Count - 1; i >= 0; i--) for (var i = nodes.Count - 1; i >= 0; i--)
{ {
if (nodes[i].Key != "Include") if (nodes[i].Key != "Include")
@@ -111,7 +127,7 @@ namespace OpenRA
} }
// Merge inherited overrides // 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"]); Metadata = FieldLoader.Load<ModMetadata>(yaml["Metadata"]);
@@ -148,59 +164,144 @@ namespace OpenRA
if (yaml.TryGetValue("SupportsMapsFrom", out var entry)) if (yaml.TryGetValue("SupportsMapsFrom", out var entry))
compat.AddRange(entry.Value.Split(',').Select(c => c.Trim())); compat.AddRange(entry.Value.Split(',').Select(c => c.Trim()));
MapCompatibility = compat.ToImmutableArray(); MapCompatibility = compat.ToArray();
if (yaml.TryGetValue("DefaultOrderGenerator", out entry)) if (yaml.TryGetValue("DefaultOrderGenerator", out entry))
DefaultOrderGenerator = entry.Value; DefaultOrderGenerator = entry.Value;
if (yaml.TryGetValue("Assemblies", out entry)) 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)) 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)) 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)) 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)) if (yaml.TryGetValue("VideoFormats", out entry))
VideoFormats = FieldLoader.GetValue<ImmutableArray<string>>("VideoFormats", entry.Value); VideoFormats = FieldLoader.GetValue<string[]>("VideoFormats", entry.Value);
if (yaml.TryGetValue("SpriteSequenceFormat", out entry))
SpriteSequenceFormat = entry.Value;
if (yaml.TryGetValue("TerrainFormat", out entry))
TerrainFormat = entry.Value;
if (yaml.TryGetValue("AllowUnusedFluentMessagesInExternalPackages", out entry)) if (yaml.TryGetValue("AllowUnusedFluentMessagesInExternalPackages", out entry))
AllowUnusedFluentMessagesInExternalPackages = AllowUnusedFluentMessagesInExternalPackages =
FieldLoader.GetValue<bool>("AllowUnusedFluentMessagesInExternalPackages", entry.Value); FieldLoader.GetValue<bool>("AllowUnusedFluentMessagesInExternalPackages", entry.Value);
if (yaml.TryGetValue("RendererConstants", out entry)) if (yaml.TryGetValue("FontSheetSize", out entry))
RendererConstants = FieldLoader.Load<RendererConstants>(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 else
RendererConstants = new RendererConstants(); {
// Automatically load the child nodes using FieldLoader
GlobalModData = yaml.Where(n => !ReservedModuleNames.Contains(n.Key)) module = oc.CreateObject<IGlobalModData>(kv.Key);
.ToFrozenDictionary(n => n.Key, n => n.Value); FieldLoader.Load(module, kv.Value);
} }
static ImmutableArray<string> YamlList(Dictionary<string, MiniYaml> yaml, string key) modules.Add(module);
{
if (!yaml.TryGetValue(key, out var value))
return [];
return value.Nodes.Select(n => n.Key).ToImmutableArray();
} }
static FrozenDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key) customDataLoaded = true;
}
static string[] YamlList(Dictionary<string, MiniYaml> yaml, string key)
{ {
if (!yaml.TryGetValue(key, out var value)) if (!yaml.TryGetValue(key, out var value))
return FrozenDictionary<string, string>.Empty; return Array.Empty<string>();
return value.ToDictionary(my => my.Value).ToFrozenDictionary(); return value.Nodes.Select(n => n.Key).ToArray();
}
static IReadOnlyDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
{
if (!yaml.TryGetValue(key, out var value))
return new Dictionary<string, string>();
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 public class OwnerInit : ActorInit, ISingleInstanceInit

View File

@@ -14,7 +14,7 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.Serialization;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Traits; using OpenRA.Traits;
@@ -30,15 +30,15 @@ namespace OpenRA
internal TypeDictionary InitDict => initDict.Value; internal TypeDictionary InitDict => initDict.Value;
public ActorReference(string type) 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; Type = type;
initDict = Exts.Lazy(() => initDict = Exts.Lazy(() =>
{ {
var dict = new TypeDictionary(); var dict = new TypeDictionary();
foreach (var i in inits.Nodes) foreach (var i in inits)
{ {
var init = LoadInit(i.Key, i.Value); var init = LoadInit(i.Key, i.Value);
if (init is ISingleInstanceInit && dict.Contains(init.GetType())) if (init is ISingleInstanceInit && dict.Contains(init.GetType()))
@@ -54,8 +54,13 @@ namespace OpenRA
public ActorReference(string type, TypeDictionary inits) public ActorReference(string type, TypeDictionary inits)
{ {
Type = type; Type = type;
var initsClone = new TypeDictionary(inits); initDict = new Lazy<TypeDictionary>(() =>
initDict = Exts.Lazy(() => initsClone); {
var dict = new TypeDictionary();
foreach (var i in inits)
dict.Add(i);
return dict;
});
} }
static ActorInit LoadInit(string initName, MiniYaml initYaml) static ActorInit LoadInit(string initName, MiniYaml initYaml)
@@ -65,15 +70,15 @@ namespace OpenRA
if (type == null) if (type == null)
throw new InvalidDataException($"Unknown initializer type '{initInstance[0]}Init'"); 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) if (initInstance.Length > 1)
type.GetField(nameof(ActorInit.InstanceName)).SetValue(init, initInstance[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) if (loader == null)
throw new InvalidDataException($"{initInstance[0]}Init does not define a yaml-assignable type."); throw new InvalidDataException($"{initInstance[0]}Init does not define a yaml-assignable type.");
loader.Invoke(init, [initYaml]); loader.Invoke(init, new[] { initYaml });
return init; return init;
} }
@@ -105,7 +110,11 @@ namespace OpenRA
public ActorReference Clone() 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) public void Add(ActorInit init)
@@ -116,15 +125,6 @@ namespace OpenRA
InitDict.Add(init); 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 void Remove(ActorInit o) { initDict.Value.Remove(o); }
public int RemoveAll<T>() where T : ActorInit public int RemoveAll<T>() where T : ActorInit

View File

@@ -9,7 +9,6 @@
*/ */
#endregion #endregion
using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
@@ -90,31 +89,6 @@ namespace OpenRA
return GetEnumerator(); return GetEnumerator();
} }
/// <summary>Returns the minimal region that covers at least the specified cells.</summary>
public static CellCoordsRegion BoundingRegion(IReadOnlyCollection<CPos> cells)
{
if (cells == null || cells.Count == 0)
throw new ArgumentException("cells must not be null or empty.", nameof(cells));
var minX = int.MaxValue;
var minY = int.MaxValue;
var maxX = int.MinValue;
var maxY = int.MinValue;
foreach (var cell in cells)
{
if (minX > cell.X)
minX = cell.X;
if (maxX < cell.X)
maxX = cell.X;
if (minY > cell.Y)
minY = cell.Y;
if (maxY < cell.Y)
maxY = cell.Y;
}
return new CellCoordsRegion(new CPos(minX, minY), new CPos(maxX, maxY));
}
public CPos TopLeft { get; } public CPos TopLeft { get; }
public CPos BottomRight { get; } public CPos BottomRight { get; }
} }

View File

@@ -63,16 +63,12 @@ namespace OpenRA
var u = (x - y) / 2; var u = (x - y) / 2;
var v = x + y; var v = x + y;
if (!Bounds.Contains(u, v))
throw new IndexOutOfRangeException();
return v * Size.Width + u; return v * Size.Width + u;
} }
// Resolve an array index from map coordinates // Resolve an array index from map coordinates
int Index(MPos uv) int Index(MPos uv)
{ {
if (!Bounds.Contains(uv.U, uv.V))
throw new IndexOutOfRangeException();
return uv.V * Size.Width + uv.U; 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)); 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 // Helper functions

View File

@@ -25,7 +25,7 @@ namespace OpenRA
protected readonly Rectangle Bounds; protected readonly Rectangle Bounds;
protected CellLayerBase(Map map) 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) protected CellLayerBase(MapGridType gridType, Size size)
{ {
@@ -40,19 +40,19 @@ namespace OpenRA
if (Size != anotherLayer.Size || GridType != anotherLayer.GridType) if (Size != anotherLayer.Size || GridType != anotherLayer.GridType)
throw new ArgumentException("Layers must have a matching size and shape (grid type).", nameof(anotherLayer)); 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> /// <summary>Clears the layer contents with their default value.</summary>
public virtual void Clear() public virtual void Clear()
{ {
Entries.AsSpan().Clear(); Array.Clear(Entries, 0, Entries.Length);
} }
/// <summary>Clears the layer contents with a known value.</summary> /// <summary>Clears the layer contents with a known value.</summary>
public virtual void Clear(T clearValue) public virtual void Clear(T clearValue)
{ {
Entries.AsSpan().Fill(clearValue); Array.Fill(Entries, clearValue);
} }
public IEnumerator<T> GetEnumerator() public IEnumerator<T> GetEnumerator()
@@ -64,15 +64,5 @@ namespace OpenRA
{ {
return GetEnumerator(); 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 HeightsOffset;
public readonly uint ResourcesOffset; public readonly uint ResourcesOffset;
public BinaryDataHeader(Stream s, Size expectedSize) public BinaryDataHeader(Stream s, int2 expectedSize)
{ {
Format = s.ReadUInt8(); Format = s.ReadUInt8();
var width = s.ReadUInt16(); var width = s.ReadUInt16();
var height = 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"); throw new InvalidDataException("Invalid tile data");
if (Format == 1) if (Format == 1)
@@ -112,7 +112,7 @@ namespace OpenRA
else if (type == Type.MiniYaml) else if (type == Type.MiniYaml)
field.SetValue(map, node.Value); field.SetValue(map, node.Value);
else else
FieldLoader.LoadFieldOrProperty(map, fieldName, node.Value.Value); FieldLoader.LoadField(map, fieldName, node.Value.Value);
} }
if (property != null) if (property != null)
@@ -122,7 +122,7 @@ namespace OpenRA
else if (type == Type.MiniYaml) else if (type == Type.MiniYaml)
property.SetValue(map, node.Value, null); property.SetValue(map, node.Value, null);
else 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> /// <summary>Defines the order of the fields in map.yaml.</summary>
static readonly MapField[] YamlFields = static readonly MapField[] YamlFields =
[ {
new("MapFormat"), new("MapFormat"),
new("RequiresMod"), new("RequiresMod"),
new("Title"), new("Title"),
@@ -179,7 +179,7 @@ namespace OpenRA
new("Voices", nameof(VoiceDefinitions), required: false), new("Voices", nameof(VoiceDefinitions), required: false),
new("Music", nameof(MusicDefinitions), required: false), new("Music", nameof(MusicDefinitions), required: false),
new("Notifications", nameof(NotificationDefinitions), required: false), new("Notifications", nameof(NotificationDefinitions), required: false),
]; };
// Format versions // Format versions
public int MapFormat { get; private set; } public int MapFormat { get; private set; }
@@ -193,13 +193,13 @@ namespace OpenRA
public bool LockPreview; public bool LockPreview;
public Rectangle Bounds; public Rectangle Bounds;
public MapVisibility Visibility = MapVisibility.Lobby; 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. // Player and actor yaml. Public for access by the map importers and lint checks.
public IReadOnlyCollection<MiniYamlNode> PlayerDefinitions = []; public IReadOnlyCollection<MiniYamlNode> PlayerDefinitions = ImmutableArray<MiniYamlNode>.Empty;
public IReadOnlyCollection<MiniYamlNode> ActorDefinitions = []; public IReadOnlyCollection<MiniYamlNode> ActorDefinitions = ImmutableArray<MiniYamlNode>.Empty;
// Custom map yaml. Public for access by the map importers and lint checks // Custom map yaml. Public for access by the map importers and lint checks
public MiniYaml RuleDefinitions; public MiniYaml RuleDefinitions;
@@ -211,7 +211,7 @@ namespace OpenRA
public MiniYaml MusicDefinitions; public MiniYaml MusicDefinitions;
public MiniYaml NotificationDefinitions; public MiniYaml NotificationDefinitions;
public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = []; public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = new();
// Generated data // Generated data
public readonly MapGrid Grid; public readonly MapGrid Grid;
@@ -283,7 +283,7 @@ namespace OpenRA
// Take the SHA1 // Take the SHA1
if (streams.Count == 0) if (streams.Count == 0)
return CryptoUtil.SHA1Hash([]); return CryptoUtil.SHA1Hash(Array.Empty<byte>());
var merged = streams[0]; var merged = streams[0];
for (var i = 1; i < streams.Count; i++) for (var i = 1; i < streams.Count; i++)
@@ -321,25 +321,26 @@ namespace OpenRA
/// Initializes a new map created by the editor or importer. /// 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. /// The map will not receive a valid UID until after it has been saved and reloaded.
/// </summary> /// </summary>
public Map(ModData modData, ITerrainInfo terrainInfo, Size size) public Map(ModData modData, ITerrainInfo terrainInfo, int width, int height)
{ {
this.modData = modData; this.modData = modData;
MapSize = size; var size = new Size(width, height);
Grid = modData.GetOrCreate<MapGrid>(); Grid = modData.Manifest.Get<MapGrid>();
Title = "Name your map here"; Title = "Name your map here";
Author = "Your name here"; Author = "Your name here";
MapSize = new int2(size);
Tileset = terrainInfo.Id; Tileset = terrainInfo.Id;
// Empty rules that can be added to by the importers. // Empty rules that can be added to by the importers.
// Will be dropped on save if nothing is added to it // Will be dropped on save if nothing is added to it
RuleDefinitions = new MiniYaml(""); RuleDefinitions = new MiniYaml("");
Tiles = new CellLayer<TerrainTile>(Grid.Type, MapSize); Tiles = new CellLayer<TerrainTile>(Grid.Type, size);
Resources = new CellLayer<ResourceTile>(Grid.Type, MapSize); Resources = new CellLayer<ResourceTile>(Grid.Type, size);
Height = new CellLayer<byte>(Grid.Type, MapSize); Height = new CellLayer<byte>(Grid.Type, size);
Ramp = new CellLayer<byte>(Grid.Type, MapSize); Ramp = new CellLayer<byte>(Grid.Type, size);
Tiles.Clear(terrainInfo.DefaultTerrainTile); Tiles.Clear(terrainInfo.DefaultTerrainTile);
if (Grid.MaximumTerrainHeight > 0) if (Grid.MaximumTerrainHeight > 0)
@@ -367,15 +368,16 @@ namespace OpenRA
if (MapFormat < SupportedMapFormat) if (MapFormat < SupportedMapFormat)
throw new InvalidDataException($"Map format {MapFormat} is not supported.\n File: {package.Name}"); throw new InvalidDataException($"Map format {MapFormat} is not supported.\n File: {package.Name}");
PlayerDefinitions = yaml.NodeWithKeyOrDefault("Players")?.Value.Nodes ?? []; PlayerDefinitions = yaml.NodeWithKeyOrDefault("Players")?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty;
ActorDefinitions = yaml.NodeWithKeyOrDefault("Actors")?.Value.Nodes ?? []; 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); var size = new Size(MapSize.X, MapSize.Y);
Resources = new CellLayer<ResourceTile>(Grid.Type, MapSize); Tiles = new CellLayer<TerrainTile>(Grid.Type, size);
Height = new CellLayer<byte>(Grid.Type, MapSize); Resources = new CellLayer<ResourceTile>(Grid.Type, size);
Ramp = new CellLayer<byte>(Grid.Type, MapSize); Height = new CellLayer<byte>(Grid.Type, size);
Ramp = new CellLayer<byte>(Grid.Type, size);
using (var s = Package.GetStream("map.bin")) using (var s = Package.GetStream("map.bin"))
{ {
@@ -383,9 +385,9 @@ namespace OpenRA
if (header.TilesOffset > 0) if (header.TilesOffset > 0)
{ {
s.Position = header.TilesOffset; 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 tile = s.ReadUInt16();
var index = s.ReadUInt8(); var index = s.ReadUInt8();
@@ -402,9 +404,9 @@ namespace OpenRA
if (header.ResourcesOffset > 0) if (header.ResourcesOffset > 0)
{ {
s.Position = header.ResourcesOffset; 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 type = s.ReadUInt8();
var density = s.ReadUInt8(); var density = s.ReadUInt8();
@@ -416,8 +418,8 @@ namespace OpenRA
if (header.HeightsOffset > 0) if (header.HeightsOffset > 0)
{ {
s.Position = header.HeightsOffset; s.Position = header.HeightsOffset;
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++)
Height[new MPos(i, j)] = s.ReadUInt8().Clamp((byte)0, Grid.MaximumTerrainHeight); 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); Sequences = new SequenceSet(this, modData, Tileset, SequenceDefinitions);
var tl = new MPos(0, 0).ToCPos(this); 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); AllCells = new CellRegion(Grid.Type, tl, br);
var btl = new PPos(Bounds.Left, Bounds.Top); var btl = new PPos(Bounds.Left, Bounds.Top);
@@ -514,7 +516,7 @@ namespace OpenRA
foreach (var cell in AllCells) foreach (var cell in AllCells)
{ {
var uv = cell.ToMPos(Grid.Type); var uv = cell.ToMPos(Grid.Type);
cellProjection[uv] = []; cellProjection[uv] = Array.Empty<PPos>();
inverseCellProjection[uv] = new List<MPos>(1); inverseCellProjection[uv] = new List<MPos>(1);
} }
@@ -530,7 +532,7 @@ namespace OpenRA
if (Grid.MaximumTerrainHeight == 0) if (Grid.MaximumTerrainHeight == 0)
{ {
uv = cell.ToMPos(Grid.Type); uv = cell.ToMPos(Grid.Type);
cellProjection[cell] = [(PPos)uv]; cellProjection[cell] = new[] { (PPos)uv };
var inverse = inverseCellProjection[uv]; var inverse = inverseCellProjection[uv];
inverse.Clear(); inverse.Clear();
inverse.Add(uv); inverse.Add(uv);
@@ -605,7 +607,7 @@ namespace OpenRA
// Any changes to this function should be reflected when setting projectionSafeBounds. // Any changes to this function should be reflected when setting projectionSafeBounds.
var height = mapHeight[uv]; var height = mapHeight[uv];
if (height == 0) if (height == 0)
return [(PPos)uv]; return new[] { (PPos)uv };
// Odd-height ramps get bumped up a level to the next even height layer // Odd-height ramps get bumped up a level to the next even height layer
if ((height & 1) == 1 && Ramp[uv] != 0) if ((height & 1) == 1 && Ramp[uv] != 0)
@@ -682,13 +684,13 @@ namespace OpenRA
writer.Write(TileFormat); writer.Write(TileFormat);
// Size // Size
writer.Write((ushort)MapSize.Width); writer.Write((ushort)MapSize.X);
writer.Write((ushort)MapSize.Height); writer.Write((ushort)MapSize.Y);
// Data offsets // Data offsets
const int TilesOffset = 17; const int TilesOffset = 17;
var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.Width * MapSize.Height + 17 : 0; var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.X * MapSize.Y + 17 : 0;
var resourcesOffset = (Grid.MaximumTerrainHeight > 0 ? 4 : 3) * MapSize.Width * MapSize.Height + 17; var resourcesOffset = (Grid.MaximumTerrainHeight > 0 ? 4 : 3) * MapSize.X * MapSize.Y + 17;
writer.Write((uint)TilesOffset); writer.Write((uint)TilesOffset);
writer.Write((uint)heightsOffset); writer.Write((uint)heightsOffset);
@@ -697,9 +699,9 @@ namespace OpenRA
// Tile data // Tile data
if (TilesOffset != 0) if (TilesOffset != 0)
{ {
for (var i = 0; i < MapSize.Width; i++) for (var i = 0; i < MapSize.X; i++)
{ {
for (var j = 0; j < MapSize.Height; j++) for (var j = 0; j < MapSize.Y; j++)
{ {
var tile = Tiles[new MPos(i, j)]; var tile = Tiles[new MPos(i, j)];
writer.Write(tile.Type); writer.Write(tile.Type);
@@ -710,16 +712,16 @@ namespace OpenRA
// Height data // Height data
if (heightsOffset != 0) if (heightsOffset != 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++)
writer.Write(Height[new MPos(i, j)]); writer.Write(Height[new MPos(i, j)]);
// Resource data // Resource data
if (resourcesOffset != 0) 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)]; var tile = Resources[new MPos(i, j)];
writer.Write(tile.Type); writer.Write(tile.Type);
@@ -756,7 +758,7 @@ namespace OpenRA
var positions = new List<(MPos Uv, Color Color)>(); var positions = new List<(MPos Uv, Color Color)>();
foreach (var actor in actors) 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 ai = Rules.Actors[actor.Value.Value];
var impsis = ai.TraitInfos<IMapPreviewSignatureInfo>(); var impsis = ai.TraitInfos<IMapPreviewSignatureInfo>();
@@ -1042,7 +1044,7 @@ namespace OpenRA
return (PPos)CellContaining(projectedPos).ToMPos(Grid.Type); return (PPos)CellContaining(projectedPos).ToMPos(Grid.Type);
} }
static readonly PPos[] NoProjectedCells = []; static readonly PPos[] NoProjectedCells = Array.Empty<PPos>();
public PPos[] ProjectedCellsCovering(MPos uv) public PPos[] ProjectedCellsCovering(MPos uv)
{ {
if (!initializedCellProjection) if (!initializedCellProjection)
@@ -1062,7 +1064,7 @@ namespace OpenRA
InitializeCellProjection(); InitializeCellProjection();
if (!inverseCellProjection.Contains(uv)) if (!inverseCellProjection.Contains(uv))
return []; return new List<MPos>();
return inverseCellProjection[uv]; return inverseCellProjection[uv];
} }
@@ -1087,15 +1089,16 @@ namespace OpenRA
var oldMapResources = Resources; var oldMapResources = Resources;
var oldMapHeight = Height; var oldMapHeight = Height;
var oldMapRamp = Ramp; var oldMapRamp = Ramp;
var newSize = new Size(width, height);
MapSize = new Size(width, height); Tiles = CellLayer.Resize(oldMapTiles, newSize, oldMapTiles[MPos.Zero]);
Tiles = CellLayer.Resize(oldMapTiles, MapSize, oldMapTiles[MPos.Zero]); Resources = CellLayer.Resize(oldMapResources, newSize, oldMapResources[MPos.Zero]);
Resources = CellLayer.Resize(oldMapResources, MapSize, oldMapResources[MPos.Zero]); Height = CellLayer.Resize(oldMapHeight, newSize, oldMapHeight[MPos.Zero]);
Height = CellLayer.Resize(oldMapHeight, MapSize, oldMapHeight[MPos.Zero]); Ramp = CellLayer.Resize(oldMapRamp, newSize, oldMapRamp[MPos.Zero]);
Ramp = CellLayer.Resize(oldMapRamp, MapSize, oldMapRamp[MPos.Zero]); MapSize = new int2(newSize);
var tl = new MPos(0, 0); 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)); 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)); 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) public byte GetTerrainIndex(CPos cell)
{
return GetTerrainIndex(cell.ToMPos(this));
}
public byte GetTerrainIndex(MPos uv)
{ {
// Lazily initialize a cache for terrain indexes. // Lazily initialize a cache for terrain indexes.
if (cachedTerrainIndexes == null) if (cachedTerrainIndexes == null)
@@ -1155,6 +1153,7 @@ namespace OpenRA
cachedTerrainIndexes.Clear(InvalidCachedTerrainIndex); cachedTerrainIndexes.Clear(InvalidCachedTerrainIndex);
} }
var uv = cell.ToMPos(this);
var terrainIndex = cachedTerrainIndexes[uv]; var terrainIndex = cachedTerrainIndexes[uv];
// PERF: Cache terrain indexes per cell on demand. // PERF: Cache terrain indexes per cell on demand.
@@ -1169,12 +1168,7 @@ namespace OpenRA
public TerrainTypeInfo GetTerrainInfo(CPos cell) public TerrainTypeInfo GetTerrainInfo(CPos cell)
{ {
return GetTerrainInfo(cell.ToMPos(this)); return Rules.TerrainInfo.TerrainTypes[GetTerrainIndex(cell)];
}
public TerrainTypeInfo GetTerrainInfo(MPos uv)
{
return Rules.TerrainInfo.TerrainTypes[GetTerrainIndex(uv)];
} }
public CPos Clamp(CPos cell) public CPos Clamp(CPos cell)

View File

@@ -14,13 +14,13 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using OpenRA.FileSystem; using OpenRA.FileSystem;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Support; using OpenRA.Support;
using FS = OpenRA.FileSystem.FileSystem;
namespace OpenRA namespace OpenRA
{ {
@@ -28,27 +28,26 @@ namespace OpenRA
{ {
public static readonly MapPreview UnknownMap = new(null, null, MapGridType.Rectangular, null); public static readonly MapPreview UnknownMap = new(null, null, MapGridType.Rectangular, null);
public IReadOnlyDictionary<IReadOnlyPackage, MapClassification> MapLocations => mapLocations; public IReadOnlyDictionary<IReadOnlyPackage, MapClassification> MapLocations => mapLocations;
readonly Dictionary<IReadOnlyPackage, MapClassification> mapLocations = []; readonly Dictionary<IReadOnlyPackage, MapClassification> mapLocations = new();
public bool LoadPreviewImages = true; public bool LoadPreviewImages = true;
readonly Manifest manifest; readonly Cache<string, MapPreview> previews;
readonly FS modFiles; readonly ModData modData;
Cache<string, MapPreview> previews;
readonly SheetBuilder sheetBuilder; readonly SheetBuilder sheetBuilder;
Thread previewLoaderThread; Thread previewLoaderThread;
bool previewLoaderThreadShutDown = true; bool previewLoaderThreadShutDown = true;
readonly object syncRoot = new(); 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> /// <summary>
/// The most recently modified or loaded map at runtime. /// The most recently modified or loaded map at runtime.
/// </summary> /// </summary>
public string LastModifiedMap { get; private set; } = null; public string LastModifiedMap { get; private set; } = null;
readonly Dictionary<string, string> mapUpdates = []; readonly Dictionary<string, string> mapUpdates = new();
string lastLoadedLastModifiedMap; string lastLoadedLastModifiedMap;
@@ -68,11 +67,13 @@ namespace OpenRA
return null; return null;
} }
public MapCache(Manifest manifest, FS modFiles) public MapCache(ModData modData)
{ {
this.manifest = manifest; this.modData = modData;
this.modFiles = modFiles;
sheetBuilder = new SheetBuilder(SheetType.BGRA, manifest.RendererConstants.MapPreviewSheetSize); 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() public void UpdateMaps()
@@ -81,21 +82,20 @@ namespace OpenRA
tracker.UpdateMaps(this); tracker.UpdateMaps(this);
} }
public void LoadMaps(ModData modData) public void LoadMaps()
{ {
// Utility mod that does not support maps // Utility mod that does not support maps
if (manifest.MapFolders.Count == 0) if (!modData.Manifest.Contains<MapGrid>())
return; return;
var gridType = modData.GetOrCreate<MapGrid>().Type; var mapGrid = modData.Manifest.Get<MapGrid>();
previews = new Cache<string, MapPreview>(uid => new MapPreview(modData, uid, gridType, this));
// Enumerate map directories // Enumerate map directories
foreach (var kv in manifest.MapFolders) foreach (var kv in modData.Manifest.MapFolders)
{ {
var name = kv.Key; var name = kv.Key;
var classification = string.IsNullOrEmpty(kv.Value) var classification = string.IsNullOrEmpty(kv.Value)
? MapClassification.Unknown : Enum.Parse<MapClassification>(kv.Value); ? MapClassification.Unknown : Enum<MapClassification>.Parse(kv.Value);
IReadOnlyPackage package; IReadOnlyPackage package;
var optional = name.StartsWith('~'); var optional = name.StartsWith('~');
@@ -110,7 +110,7 @@ namespace OpenRA
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && !File.Exists(resolved)) if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && !File.Exists(resolved))
Directory.CreateDirectory(resolved); Directory.CreateDirectory(resolved);
package = modFiles.OpenPackage(name); package = modData.ModFiles.OpenPackage(name);
} }
catch catch
{ {
@@ -121,38 +121,42 @@ namespace OpenRA
} }
mapLocations.Add(package, classification); 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. // PERF: Load the mod YAML once outside the loop, and reuse it when resolving each maps custom YAML.
var modDataRules = modData.GetRulesYaml(); var modDataRules = modData.GetRulesYaml();
foreach (var kv in MapLocations) foreach (var kv in MapLocations)
{
foreach (var map in kv.Key.Contents) 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 // We only want to track maps in runtime, not at loadtime
LastModifiedMap = null; 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, void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap,
MapGridType? gridType = null, MiniYamlNode[][] modDataRules = null) IEnumerable<List<MiniYamlNode>> modDataRules)
{ {
IReadOnlyPackage mapPackage = null; IReadOnlyPackage mapPackage = null;
try try
{ {
using (new PerfTimer(map)) using (new PerfTimer(map))
{ {
mapPackage = package.OpenPackage(map, modFiles); mapPackage = package.OpenPackage(map, modData.ModFiles);
if (mapPackage != null) if (mapPackage != null)
{ {
var uid = Map.ComputeUID(mapPackage); var uid = Map.ComputeUID(mapPackage);
previews[uid].UpdateFromMapWithoutOwningPackage(mapPackage, package, classification, gridType, modDataRules); previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type, modDataRules);
mapPackage.Dispose();
// Freeing the package to save memory if there is a lot of Maps
previews[uid].DisposePackage();
if (oldMap != uid) if (oldMap != uid)
{ {
@@ -177,8 +181,12 @@ namespace OpenRA
public IEnumerable<IReadWritePackage> EnumerateMapDirPackages(MapClassification classification = MapClassification.System) 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 // Enumerate map directories
foreach (var kv in manifest.MapFolders) foreach (var kv in modData.Manifest.MapFolders)
{ {
if (!Enum.TryParse(kv.Value, out MapClassification packageClassification)) if (!Enum.TryParse(kv.Value, out MapClassification packageClassification))
continue; continue;
@@ -196,7 +204,7 @@ namespace OpenRA
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && (!Directory.Exists(resolved) || !File.Exists(resolved))) if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && (!Directory.Exists(resolved) || !File.Exists(resolved)))
continue; continue;
using (var package = (IReadWritePackage)modFiles.OpenPackage(name)) using (var package = (IReadWritePackage)modData.ModFiles.OpenPackage(name))
yield return package; yield return package;
} }
} }
@@ -216,7 +224,7 @@ namespace OpenRA
foreach (var mapDirPackage in mapDirPackages) foreach (var mapDirPackage in mapDirPackages)
foreach (var map in mapDirPackage.Contents) 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; yield return mapPackage;
} }
@@ -231,37 +239,67 @@ namespace OpenRA
.ToList(); .ToList();
foreach (var uid in queryUids) foreach (var uid in queryUids)
previews[uid].BeginRemoteSearch(); previews[uid].UpdateRemoteSearch(MapStatus.Searching, null, null);
Task.Run(async () => Task.Run(async () =>
{ {
var client = HttpClientFactory.Create(); var client = HttpClientFactory.Create();
var stringPool = new HashSet<string>(); // Reuse common strings in YAML var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var buffer = new StringBuilder();
// Limit each query to 50 maps at a time to avoid request size limits // Limit each query to 50 maps at a time to avoid request size limits
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"; var url = repositoryUrl + "hash/" + string.Join(",", batchUids) + "/yaml";
using (new PerfTimer("RemoteMapDetails")) using (new PerfTimer("RemoteMapDetails"))
{ {
try try
{ {
var result = await client.GetStreamAsync(url); await using (var resultStream = await client.GetStreamAsync(url))
foreach (var kv in MiniYaml.FromStream(result, url, stringPool: stringPool)) {
previews[kv.Key].CompleteRemoteSearch(kv.Value, mapDetailsReceived); 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) catch (Exception e)
{ {
Log.Write("debug", "Remote map query failed with error:"); Log.Write("debug", "Remote map query failed with error:");
Log.Write("debug", e); Log.Write("debug", e);
Log.Write("debug", $"URL was: {url}"); Log.Write("debug", $"URL was: {url}");
}
foreach (var uid in batchUids) foreach (var uid in batchUids)
{ {
var p = previews[uid]; var p = previews[uid];
if (p.Status == MapStatus.Searching) p.UpdateRemoteSearch(MapStatus.Unavailable, null, null);
p.CompleteRemoteSearch(null, mapQueryFailed); 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. // Release the buffer by forcing changes to be written out to the texture, allowing the buffer to be reclaimed by GC.
if (sheetBuilder.Current != null)
Game.RunAfterTick(sheetBuilder.Current.ReleaseBuffer); Game.RunAfterTick(sheetBuilder.Current.ReleaseBuffer);
Log.Write("debug", "MapCache.LoadAsyncInternal ended"); Log.Write("debug", "MapCache.LoadAsyncInternal ended");
} }

View File

@@ -20,24 +20,26 @@ namespace OpenRA
public sealed class MapDirectoryTracker : IDisposable public sealed class MapDirectoryTracker : IDisposable
{ {
readonly FileSystemWatcher watcher; readonly FileSystemWatcher watcher;
readonly MapGrid mapGrid;
readonly IReadOnlyPackage package; readonly IReadOnlyPackage package;
readonly MapClassification classification; readonly MapClassification classification;
enum MapAction { Add, Delete, Update } enum MapAction { Add, Delete, Update }
readonly Dictionary<string, MapAction> mapActionQueue = []; readonly Dictionary<string, MapAction> mapActionQueue = new();
bool dirty = false; bool dirty = false;
public MapDirectoryTracker(IReadOnlyPackage package, MapClassification classification) public MapDirectoryTracker(MapGrid mapGrid, IReadOnlyPackage package, MapClassification classification)
{ {
this.mapGrid = mapGrid;
this.package = package; this.package = package;
this.classification = classification; this.classification = classification;
watcher = new FileSystemWatcher(package.Name); watcher = new FileSystemWatcher(package.Name);
watcher.Changed += (_, e) => AddMapAction(MapAction.Update, e.FullPath); watcher.Changed += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Update, e.FullPath);
watcher.Created += (_, e) => AddMapAction(MapAction.Add, e.FullPath); watcher.Created += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Add, e.FullPath);
watcher.Deleted += (_, e) => AddMapAction(MapAction.Delete, e.FullPath); watcher.Deleted += (object sender, FileSystemEventArgs e) => AddMapAction(MapAction.Delete, e.FullPath);
watcher.Renamed += (_, e) => AddMapAction(MapAction.Add, e.FullPath, e.OldFullPath); watcher.Renamed += (object sender, RenamedEventArgs e) => AddMapAction(MapAction.Add, e.FullPath, e.OldFullPath);
watcher.IncludeSubdirectories = true; watcher.IncludeSubdirectories = true;
watcher.EnableRaisingEvents = true; watcher.EnableRaisingEvents = true;
@@ -84,7 +86,7 @@ namespace OpenRA
dirty = false; dirty = false;
foreach (var mapAction in mapActionQueue) foreach (var mapAction in mapActionQueue)
{ {
var map = mapcache.FirstOrDefault(x => x.Path == mapAction.Key && x.Status == MapStatus.Available); var map = mapcache.FirstOrDefault(x => x.PackageName == mapAction.Key && x.Status == MapStatus.Available);
if (map != null) if (map != null)
{ {
if (mapAction.Value == MapAction.Delete) if (mapAction.Value == MapAction.Delete)
@@ -96,7 +98,7 @@ namespace OpenRA
{ {
Console.WriteLine(mapAction.Key + " was updated"); Console.WriteLine(mapAction.Key + " was updated");
map.Invalidate(); 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 else
@@ -104,7 +106,7 @@ namespace OpenRA
if (mapAction.Value != MapAction.Delete) if (mapAction.Value != MapAction.Delete)
{ {
Console.WriteLine(mapAction.Key + " was added"); 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 #endregion
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using OpenRA.Primitives;
using OpenRA.Traits; using OpenRA.Traits;
namespace OpenRA namespace OpenRA
@@ -25,8 +25,8 @@ namespace OpenRA
public readonly struct CellRamp public readonly struct CellRamp
{ {
public readonly int CenterHeightOffset; public readonly int CenterHeightOffset;
public readonly ImmutableArray<WVec> Corners; public readonly WVec[] Corners;
public readonly ImmutableArray<ImmutableArray<WVec>> Polygons; public readonly WVec[][] Polygons;
public readonly WRot Orientation; public readonly WRot Orientation;
public CellRamp(MapGridType type, WRot orientation, public CellRamp(MapGridType type, WRot orientation,
@@ -37,43 +37,43 @@ namespace OpenRA
Orientation = orientation; Orientation = orientation;
if (type == MapGridType.RectangularIsometric) if (type == MapGridType.RectangularIsometric)
{ {
Corners = Corners = new[]
[ {
new WVec(0, -724, 724 * (int)tl), new WVec(0, -724, 724 * (int)tl),
new WVec(724, 0, 724 * (int)tr), new WVec(724, 0, 724 * (int)tr),
new WVec(0, 724, 724 * (int)br), new WVec(0, 724, 724 * (int)br),
new WVec(-724, 0, 724 * (int)bl), new WVec(-724, 0, 724 * (int)bl),
]; };
} }
else else
{ {
Corners = Corners = new[]
[ {
new WVec(-512, -512, 512 * (int)tl), new WVec(-512, -512, 512 * (int)tl),
new WVec(512, -512, 512 * (int)tr), new WVec(512, -512, 512 * (int)tr),
new WVec(512, 512, 512 * (int)br), new WVec(512, 512, 512 * (int)br),
new WVec(-512, 512, 512 * (int)bl) new WVec(-512, 512, 512 * (int)bl)
]; };
} }
if (split == RampSplit.X) if (split == RampSplit.X)
{ {
Polygons = Polygons = new[]
[ {
[Corners[0], Corners[1], Corners[3]], new[] { Corners[0], Corners[1], Corners[3] },
[Corners[1], Corners[2], Corners[3]] new[] { Corners[1], Corners[2], Corners[3] }
]; };
} }
else if (split == RampSplit.Y) else if (split == RampSplit.Y)
{ {
Polygons = Polygons = new[]
[ {
[Corners[0], Corners[1], Corners[2]], new[] { Corners[0], Corners[1], Corners[2] },
[Corners[0], Corners[2], Corners[3]] new[] { Corners[0], Corners[2], Corners[3] }
]; };
} }
else else
Polygons = [Corners]; Polygons = new[] { Corners };
// Initial value must be assigned before HeightOffset can be called // Initial value must be assigned before HeightOffset can be called
CenterHeightOffset = 0; CenterHeightOffset = 0;
@@ -84,11 +84,10 @@ namespace OpenRA
{ {
// Enumerate over the polygons, assuming that they are triangles // 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 // If the ramp is not split we will take the first three vertices of the corners as a valid triangle
int u; WVec[] p = null;
int v; var u = 0;
ImmutableArray<WVec> p; var v = 0;
var i = 0; for (var i = 0; i < Polygons.Length; i++)
do
{ {
p = Polygons[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; 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 // Point is within the triangle if 0 <= u,v <= 1024
if (u >= 0 && u <= 1024 && v >= 0 && v <= 1024) if (u >= 0 && u <= 1024 && v >= 0 && v <= 1024)
break; break;
i++;
} }
while (i < Polygons.Length);
// Calculate w from u,v and interpolate height // Calculate w from u,v and interpolate height
return (u * p[0].Z + v * p[1].Z + (1024 - u - v) * p[2].Z) / 1024; 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 class MapGrid : IGlobalModData
{ {
public readonly MapGridType Type = MapGridType.Rectangular; public readonly MapGridType Type = MapGridType.Rectangular;
public readonly Size TileSize = new(24, 24);
public readonly byte MaximumTerrainHeight = 0; public readonly byte MaximumTerrainHeight = 0;
public readonly SubCell DefaultSubCell = (SubCell)byte.MaxValue; public readonly SubCell DefaultSubCell = (SubCell)byte.MaxValue;
@@ -117,19 +114,19 @@ namespace OpenRA
public readonly bool EnableDepthBuffer = false; public readonly bool EnableDepthBuffer = false;
public readonly ImmutableArray<WVec> SubCellOffsets = public readonly WVec[] SubCellOffsets =
[ {
new(0, 0, 0), // full cell - index 0 new(0, 0, 0), // full cell - index 0
new(-299, -256, 0), // top left - index 1 new(-299, -256, 0), // top left - index 1
new(256, -256, 0), // top right - index 2 new(256, -256, 0), // top right - index 2
new(0, 0, 0), // center - index 3 new(0, 0, 0), // center - index 3
new(-299, 256, 0), // bottom left - index 4 new(-299, 256, 0), // bottom left - index 4
new(256, 256, 0), // bottom right - index 5 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; } public int TileScale { get; }
@@ -162,9 +159,8 @@ namespace OpenRA
var halfBackward = -halfForward; var halfBackward = -halfForward;
// Slope types are hardcoded following the convention from the TS and RA2 map format // Slope types are hardcoded following the convention from the TS and RA2 map format
Ramps = Ramps = new[]
[ {
// Flat // Flat
new CellRamp(Type, WRot.None), 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, 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, tr: RampCornerHeight.Half, bl: RampCornerHeight.Half, split: RampSplit.X),
new CellRamp(Type, WRot.None, tl: RampCornerHeight.Half, br: RampCornerHeight.Half, split: RampSplit.X), new CellRamp(Type, WRot.None, tl: RampCornerHeight.Half, br: RampCornerHeight.Half, split: RampSplit.X),
]; };
TilesByDistance = CreateTilesByDistance(); TilesByDistance = CreateTilesByDistance();
} }
ImmutableArray<ImmutableArray<CVec>> CreateTilesByDistance() CVec[][] CreateTilesByDistance()
{ {
var ts = new List<CVec>[MaximumTileSearchRange + 1]; var ts = new List<CVec>[MaximumTileSearchRange + 1];
for (var i = 0; i < MaximumTileSearchRange + 1; i++) for (var i = 0; i < MaximumTileSearchRange + 1; i++)
ts[i] = []; ts[i] = new List<CVec>();
for (var j = -MaximumTileSearchRange; j <= MaximumTileSearchRange; j++) for (var j = -MaximumTileSearchRange; j <= MaximumTileSearchRange; j++)
for (var i = -MaximumTileSearchRange; i <= MaximumTileSearchRange; i++) 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) public WVec OffsetOfSubCell(SubCell subCell)

View File

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

View File

@@ -11,7 +11,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -24,11 +23,10 @@ using OpenRA.FileSystem;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Support; using OpenRA.Support;
using OpenRA.Traits;
namespace OpenRA 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 // Used for grouping maps in the UI
[Flags] [Flags]
@@ -37,8 +35,7 @@ namespace OpenRA
Unknown = 0, Unknown = 0,
System = 1, System = 1,
User = 2, User = 2,
Remote = 4, Remote = 4
Generated = 8
} }
[SuppressMessage("StyleCop.CSharp.NamingRules", [SuppressMessage("StyleCop.CSharp.NamingRules",
@@ -51,10 +48,10 @@ namespace OpenRA
{ {
public readonly string title; public readonly string title;
public readonly string author; public readonly string author;
public readonly ImmutableArray<string> categories; public readonly string[] categories;
public readonly int players; public readonly int players;
public readonly Rectangle bounds; public readonly Rectangle bounds;
public readonly ImmutableArray<short> spawnpoints = []; public readonly short[] spawnpoints = Array.Empty<short>();
public readonly MapGridType map_grid_type; public readonly MapGridType map_grid_type;
public readonly string minimap; public readonly string minimap;
public readonly bool downloading; public readonly bool downloading;
@@ -72,12 +69,12 @@ namespace OpenRA
{ {
public int MapFormat; public int MapFormat;
public string Title; public string Title;
public ImmutableArray<string> Categories; public string[] Categories;
public string Author; public string Author;
public string TileSet; public string TileSet;
public MapPlayers Players; public MapPlayers Players;
public int PlayerCount; public int PlayerCount;
public ImmutableArray<CPos> SpawnPoints; public CPos[] SpawnPoints;
public MapGridType GridType; public MapGridType GridType;
public Rectangle Bounds; public Rectangle Bounds;
public Png Preview; public Png Preview;
@@ -85,7 +82,6 @@ namespace OpenRA
public MapClassification Class; public MapClassification Class;
public MapVisibility Visibility; public MapVisibility Visibility;
public DateTime ModifiedDate; public DateTime ModifiedDate;
public MapGenerationArgs GenerationArgs;
public MiniYaml RuleDefinitions; public MiniYaml RuleDefinitions;
public MiniYaml WeaponDefinitions; public MiniYaml WeaponDefinitions;
@@ -117,7 +113,7 @@ namespace OpenRA
return key == "world" || key == "player"; 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"); RuleDefinitions = LoadRuleSection(yaml, "Rules");
WeaponDefinitions = LoadRuleSection(yaml, "Weapons"); WeaponDefinitions = LoadRuleSection(yaml, "Weapons");
@@ -132,9 +128,9 @@ namespace OpenRA
{ {
if (FluentMessageDefinitions != null) if (FluentMessageDefinitions != null)
{ {
var files = ImmutableArray<string>.Empty; var files = Array.Empty<string>();
if (FluentMessageDefinitions.Value != null) if (FluentMessageDefinitions.Value != null)
files = FieldLoader.GetValue<ImmutableArray<string>>("value", FluentMessageDefinitions.Value); files = FieldLoader.GetValue<string[]>("value", FluentMessageDefinitions.Value);
string text = null; string text = null;
if (FluentMessageDefinitions.Nodes.Length > 0) if (FluentMessageDefinitions.Nodes.Length > 0)
@@ -160,8 +156,8 @@ namespace OpenRA
var files = Enumerable.Empty<string>(); var files = Enumerable.Empty<string>();
if (RuleDefinitions.Value != null) if (RuleDefinitions.Value != null)
{ {
var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", RuleDefinitions.Value); var mapFiles = FieldLoader.GetValue<string[]>("value", RuleDefinitions.Value);
files = files.Concat(mapFiles); files = files.Append(mapFiles);
} }
var stringPool = new HashSet<string>(); // Reuse common strings in YAML 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 MapCache cache;
readonly ModData modData; readonly ModData modData;
IReadOnlyPackage package;
public readonly string Uid; public readonly string Uid;
public string PackageName { get; private set; }
public string Path { get; private set; } IReadOnlyPackage package;
public IReadOnlyPackage Package
void LoadPackage()
{ {
if (package == null && parentPackage != null) get
package = parentPackage.OpenPackage(Path, modData.ModFiles); {
package ??= parentPackage.OpenPackage(PackageName, modData.ModFiles);
return package;
} }
public Map ToMap() private set => package = value;
{
LoadPackage();
using (new PerfTimer("Map"))
return new Map(modData, package);
} }
IReadOnlyPackage parentPackage; IReadOnlyPackage parentPackage;
@@ -227,12 +219,12 @@ namespace OpenRA
public int MapFormat => innerData.MapFormat; public int MapFormat => innerData.MapFormat;
public string Title => innerData.Title; public string Title => innerData.Title;
public ImmutableArray<string> Categories => innerData.Categories; public string[] Categories => innerData.Categories;
public string Author => innerData.Author; public string Author => innerData.Author;
public string TileSet => innerData.TileSet; public string TileSet => innerData.TileSet;
public MapPlayers Players => innerData.Players; public MapPlayers Players => innerData.Players;
public int PlayerCount => innerData.PlayerCount; public int PlayerCount => innerData.PlayerCount;
public ImmutableArray<CPos> SpawnPoints => innerData.SpawnPoints; public CPos[] SpawnPoints => innerData.SpawnPoints;
public MapGridType GridType => innerData.GridType; public MapGridType GridType => innerData.GridType;
public Rectangle Bounds => innerData.Bounds; public Rectangle Bounds => innerData.Bounds;
public Png Preview => innerData.Preview; public Png Preview => innerData.Preview;
@@ -248,8 +240,6 @@ namespace OpenRA
public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo; public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo;
public DateTime ModifiedDate => innerData.ModifiedDate; public DateTime ModifiedDate => innerData.ModifiedDate;
public MapGenerationArgs GenerationArgs => innerData.GenerationArgs;
public long DownloadBytes { get; private set; } public long DownloadBytes { get; private set; }
public int DownloadPercentage { get; private set; } public int DownloadPercentage { get; private set; }
@@ -327,12 +317,12 @@ namespace OpenRA
{ {
MapFormat = 0, MapFormat = 0,
Title = "Unknown Map", Title = "Unknown Map",
Categories = ["Unknown"], Categories = new[] { "Unknown" },
Author = "Unknown Author", Author = "Unknown Author",
TileSet = "unknown", TileSet = "unknown",
Players = null, Players = null,
PlayerCount = 0, PlayerCount = 0,
SpawnPoints = [], SpawnPoints = NoSpawns,
GridType = gridType, GridType = gridType,
Bounds = Rectangle.Empty, Bounds = Rectangle.Empty,
Preview = null, Preview = null,
@@ -342,28 +332,57 @@ namespace OpenRA
}; };
} }
/// <summary> // For linting purposes only!
/// Updates internal state from a map without taking ownership of its package. public MapPreview(Map map, ModData modData)
/// 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)
{ {
UpdateFromMap(p, classification, gridType, modDataRules); this.modData = modData;
parentPackage = parent; cache = modData.MapCache;
package = null;
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> innerData = new InnerData
/// 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)
{ {
Path = p.Name; MapFormat = map.MapFormat,
package = p; 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; Dictionary<string, MiniYaml> yaml;
using (var yamlStream = p.GetStream("map.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(); yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, $"{p.Name}:map.yaml", stringPool: cache.StringPool)).ToDictionary();
} }
PackageName = p.Name;
parentPackage = parent;
var newData = innerData.Clone(); var newData = innerData.Clone();
newData.GridType = gridType;
newData.Class = classification; newData.Class = classification;
newData.GridType = gridType ?? modData.GetOrCreate<MapGrid>().Type;
if (yaml.TryGetValue("MapFormat", out var temp)) if (yaml.TryGetValue("MapFormat", out var temp))
{ {
@@ -388,7 +410,7 @@ namespace OpenRA
newData.Title = temp.Value; newData.Title = temp.Value;
if (yaml.TryGetValue("Categories", out temp)) 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)) if (yaml.TryGetValue("Tileset", out temp))
newData.TileSet = temp.Value; newData.TileSet = temp.Value;
@@ -409,7 +431,7 @@ namespace OpenRA
if (yaml.TryGetValue("MapFormat", out temp)) if (yaml.TryGetValue("MapFormat", out temp))
newData.MapFormat = FieldLoader.GetValue<int>("MapFormat", temp.Value); 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; MapStatus.Available : MapStatus.Unavailable;
try try
@@ -420,18 +442,18 @@ namespace OpenRA
var spawns = new List<CPos>(); var spawns = new List<CPos>();
foreach (var kv in actorDefinitions.Nodes.Where(d => d.Value.Value == "mpspawn")) 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); spawns.Add(s.Get<LocationInit>().Value);
} }
newData.SpawnPoints = spawns.ToImmutableArray(); newData.SpawnPoints = spawns.ToArray();
} }
else else
newData.SpawnPoints = []; newData.SpawnPoints = Array.Empty<CPos>();
} }
catch (Exception) catch (Exception)
{ {
newData.SpawnPoints = []; newData.SpawnPoints = Array.Empty<CPos>();
newData.Status = MapStatus.Unavailable; newData.Status = MapStatus.Unavailable;
} }
@@ -455,122 +477,31 @@ namespace OpenRA
using (var dataStream = p.GetStream("map.png")) using (var dataStream = p.GetStream("map.png"))
newData.Preview = new Png(dataStream); 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 // Assign the new data atomically
// Local maps have higher precedence than remote/generated maps,
// so should always replace their metadata
lock (syncRoot)
innerData = newData; innerData = newData;
} }
public void UpdateFromGenerationArgs(MapGenerationArgs args) public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, string[] mapCompatibility, Action<MapPreview> parseMetadata = null)
{
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()
{ {
var newData = innerData.Clone(); var newData = innerData.Clone();
newData.Status = status;
newData.Class = MapClassification.Remote; newData.Class = MapClassification.Remote;
newData.Status = MapStatus.Searching;
// We may have been resolved to a local/generated map by another if (status == MapStatus.DownloadAvailable)
// 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)
{ {
try try
{ {
var r = FieldLoader.Load<RemoteMapData>(yaml); 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.Title = r.title;
newData.Categories = r.categories; newData.Categories = r.categories;
newData.Author = r.author; newData.Author = r.author;
@@ -582,7 +513,7 @@ namespace OpenRA
var spawns = new CPos[r.spawnpoints.Length / 2]; var spawns = new CPos[r.spawnpoints.Length / 2];
for (var j = 0; j < r.spawnpoints.Length; j += 2) for (var j = 0; j < r.spawnpoints.Length; j += 2)
spawns[j / 2] = new CPos(r.spawnpoints[j], r.spawnpoints[j + 1]); 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; newData.GridType = r.map_grid_type;
if (cache.LoadPreviewImages) if (cache.LoadPreviewImages)
{ {
@@ -610,37 +541,29 @@ namespace OpenRA
// Map is for a different mod: update its information so it can be displayed // 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 // 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. // 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; newData.Status = MapStatus.Unavailable;
} }
catch (Exception e) catch (Exception e)
{ {
Log.Write("debug", "Failed parsing mapserver response:"); Log.Write("debug", "Failed parsing mapserver response:");
Log.Write("debug", e); Log.Write("debug", e);
newData.Status = MapStatus.Unavailable;
}
} }
// We may have been resolved to a local/generated map by another // Commit updated data before running the callbacks
// async task. Make sure we don't stomp over their state!
MapClassification mapClassification;
lock (syncRoot)
{
mapClassification = innerData.Class;
if (mapClassification == MapClassification.Remote)
innerData = newData; innerData = newData;
}
if (mapClassification == MapClassification.Remote)
{
if (innerData.Preview != null) if (innerData.Preview != null)
cache.CacheMinimap(this); cache.CacheMinimap(this);
parseMetadata?.Invoke(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) if ((Status != MapStatus.DownloadError && Status != MapStatus.DownloadAvailable) || !Game.Settings.Game.AllowDownloading)
return; return;
@@ -693,11 +616,14 @@ namespace OpenRA
mapInstallPackage.Update(mapFilename, fileStream.ToArray()); mapInstallPackage.Update(mapFilename, fileStream.ToArray());
Log.Write("debug", $"Downloaded map to '{mapFilename}'"); Log.Write("debug", $"Downloaded map to '{mapFilename}'");
var p = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles); var package = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles);
if (p == null) if (package == null)
innerData.Status = MapStatus.DownloadError; innerData.Status = MapStatus.DownloadError;
else else
UpdateFromMapWithoutOwningPackage(p, mapInstallPackage, MapClassification.User, GridType); {
UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType, null);
Game.RunAfterTick(onSuccess);
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -710,31 +636,34 @@ namespace OpenRA
public void Invalidate() public void Invalidate()
{ {
lock (syncRoot)
{
innerData.Class = MapClassification.Unknown;
innerData.Status = MapStatus.Unavailable; innerData.Status = MapStatus.Unavailable;
} }
}
public void Dispose() public void Dispose()
{ {
package?.Dispose(); DisposePackage();
}
public void DisposePackage()
{
if (package != null)
{
package.Dispose();
package = null; package = null;
} }
}
public void Delete() public void Delete()
{ {
Invalidate(); Invalidate();
(parentPackage as IReadWritePackage)?.Delete(Path); (parentPackage as IReadWritePackage)?.Delete(Package.Name);
} }
Stream IReadOnlyFileSystem.Open(string filename) Stream IReadOnlyFileSystem.Open(string filename)
{ {
// Explicit package paths never refer to a map // Explicit package paths never refer to a map
LoadPackage(); if (!filename.Contains('|') && Package.Contains(filename))
if (!filename.Contains('|') && package.Contains(filename)) return Package.GetStream(filename);
return package.GetStream(filename);
return modData.DefaultFileSystem.Open(filename); return modData.DefaultFileSystem.Open(filename);
} }
@@ -750,8 +679,7 @@ namespace OpenRA
// Explicit package paths never refer to a map // Explicit package paths never refer to a map
if (!filename.Contains('|')) if (!filename.Contains('|'))
{ {
LoadPackage(); s = Package.GetStream(filename);
s = package.GetStream(filename);
if (s != null) if (s != null)
return true; return true;
} }
@@ -762,8 +690,7 @@ namespace OpenRA
bool IReadOnlyFileSystem.Exists(string filename) bool IReadOnlyFileSystem.Exists(string filename)
{ {
// Explicit package paths never refer to a map // Explicit package paths never refer to a map
LoadPackage(); if (!filename.Contains('|') && Package.Contains(filename))
if (!filename.Contains('|') && package.Contains(filename))
return true; return true;
return modData.DefaultFileSystem.Exists(filename); return modData.DefaultFileSystem.Exists(filename);

View File

@@ -9,7 +9,7 @@
*/ */
#endregion #endregion
using System.Collections.Immutable; using System;
using OpenRA.Primitives; using OpenRA.Primitives;
namespace OpenRA namespace OpenRA
@@ -31,7 +31,7 @@ namespace OpenRA
public string Faction; public string Faction;
public bool LockColor = false; public bool LockColor = false;
public Color Color = Game.ModData.GetOrCreate<DefaultPlayer>().Color; public Color Color = Game.ModData.Manifest.Get<DefaultPlayer>().Color;
/// <summary> /// <summary>
/// Sets the "Home" location, which can be used by traits and scripts to e.g. set the initial camera /// 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 bool LockHandicap = false;
public int Handicap = 0; public int Handicap = 0;
public ImmutableArray<string> Allies = []; public string[] Allies = Array.Empty<string>();
public ImmutableArray<string> Enemies = []; public string[] Enemies = Array.Empty<string>();
public PlayerReference() { } public PlayerReference() { }
public PlayerReference(MiniYaml my) { FieldLoader.Load(this, my); } public PlayerReference(MiniYaml my) { FieldLoader.Load(this, my); }

View File

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

View File

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

View File

@@ -11,35 +11,32 @@
namespace OpenRA namespace OpenRA
{ {
public readonly struct TerrainTile(ushort type, byte index) public readonly struct TerrainTile
{ {
public readonly ushort Type = type; public readonly ushort Type;
public readonly byte Index = index; 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 int GetHashCode() { return Type.GetHashCode() ^ Index.GetHashCode(); }
public override string ToString() { return Type + "," + Index; } 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; public readonly struct ResourceTile
return false;
}
}
public readonly struct ResourceTile(byte type, byte index)
{ {
public readonly byte Type = type; public readonly byte Type;
public readonly byte Index = index; public readonly byte Index;
public ResourceTile(byte type, byte index)
{
Type = type;
Index = index;
}
public override int GetHashCode() { return Type.GetHashCode() ^ Index.GetHashCode(); } public override int GetHashCode() { return Type.GetHashCode() ^ Index.GetHashCode(); }
} }

View File

@@ -59,10 +59,16 @@ namespace OpenRA
public sealed class MiniYamlNode public sealed class MiniYamlNode
{ {
public readonly struct SourceLocation(string name, int line) public readonly struct SourceLocation
{ {
public readonly string Name = name; public readonly string Name;
public readonly int Line = line; public readonly int Line;
public SourceLocation(string name, int line)
{
Name = name;
Line = line;
}
public override string ToString() { return $"{Name}:{Line}"; } public override string ToString() { return $"{Name}:{Line}"; }
} }
@@ -93,7 +99,7 @@ namespace OpenRA
} }
public MiniYamlNode(string k, string v, string c = null) public MiniYamlNode(string k, string v, string c = null)
: this(k, new MiniYaml(v, []), c) { } : this(k, new MiniYaml(v, Enumerable.Empty<MiniYamlNode>()), c) { }
public MiniYamlNode(string k, string v, IEnumerable<MiniYamlNode> n) public MiniYamlNode(string k, string v, IEnumerable<MiniYamlNode> n)
: this(k, new MiniYaml(v, n), null) { } : this(k, new MiniYaml(v, n), null) { }
@@ -109,7 +115,7 @@ namespace OpenRA
const int SpacesPerLevel = 4; const int SpacesPerLevel = 4;
static readonly Func<string, string> StringIdentity = s => s; static readonly Func<string, string> StringIdentity = s => s;
static readonly Func<MiniYaml, MiniYaml> MiniYamlIdentity = my => my; static readonly Func<MiniYaml, MiniYaml> MiniYamlIdentity = my => my;
static readonly Dictionary<string, MiniYamlNode> ConflictScratch = []; static readonly Dictionary<string, MiniYamlNode> ConflictScratch = new();
public readonly string Value; public readonly string Value;
public readonly ImmutableArray<MiniYamlNode> Nodes; public readonly ImmutableArray<MiniYamlNode> Nodes;
@@ -190,22 +196,21 @@ namespace OpenRA
} }
public MiniYaml(string value) public MiniYaml(string value)
: this(value, []) { } : this(value, Enumerable.Empty<MiniYamlNode>()) { }
public MiniYaml(string value, IEnumerable<MiniYamlNode> nodes) public MiniYaml(string value, IEnumerable<MiniYamlNode> nodes)
{ {
Value = value; Value = value;
Nodes = nodes.ToImmutableArray(); Nodes = ImmutableArray.CreateRange(nodes);
} }
static IEnumerable<MiniYamlNode> FromLines( static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string name, bool discardCommentsAndWhitespace, HashSet<string> stringPool)
IEnumerable<ReadOnlyMemory<char>> lines, string name, bool discardCommentsAndWhitespace, HashSet<string> stringPool)
{ {
// YAML config often contains repeated strings for key, values, comments. // YAML config often contains repeated strings for key, values, comments.
// Pool these strings so we only need one copy of each unique string. // 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. // 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. // 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>> var result = new List<List<MiniYamlNode>>
{ {
@@ -336,25 +341,18 @@ namespace OpenRA
parsedLines.Add((level, keyString, valueString, commentString, location)); parsedLines.Add((level, keyString, valueString, commentString, location));
} }
foreach (var topLevelNode in result[0])
yield return topLevelNode;
result[0].Clear();
} }
if (parsedLines.Count > 0) if (parsedLines.Count > 0)
{
BuildCompletedSubNode(0); BuildCompletedSubNode(0);
foreach (var topLevelNode in result[0])
yield return topLevelNode; return result[0];
result[0].Clear();
}
void BuildCompletedSubNode(int level) void BuildCompletedSubNode(int level)
{ {
var lastLevel = parsedLines[^1].Level; var lastLevel = parsedLines[^1].Level;
while (lastLevel >= result.Count) while (lastLevel >= result.Count)
result.Add([]); result.Add(new List<MiniYamlNode>());
while (parsedLines.Count > 0 && parsedLines[^1].Level >= level) 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); 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); 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(); var sourcesList = sources.ToList();
if (sourcesList.Count == 0) if (sourcesList.Count == 0)
return []; return new List<MiniYamlNode>();
var tree = sourcesList var tree = sourcesList
.Where(s => s != null) .Where(s => s != null)
.Select(s => s as IReadOnlyCollection<MiniYamlNode> ?? s.ToList())
.Select(MergeSelfPartial) .Select(MergeSelfPartial)
.Aggregate(MergePartial) .Aggregate(MergePartial)
.Where(n => n.Key != null) .Where(n => n.Key != null)
@@ -506,9 +503,6 @@ namespace OpenRA
var ret = new List<MiniYamlNode>(existingNodes.Count); var ret = new List<MiniYamlNode>(existingNodes.Count);
foreach (var n in existingNodes) foreach (var n in existingNodes)
{ {
if (n.Key == null)
continue;
if (keys.Add(n.Key)) if (keys.Add(n.Key))
ret.Add(n); ret.Add(n);
else else
@@ -523,60 +517,17 @@ namespace OpenRA
return ret; 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) static MiniYaml MergePartial(MiniYaml existingNodes, MiniYaml overrideNodes)
{ {
var resolvedExistingNodes = WeakResolveRemovals(existingNodes?.Nodes);
var resolvedOverrideNodes = WeakResolveRemovals(overrideNodes?.Nodes);
lock (ConflictScratch) lock (ConflictScratch)
{
try
{ {
// PERF: Reuse ConflictScratch for all conflict checks to avoid allocations. // 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})"); 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})"); n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
ConflictScratch.Clear(); ConflictScratch.Clear();
} }
catch (ArgumentException ex)
{
throw new YamlException(ex.Message);
}
}
if (existingNodes == null) if (existingNodes == null)
return overrideNodes; return overrideNodes;
@@ -605,9 +556,6 @@ namespace OpenRA
void MergeNode(MiniYamlNode node) void MergeNode(MiniYamlNode node)
{ {
if (node.Key == null)
return;
// Append Removal nodes to the result. // Append Removal nodes to the result.
// Therefore: we know the remainder of the method deals with a plain node. // Therefore: we know the remainder of the method deals with a plain node.
if (node.Key.StartsWith('-')) if (node.Key.StartsWith('-'))
@@ -678,12 +626,12 @@ namespace OpenRA
{ {
if (mapRules != null && mapRules.Value != null) if (mapRules != null && mapRules.Value != null)
{ {
var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", mapRules.Value); var mapFiles = FieldLoader.GetValue<string[]>("value", mapRules.Value);
files = files.Concat(mapFiles); files = files.Append(mapFiles);
} }
var stringPool = new HashSet<string>(); // Reuse common strings in YAML 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) if (mapRules != null && mapRules.Nodes.Length > 0)
yaml = yaml.Append(mapRules.Nodes); yaml = yaml.Append(mapRules.Nodes);
@@ -748,7 +696,7 @@ namespace OpenRA
public MiniYamlBuilder(string value, List<MiniYamlNode> nodes) public MiniYamlBuilder(string value, List<MiniYamlNode> nodes)
{ {
Value = value; 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() public MiniYaml Build()
@@ -776,6 +724,7 @@ namespace OpenRA
} }
} }
[Serializable]
public class YamlException : Exception public class YamlException : Exception
{ {
public YamlException(string s) public YamlException(string s)

View File

@@ -10,22 +10,18 @@
#endregion #endregion
using System; using System;
using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using OpenRA.FileSystem; using OpenRA.FileSystem;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Video; using OpenRA.Video;
using OpenRA.Widgets; using OpenRA.Widgets;
using FS = OpenRA.FileSystem.FileSystem; using FS = OpenRA.FileSystem.FileSystem;
namespace OpenRA namespace OpenRA
{ {
public interface IGlobalModData { }
public sealed class ModData : IDisposable public sealed class ModData : IDisposable
{ {
public readonly Manifest Manifest; public readonly Manifest Manifest;
@@ -35,13 +31,14 @@ namespace OpenRA
public readonly IPackageLoader[] PackageLoaders; public readonly IPackageLoader[] PackageLoaders;
public readonly ISoundLoader[] SoundLoaders; public readonly ISoundLoader[] SoundLoaders;
public readonly ISpriteLoader[] SpriteLoaders; public readonly ISpriteLoader[] SpriteLoaders;
public readonly ITerrainLoader TerrainLoader;
public readonly ISpriteSequenceLoader SpriteSequenceLoader; public readonly ISpriteSequenceLoader SpriteSequenceLoader;
public readonly IVideoLoader[] VideoLoaders; public readonly IVideoLoader[] VideoLoaders;
public readonly HotkeyManager Hotkeys; public readonly HotkeyManager Hotkeys;
public readonly IFileSystemLoader FileSystemLoader; public readonly IFileSystemLoader FileSystemLoader;
public readonly FrozenDictionary<string, CursorSequence> Cursors;
public ILoadScreen LoadScreen { get; } public ILoadScreen LoadScreen { get; }
public CursorProvider CursorProvider { get; private set; }
public FS ModFiles; public FS ModFiles;
public IReadOnlyFileSystem DefaultFileSystem => ModFiles; public IReadOnlyFileSystem DefaultFileSystem => ModFiles;
@@ -51,11 +48,9 @@ namespace OpenRA
readonly Lazy<IReadOnlyDictionary<string, ITerrainInfo>> defaultTerrainInfo; readonly Lazy<IReadOnlyDictionary<string, ITerrainInfo>> defaultTerrainInfo;
public IReadOnlyDictionary<string, ITerrainInfo> DefaultTerrainInfo => defaultTerrainInfo.Value; public IReadOnlyDictionary<string, ITerrainInfo> DefaultTerrainInfo => defaultTerrainInfo.Value;
readonly TypeDictionary modules = [];
public ModData(Manifest mod, InstalledMods mods, bool useLoadScreen = false) public ModData(Manifest mod, InstalledMods mods, bool useLoadScreen = false)
{ {
Languages = []; Languages = Array.Empty<string>();
// Take a local copy of the manifest // Take a local copy of the manifest
Manifest = new Manifest(mod.Id, mod.Package); Manifest = new Manifest(mod.Id, mod.Package);
@@ -65,64 +60,53 @@ namespace OpenRA
FileSystemLoader = ObjectCreator.GetLoader<IFileSystemLoader>(Manifest.FileSystem.Value, "filesystem"); FileSystemLoader = ObjectCreator.GetLoader<IFileSystemLoader>(Manifest.FileSystem.Value, "filesystem");
FieldLoader.Load(FileSystemLoader, Manifest.FileSystem); FieldLoader.Load(FileSystemLoader, Manifest.FileSystem);
FileSystemLoader.Mount(Manifest, ModFiles, ObjectCreator); FileSystemLoader.Mount(ModFiles, ObjectCreator);
ModFiles.TrimExcess(); ModFiles.TrimExcess();
foreach (var kv in Manifest.GlobalModData) Manifest.LoadCustomData(ObjectCreator);
{
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.");
IGlobalModData module; FluentProvider.Initialize(this, DefaultFileSystem);
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);
if (useLoadScreen) if (useLoadScreen)
{ {
LoadScreen = ObjectCreator.CreateObject<ILoadScreen>(Manifest.LoadScreen.Value); LoadScreen = ObjectCreator.CreateObject<ILoadScreen>(Manifest.LoadScreen.Value);
LoadScreen.Init(Manifest, DefaultFileSystem); LoadScreen.Init(this, Manifest.LoadScreen.ToDictionary(my => my.Value));
LoadScreen.Display(); LoadScreen.Display();
} }
WidgetLoader = new WidgetLoader(Manifest, DefaultFileSystem); WidgetLoader = new WidgetLoader(this);
MapCache = new MapCache(Manifest, ModFiles); MapCache = new MapCache(this);
SoundLoaders = ObjectCreator.GetLoaders<ISoundLoader>(Manifest.SoundFormats, "sound"); SoundLoaders = ObjectCreator.GetLoaders<ISoundLoader>(Manifest.SoundFormats, "sound");
SpriteLoaders = ObjectCreator.GetLoaders<ISpriteLoader>(Manifest.SpriteFormats, "sprite"); SpriteLoaders = ObjectCreator.GetLoaders<ISpriteLoader>(Manifest.SpriteFormats, "sprite");
VideoLoaders = ObjectCreator.GetLoaders<IVideoLoader>(Manifest.VideoFormats, "video"); VideoLoaders = ObjectCreator.GetLoaders<IVideoLoader>(Manifest.VideoFormats, "video");
SpriteSequenceLoader = ObjectCreator.GetLoader<ISpriteSequenceLoader>(Manifest.SpriteSequenceFormat, "sequence");
Hotkeys = new HotkeyManager(ModFiles, ObjectCreator, Manifest); var terrainFormat = Manifest.Get<TerrainFormat>();
Cursors = ParseCursors(Manifest, DefaultFileSystem); 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)); defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this));
defaultTerrainInfo = Exts.Lazy(() => 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 items = new Dictionary<string, ITerrainInfo>();
var terrainLoader = (ITerrainLoader)terrainCtor.Invoke([this]);
foreach (var file in Manifest.TileSets) foreach (var file in Manifest.TileSets)
{ {
var t = terrainLoader.ParseTerrain(DefaultFileSystem, file); var t = TerrainLoader.ParseTerrain(DefaultFileSystem, file);
items.Add(t.Id, t); items.Add(t.Id, t);
} }
@@ -132,23 +116,6 @@ namespace OpenRA
initialThreadId = Environment.CurrentManagedThreadId; 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. // HACK: Only update the loading screen if we're in the main thread.
readonly int initialThreadId; readonly int initialThreadId;
internal void HandleLoadingProgress() internal void HandleLoadingProgress()
@@ -165,18 +132,26 @@ namespace OpenRA
// horribly when you use ModData in unexpected ways. // horribly when you use ModData in unexpected ways.
ChromeMetrics.Initialize(this); ChromeMetrics.Initialize(this);
ChromeProvider.Initialize(this); ChromeProvider.Initialize(this);
Ui.Initialize(this); FluentProvider.Initialize(this, fileSystem);
FluentProvider.Initialize(Manifest, fileSystem);
Game.Sound.Initialize(SoundLoaders, fileSystem); Game.Sound.Initialize(SoundLoaders, fileSystem);
CursorProvider = new CursorProvider(this);
} }
public IEnumerable<string> Languages { get; } public IEnumerable<string> Languages { get; }
public void PrepareMap(Map map) public Map PrepareMap(string uid)
{ {
LoadScreen?.Display(); 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 // Reinitialize all our assets
InitializeLoaders(map); InitializeLoaders(map);
map.Sequences.LoadSprites(); map.Sequences.LoadSprites();
@@ -185,37 +160,14 @@ namespace OpenRA
using (new Support.PerfTimer("Map.Music")) using (new Support.PerfTimer("Map.Music"))
foreach (var entry in map.Rules.Music) foreach (var entry in map.Rules.Music)
entry.Value.Load(map); entry.Value.Load(map);
return map;
} }
public MiniYamlNode[][] GetRulesYaml() public List<MiniYamlNode>[] GetRulesYaml()
{ {
var stringPool = new HashSet<string>(); // Reuse common strings in YAML 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(); return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s, stringPool: stringPool)).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);
} }
public void Dispose() public void Dispose()
@@ -225,18 +177,14 @@ namespace OpenRA
ObjectCreator?.Dispose(); ObjectCreator?.Dispose();
foreach (var module in modules) Manifest.Dispose();
{
var disposableModule = module as IDisposable;
disposableModule?.Dispose();
}
} }
} }
public interface ILoadScreen : IDisposable public interface ILoadScreen : IDisposable
{ {
/// <summary>Initializes the loadscreen with yaml data from the LoadScreen block in mod.yaml.</summary> /// <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> /// <summary>Called at arbitrary times during mod load to rerender the loadscreen.</summary>
void Display(); void Display();
@@ -245,7 +193,7 @@ namespace OpenRA
/// Called before loading the mod assets. /// Called before loading the mod assets.
/// Returns false if mod loading should be aborted (e.g. switching to another mod instead). /// Returns false if mod loading should be aborted (e.g. switching to another mod instead).
/// </summary> /// </summary>
bool BeforeLoad(ModData modData); bool BeforeLoad();
/// <summary>Called when the engine expects to connect to a server/replay or load the shellmap.</summary> /// <summary>Called when the engine expects to connect to a server/replay or load the shellmap.</summary>
void StartGame(Arguments args); void StartGame(Arguments args);
@@ -253,6 +201,6 @@ namespace OpenRA
public interface IFileSystemLoader 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 public sealed class EchoConnection : IConnection
{ {
const int LocalClientId = 1; const int LocalClientId = 1;
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sync = []; readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sync = new();
readonly Queue<(int Frame, OrderPacket Orders)> orders = []; readonly Queue<(int Frame, OrderPacket Orders)> orders = new();
readonly Queue<OrderPacket> immediateOrders = []; readonly Queue<OrderPacket> immediateOrders = new();
bool disposed; bool disposed;
int IConnection.LocalClientId => LocalClientId; int IConnection.LocalClientId => LocalClientId;
@@ -52,7 +52,7 @@ namespace OpenRA.Network
void IConnection.StartGame() void IConnection.StartGame()
{ {
// Inject an empty frame to fill the gap we are making by projecting forward orders // 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) void IConnection.Send(int frame, IEnumerable<Order> o)
@@ -100,12 +100,12 @@ namespace OpenRA.Network
{ {
public readonly ConnectionTarget Target; public readonly ConnectionTarget Target;
internal ReplayRecorder Recorder { get; private set; } internal ReplayRecorder Recorder { get; private set; }
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sentSync = []; readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> sentSync = new();
readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> queuedSyncPackets = []; readonly Queue<(int Frame, int SyncHash, ulong DefeatState)> queuedSyncPackets = new();
readonly Queue<(int Frame, OrderPacket Orders)> sentOrders = []; readonly Queue<(int Frame, OrderPacket Orders)> sentOrders = new();
readonly Queue<OrderPacket> sentImmediateOrders = []; readonly Queue<OrderPacket> sentImmediateOrders = new();
readonly ConcurrentQueue<(int FromClient, byte[] Data)> receivedPackets = []; readonly ConcurrentQueue<(int FromClient, byte[] Data)> receivedPackets = new();
TcpClient tcp; TcpClient tcp;
volatile ConnectionState connectionState = ConnectionState.Connecting; volatile ConnectionState connectionState = ConnectionState.Connecting;
volatile int clientId; volatile int clientId;

View File

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

View File

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

View File

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

View File

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

View File

@@ -269,11 +269,6 @@ namespace OpenRA
return new Order(order, null, false) { IsImmediate = isImmediate, TargetString = targetString }; 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) public static Order FromGroupedOrder(Order grouped, Actor subject)
{ {
return new Order( return new Order(

View File

@@ -78,7 +78,7 @@ namespace OpenRA.Network
public static class OrderIO 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) public static byte[] SerializeSync((int Frame, int SyncHash, ulong DefeatState) data)
{ {

View File

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

View File

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

View File

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

View File

@@ -166,9 +166,9 @@ namespace OpenRA.Network
public int Frame; public int Frame;
public int SyncedRandom; public int SyncedRandom;
public int TotalCount; public int TotalCount;
public readonly List<TraitReport> Traits = []; public readonly List<TraitReport> Traits = new();
public readonly List<EffectReport> Effects = []; public readonly List<EffectReport> Effects = new();
public readonly List<OrderManager.ClientOrder> Orders = []; public readonly List<OrderManager.ClientOrder> Orders = new();
} }
struct TraitReport struct TraitReport
@@ -202,10 +202,10 @@ namespace OpenRA.Network
{ {
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
var fields = type.GetFields(Flags) var fields = type.GetFields(Flags)
.Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<VerifySyncAttribute>()) .Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>())
.ToList(); .ToList();
var properties = type.GetProperties(Flags) var properties = type.GetProperties(Flags)
.Where(pi => pi.HasAttribute<VerifySyncAttribute>()) .Where(pi => pi.HasAttribute<SyncAttribute>())
.ToList(); .ToList();
foreach (var prop in properties) 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. // 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. // Instead, we can just return the resulting strings directly.
var getBoolString = Expression.Condition(getMember, TrueString, FalseString); 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)); 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 // 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(); } // (ISync sync) => { var foo = ((TSync)sync).Foo; return foo == null ? null : foo.ToString(); }
var memberVariable = Expression.Variable(memberType, getMember.Member.Name); var memberVariable = Expression.Variable(memberType, getMember.Member.Name);
var assignMemberVariable = Expression.Assign(memberVariable, getMember); var assignMemberVariable = Expression.Assign(memberVariable, getMember);
var member = Expression.Block([memberVariable], assignMemberVariable); var member = Expression.Block(new[] { memberVariable }, assignMemberVariable);
getString = Expression.Call(member, toString); getString = Expression.Call(member, toString);
var nullMember = Expression.Constant(null, memberType); var nullMember = Expression.Constant(null, memberType);
getString = Expression.Condition(Expression.Equal(member, nullMember), NullString, getString); 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 #endregion
using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using OpenRA.Server; using OpenRA.Server;
@@ -30,6 +29,9 @@ namespace OpenRA.Network
[FluentReference] [FluentReference]
const string GameStarted = "notification-game-has-started"; const string GameStarted = "notification-game-has-started";
[FluentReference]
const string GameSaved = "notification-game-saved";
[FluentReference("player")] [FluentReference("player")]
const string GamePaused = "notification-game-paused"; const string GamePaused = "notification-game-paused";
@@ -65,11 +67,11 @@ namespace OpenRA.Network
{ {
var message = new FluentMessage(node.Value); var message = new FluentMessage(node.Value);
if (message.Key == Joined) if (message.Key == Joined)
TextNotificationsManager.AddPlayerJoinedLine(message.Key, message.Arguments.ToArray()); TextNotificationsManager.AddPlayerJoinedLine(message.Key, message.Arguments);
else if (message.Key == Left) else if (message.Key == Left)
TextNotificationsManager.AddPlayerLeftLine(message.Key, message.Arguments.ToArray()); TextNotificationsManager.AddPlayerLeftLine(message.Key, message.Arguments);
else else
TextNotificationsManager.AddSystemLine(message.Key, message.Arguments.ToArray()); TextNotificationsManager.AddSystemLine(message.Key, message.Arguments);
} }
break; break;
@@ -181,7 +183,7 @@ namespace OpenRA.Network
if (!string.IsNullOrEmpty(order.TargetString)) 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"); var saveLastOrdersFrame = data.FirstOrDefault(n => n.Key == "SaveLastOrdersFrame");
if (saveLastOrdersFrame != null) if (saveLastOrdersFrame != null)
orderManager.GameSaveLastFrame = orderManager.GameSaveLastFrame =
@@ -201,7 +203,7 @@ namespace OpenRA.Network
case "SaveTraitData": 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); var traitIndex = Exts.ParseInt32Invariant(data.Key);
world?.AddGameSaveTraitData(traitIndex, data.Value); world?.AddGameSaveTraitData(traitIndex, data.Value);
@@ -210,8 +212,11 @@ namespace OpenRA.Network
} }
case "GameSaved": case "GameSaved":
if (!orderManager.World.IsReplay)
TextNotificationsManager.AddSystemLine(GameSaved);
foreach (var nsr in orderManager.World.WorldActor.TraitsImplementing<INotifyGameSaved>()) foreach (var nsr in orderManager.World.WorldActor.TraitsImplementing<INotifyGameSaved>())
nsr.GameSaved(orderManager.World, order.ExtraData != 0); nsr.GameSaved(orderManager.World);
break; break;
case "PauseGame": case "PauseGame":
@@ -380,21 +385,7 @@ namespace OpenRA.Network
case "SyncMapPool": case "SyncMapPool":
{ {
orderManager.ServerMapPool = FieldLoader.GetValue<FrozenSet<string>>("SyncMapPool", order.TargetString); orderManager.ServerMapPool = FieldLoader.GetValue<HashSet<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();
}
break; break;
} }

View File

@@ -22,7 +22,7 @@ namespace OpenRA
{ {
// .NET does not support unloading assemblies, so mod libraries will leak across mod changes. // .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 // 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<string, Type> typeCache;
readonly Cache<Type, ConstructorInfo> ctorCache; readonly Cache<Type, ConstructorInfo> ctorCache;
@@ -55,9 +55,26 @@ namespace OpenRA
if (!ResolvedAssemblies.TryGetValue(hash, out var assembly)) if (!ResolvedAssemblies.TryGetValue(hash, out var assembly))
{ {
#if NET5_0_OR_GREATER
var loader = new Support.AssemblyLoader(resolvedPath); var loader = new Support.AssemblyLoader(resolvedPath);
assembly = loader.LoadDefaultAssembly(); assembly = loader.LoadDefaultAssembly();
ResolvedAssemblies.Add(hash, assembly); 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); assemblyList.Add(assembly);
@@ -77,7 +94,7 @@ namespace OpenRA
public T CreateObject<T>(string className) 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) public T CreateObject<T>(string className, Dictionary<string, object> args)
@@ -119,7 +136,7 @@ namespace OpenRA
public object CreateBasic(Type type) 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) public object CreateUsingArgs(ConstructorInfo ctor, Dictionary<string, object> args)

View File

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

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