Compare commits

...

643 Commits

Author SHA1 Message Date
chacha
5a65e3c7b8 fix coding style 2024-04-24 14:17:28 +02:00
chacha
970c9c7a8f Do not keep map pakages loaded on Game start to reduce memory impact 2024-04-24 13:27:20 +02:00
Gustas
150e28a672 Fix missing map files preventing map saves 2024-04-11 23:02:48 +02:00
Gustas
cf21c8e906 Fix support power name not really being optional 2024-04-10 23:52:43 +02:00
RoosterDragon
7859b913bc Trim memory usage of Png.
For indexed PNGs, we only need to allocate a palette large enough to accommodate the number of indexed colours in the image. For images that don't use all 256 colours, this slightly reduces the memory usage for these images.
2024-04-06 10:47:19 +03:00
RoosterDragon
2481bddf58 Trim memory usage of FileSystem.
When LoadFromManifest is called, trim the various backing collections. These backing collections tend to live a long time without further modifications.
2024-04-06 10:47:19 +03:00
RoosterDragon
c547f3f26d Trim memory usage of SpriteCache.
As the SpriteCache is used as a one-shot operation in practise, holding on to the capacity of backing collections is not required. Memory usage can be reduced by allowing the capacity to be reset after the SpriteCache has resolved items.

- Once LoadReservations is called, reset the reservation dictionaries so their backing collections can be reclaimed.
- When ResolveSprites is called, shrink the resolved dictionary as resolutions take place.
2024-04-06 10:47:19 +03:00
RoosterDragon
a4bb58007f Trim memory usage of IReadOnlyPackage implementations.
These implementations are often backed by a Dictionary, and tend to live a long time after being loaded. Ensure TrimExcess is called on the backing dictionaries to reduce the long term memory usage. In some cases, we can also preallocate the dictionary size for efficiency.
2024-04-06 10:47:19 +03:00
michaeldgg2
ed5c7bb836 Minelayer: remove unnecessary requirement Rearmable 2024-04-03 15:05:29 +03:00
Matthias Mailänder
af81dca3ff Update GitHub Actions 2024-04-03 15:03:51 +03:00
Matthias Mailänder
188f0e2451 Extract strings from support power name and description. 2024-04-03 11:38:08 +03:00
Gustas
0c43801a2c Remove hack fix 2024-03-30 15:51:13 +01:00
JovialFeline
8f985118cd Fix RA+CnC map import of BARB/FENC 2024-03-29 11:32:29 +02:00
JovialFeline
ec09e618ff Correct landing craft passenger subcells 2024-03-29 11:07:46 +02:00
RoosterDragon
799c4c9e3c Fix map editor not removing an actor properly.
If you edit an actor name, then delete the actor - it fails to be removed from the map in the editor. This is because the actor previews are keyed by ID. Editing their name edits their ID and breaks the stability of their hash code. This unstable hash code means the preview will now fail to be removed from collections, even though it's the "same" object.

Fix this by making the ID immutable to ensure hash stability - this means that a preview can be added and removed from collections successfully. Now when we edit the ID in the UI, we can't update the ID in place on the preview. Instead we must generate a new preview with the correct ID and swap it with the preview currently in use.
2024-03-28 12:11:26 +02:00
David Wilson
25a6b4b6b9 Editor marker tiles layer 2024-03-21 13:11:04 +02:00
Paul Chote
714f2c6dc2 Add TUC Steam metadata for TS. 2024-03-16 20:53:55 +02:00
Paul Chote
e04439ea14 Add TUC Steam metadata for RA. 2024-03-16 20:53:55 +02:00
Paul Chote
24093fd0c5 Add TUC Steam metadata for TD. 2024-03-16 20:53:55 +02:00
RoosterDragon
c18d10c846 Fix ActorIndex when dealing with multiple trait instances.
The intended check was "has any trait", but TraitOrDefault throws if there is more than one. Adjust this check so it doesn't throw in the face of multiple trait instances.

Resolves a regression introduced in 63de527d9e.
2024-03-16 11:21:23 +02:00
RoosterDragon
4fca85f63d Improve sheet packing in Dune 2000.
In a3d0a50f4d, SpriteCache is updated to sort sprites by height before adding them onto the sheet. This improves packing by reducing wasted space as the sprites are packed onto the sheet. D2kSpriteSequence does not fully benefit from this change, as it creates additional sprites afterwards in the ResolveSprites method. These are not sorted, so they often waste space due to height changes between adjacent sprites and cause an inefficient packing. Sorting them in place is insufficient, as each sequence performs the operation independently. So sets of sprites across different sequences end up with poor packing overall. We need all the sprites to be collected together and sorted in one place for best effect.

We restructure SpriteCache to allow a frame mutation function to be provided when reserving sprites. This removes the need for the ReserveFrames and ResolveFrames methods in SpriteCache. D2kSpriteSequence can use this new function to pass in the required modification, and no longer has to add frames to the sheet builder itself. Now the SpriteCache can apply the desired frame mutations, it can batch together these mutated frames with the other frames and sort them all as a single batch. With all frames sorted together the maximum benefit of this packing approach is realised.

This reduces the number of BGRA sheets required for the d2k mod from 3 to 2.
2024-03-12 22:44:45 +02:00
RoosterDragon
dc0f26a1cd Improve BotModule performance.
Several parts of bot module logic, often through the AIUtils helper class, will query or count over all actors in the world. This is not a fast operation and the AI tends to repeat it often.

Introduce some ActorIndex classes that can maintain an index of actors in the world that match a query based on a mix of actor name, owner or trait. These indexes introduce some overhead to maintain, but allow the queries or counts that bot modules needs to perform to be greatly sped up, as the index means there is a much smaller starting set of actors to consider. This is beneficial to the bot logic as the TraitDictionary index maintained by the world works only in terms of traits and doesn't allow the bot logic to perform a sufficiently selective lookup. This is because the bot logic is usually defined in terms of actor names rather than traits.
2024-03-12 16:14:29 +02:00
N.N
d4457a4028 add join/leave/changeOption sounds into lobby 2024-03-12 13:18:50 +02:00
N.N
6c032bb8f7 Add unique beacon sound 2024-03-12 13:18:50 +02:00
RoosterDragon
a3d0a50f4d Improve sheet packing.
When sheet builders are adding sprites to a sheet, they work left to right along each row. They reserve height for the highest sprite seen along that row, resetting the height reservation when the row runs out of space and it moves down to the next row.

As the SpriteCache adds the sprites in a giant batch, it can optimise this operation by ordering the sprites by their height. This reduces wastage where shorter sprites don't use the the full height reserved within the row. The reduced wastage can help the sheet builder allocate fewer sheets, improving load times and improving GPU memory usage as less texture memory is required.
2024-03-11 08:47:56 +02:00
RoosterDragon
519db10f61 Improve performance of R8Loader.
The repeated small stream reads of ReadUInt16 generate a lot of overhead. Instead, consume the data in a single ReadBytes call and then unpack within the same buffer.
2024-03-09 21:50:18 +02:00
RoosterDragon
00a23e6c11 Fetch actors directly in DropPodsPower.
Use direct dictionary lookups, rather than iterating the entire actors dictionary.
2024-03-09 21:33:42 +02:00
RoosterDragon
6e89bef657 Speed up Util.FastCopyIntoChannel.
The assets for the Tiberian Dawn HD mod are much larger than assets for the default mods, causing a lot of load time to be spent in Util.FastCopyIntoChannel.

We can provide a special case for the SpriteFrameType.Bgra32 format, which is the same format as the destination buffer. In this scenario we can just perform memory copies between the source and destination. Additionally, whilst the default mods require all their assets to get their alpha premultiplied, many of the Tiberian Dawn assets are already premultiplied. Being able to skip this step for these assets saves us having to interpret the bytes into colors and back again.

For the default mods, there isn't a noticeable timing difference. For Tiberian Dawn HD or other mods with modern assets sizes, a large speedup is achieved.
2024-03-09 21:26:03 +02:00
RoosterDragon
5f97e2de5a Make Color use uint for ARGB.
This is a more natural representation than int that allows removal of casts in many places that require uint. Additionally, we can change the internal representation from long to uint, making the Color struct smaller. Since arrays of colors are common, this can save on memory.
2024-03-09 21:10:02 +02:00
Wojciech Walaszek
7b82d85b27 Editor actor move 2024-03-03 14:27:35 +02:00
JovialFeline
ac610c54eb Add bridge break, fixes to Soviet-06b 2024-03-02 17:02:10 -06:00
michaeldgg2
63247d2d11 ParallelProductionQueue: pause production, when all Production traits are paused 2024-02-25 11:52:25 +01:00
Gustas
d3c44de5d2 Fix force rally point not setting building as primary 2024-02-23 19:10:35 +01:00
JovialFeline
ade07607a5 Add crash fix, minor polish to volkov-n-chitzkoi 2024-02-22 17:00:25 +01:00
michaeldgg2
3760b14235 Land activity: fix bug which causes crash in Aircraft.AddInflunce()
Fixes #21302
2024-02-19 10:56:31 +02:00
atlimit8
a054d2115d remove unused RenderSprite trait fields 2024-02-16 09:36:44 +02:00
michaeldgg2
9d29303142 Hovers: remove dependency on IMove trait 2024-02-13 11:30:35 -06:00
JovialFeline
12e1d327ef Restore allies-05 prison self-targeting 2024-02-11 18:40:52 +01:00
JovialFeline
09834d3954 Add Pillbox, early dog attack to allies-02 2024-02-11 18:29:30 +02:00
atlimit8
8fda46e241 Prevent reading not yet cached Actor.Crushable() in Crate ctor using HierarchicalPathFinder.ActorIsBlocking(Actor actor).
Only occurs if the crate might be blocked.
Test Mod: td
Test Map: Island Duel
Line:
			foreach (var crushable in actor.Crushables)

Stack trace:
OpenRA.Mods.Common.dll!OpenRA.Mods.Common.Pathfinder.HierarchicalPathFinder.ActorIsBlocking(OpenRA.Actor actor) Line 660 (OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs:660)
OpenRA.Mods.Common.dll!OpenRA.Mods.Common.Pathfinder.HierarchicalPathFinder.RequireBlockingRefreshInCell(OpenRA.CPos cell) Line 607 (OpenRA.Mods.Common/Pathfinder/HierarchicalPathFinder.cs:607)
OpenRA.Mods.Common.dll!OpenRA.Mods.Common.Traits.ActorMap.AddInfluence(OpenRA.Actor self, OpenRA.Traits.IOccupySpace ios) Line 428 (OpenRA.Mods.Common/Traits/World/ActorMap.cs:428)
OpenRA.Mods.Common.dll!OpenRA.Mods.Common.Traits.Crate.SetLocation(OpenRA.Actor self, OpenRA.CPos cell) Line 224 (OpenRA.Mods.Common/Traits/Crates/Crate.cs:224)
OpenRA.Mods.Common.dll!OpenRA.Mods.Common.Traits.Crate.SetPosition(OpenRA.Actor self, OpenRA.CPos cell, OpenRA.Traits.SubCell subCell) Line 203 (OpenRA.Mods.Common/Traits/Crates/Crate.cs:203)
OpenRA.Mods.Common.dll!OpenRA.Mods.Common.Traits.Crate.Crate(OpenRA.ActorInitializer init, OpenRA.Mods.Common.Traits.CrateInfo info) Line 94 (OpenRA.Mods.Common/Traits/Crates/Crate.cs:94)
OpenRA.Mods.Common.dll!OpenRA.Mods.Common.Traits.CrateInfo.Create(OpenRA.ActorInitializer init) Line 33 (OpenRA.Mods.Common/Traits/Crates/Crate.cs:33)
OpenRA.Game.dll!OpenRA.Actor.Actor(OpenRA.World world, string name, OpenRA.Primitives.TypeDictionary initDict) Line 163 (OpenRA.Game/Actor.cs:163)
OpenRA.Game.dll!OpenRA.World.CreateActor(bool addToWorld, string name, OpenRA.Primitives.TypeDictionary initDict) Line 339 (OpenRA.Game/World.cs:339)
OpenRA.Game.dll!OpenRA.World.CreateActor(string name, OpenRA.Primitives.TypeDictionary initDict) Line 329 (OpenRA.Game/World.cs:329)
OpenRA.Mods.Common.dll!OpenRA.Mods.Common.Traits.CrateSpawner.SpawnCrate.AnonymousMethod__0(OpenRA.World w) Line 168 (OpenRA.Mods.Common/Traits/World/CrateSpawner.cs:168)
OpenRA.Game.dll!OpenRA.World.Tick() Line 464 (OpenRA.Game/World.cs:464)
OpenRA.Game.dll!OpenRA.Game.InnerLogicTick(OpenRA.Network.OrderManager orderManager) Line 634 (OpenRA.Game/Game.cs:634)
OpenRA.Game.dll!OpenRA.Game.LogicTick() Line 658 (OpenRA.Game/Game.cs:658)
OpenRA.Game.dll!OpenRA.Game.Loop() Line 830 (OpenRA.Game/Game.cs:830)
OpenRA.Game.dll!OpenRA.Game.Run() Line 883 (OpenRA.Game/Game.cs:883)
OpenRA.Game.dll!OpenRA.Game.InitializeAndRun(string[] args) Line 313 (OpenRA.Game/Game.cs:313)
OpenRA.dll!OpenRA.Launcher.Program.Main(string[] args) Line 26 (OpenRA.Launcher/Program.cs:26)
[External Code] (Unknown Source:0)
2024-02-09 16:30:05 +02:00
atlimit8
8993901641 Add null check to Actor.Crushables 2024-02-09 16:30:05 +02:00
Gustas
2fe13fe442 Manually review chrome translation keys and do some deduplication 2024-02-07 19:20:11 +01:00
Gustas
1a4f366e4b Make notifyAttacks more consistent 2024-02-07 15:30:41 +01:00
Gustas
2d332d0a13 Fix pillbox not uncloaking upon firing 2024-02-07 15:30:41 +01:00
David Wilson
d630a6ef7d Fix editor area/actor deselection bugs 2024-02-07 15:30:23 +02:00
RoosterDragon
0c22499534 Fix NREs in DiscordService.
Handle the client being null. Previously, a service could be created with a null client. This would leads to NREs when invoking the static Update methods. Now we guard against a null client.
2024-02-07 15:18:55 +02:00
LipkeGu
4077f28285 Renderer: Dispose worldBuffer only when it was initialized. 2024-02-06 16:55:05 +02:00
N.N
4e031a6ea5 Selection info into Area selection tab
Selection info into Area selection tab

add Resource counter and measure info into Area selection tab
2024-02-03 12:26:21 +02:00
LipkeGu
311d55ff45 Add [FieldLoader.Require] to TooltipInfoBase.Name 2024-02-03 12:17:58 +02:00
Vapre
64cdfcbeab Cache ICrushable traits in actor. 2024-01-31 13:29:58 +02:00
Gustas
6026d088c8 Use HashSets instead of .Distinct
And don't cast to array / list where unnecessary
2024-01-30 22:06:58 -06:00
JovialFeline
53e4d0dd87 Add Turtle condition to RA bots' mine laying 2024-01-29 14:39:21 +01:00
David Wilson
2ced4abc24 Editor selection refactor pt1 2024-01-24 10:11:39 +02:00
RoosterDragon
b58c1ea5bc Provide names and pools when creating MiniYaml.
- Rename the filename parameter to name and make it mandatory. Review all callers and ensure a useful string is provided as input, to ensure sufficient context is included for logging and debugging. This can be a filename, url, or any arbitrary text so include whatever context seems reasonable.
- When several MiniYamls are created that have similar content, provide a shared string pool. This allows strings that are common between all the yaml to be shared, reducing long term memory usage. We also change the pool from a dictionary to a set. Originally a Dictionary had to be used so we could call TryGetValue to get a reference to the pooled string. Now that more recent versions of dotnet provide a TryGetValue on HashSet, we can use a set directly without the memory wasted by having to store both keys and values in a dictionary.
2024-01-21 12:39:10 +02:00
RoosterDragon
ca6aa5ebf1 Adjust widget sizes to ensure they accommodate the English translation text.
Some existing widget are too small to accommodate their text. Adjust their sizes to fit. Text can be rendered outside the widget bounds so visually this often has no impact, but adjusting this now will help in the future for checking translation text for other languages fit in their widgets.
2024-01-21 12:34:28 +02:00
Thomas Christlieb
f979e6da0f Don't allow to unspy a spy by clicking on itself 2024-01-20 00:44:46 +01:00
dnqbob
32121a38f4 Fix Hovers desync caused by changing 'WorldVisualOffset' in renderer. 2024-01-15 15:21:45 +02:00
RoosterDragon
2fde98a0d1 Fix uses of LabelWidget.Text and ButtonWidget.Text to use GetText instead.
The Text element of these widgets was changed from display text to a translation key as part of adding translation support. Functions interested in the display text need to invoke GetText instead. Lots of functions have not been updated, resulting in symptoms such as measuring the font size of the translation key rather than the display text and resizing a widget to the wrong size.

Update all callers to use GetText when getting or setting display text. This ensure their existing functionality that was intended to work in terms of the display text and not the translation key works as expected.
2024-01-15 15:16:58 +02:00
JovialFeline
ead78bc3a3 Add IsDead/aircraft checks to Soviet 11 2024-01-12 19:26:08 +01:00
Wojciech Walaszek
00857df990 adds tilting on slopes to suitable actor previews 2024-01-09 14:56:30 +02:00
Gustas
1a037c06bf Fix smudges incorrectly generating on slopes 2024-01-08 18:22:41 +01:00
Bujacikk
0741439dd6 Task 20918. Improving Png.Save and tests
Comments removing

update3
2024-01-07 11:46:11 +02:00
N.N
0e5447d6d2 Replace 8-bit custom tiles with 16bit equivalents 2024-01-06 13:26:12 +02:00
Wojciech Walaszek
680144b24f adds Hovers WorldVisualOffset to muzzle calculations 2024-01-06 13:06:08 +02:00
michaeldgg2
9a1823d805 Make UnitOrderGenerator more extensible by giving inherited classes access to some methods 2024-01-06 12:39:29 +02:00
reaperrr
e96865b55e Add scorch flames to RA and TD 2024-01-06 12:31:17 +02:00
reaperrr
8ba144f43a Randomize smudge smoke offsets in RA, TD and TS 2024-01-06 12:31:17 +02:00
reaperrr
1f10dafbea Add MaxSmokeOffsetDistance to SmudgeLayer 2024-01-06 12:31:17 +02:00
Wojciech Walaszek
da638a495c implements flashing on healing units 2023-12-31 14:06:40 +02:00
N.N
d83a871520 Add RemoveOrders into RejectOrders trait 2023-12-27 17:28:10 +02:00
N.N
a0b1bdd154 fix rebase, fix muzzle offset 2023-12-20 13:45:47 +02:00
Paul Chote
34ff23d030 Use higher colour depth sprites in D2k. 2023-12-20 12:38:00 +02:00
N.N
f7f304a2e0 Adjust AI rules 2023-12-16 19:33:57 +01:00
Wojciech Walaszek
32ad81d0ff fixes gapowr plugs offsets 2023-12-15 18:26:38 +02:00
dnqbob
ea3a62927d Add translation lines for TooltipDescription 2023-12-15 18:20:00 +02:00
dnqbob
ba951b6470 Add Translation to TooltipDescription 2023-12-15 18:20:00 +02:00
Paul Chote
6c56ea4c55 Introduce Renderer.WorldBufferSnapshot(). 2023-12-15 13:37:05 +02:00
Paul Chote
6a86a99fce Dispose Renderer frame buffers. 2023-12-15 13:37:05 +02:00
RoosterDragon
f270cb3bde Fix handling of empty indented MiniYAML comments.
An empty MiniYaml comment that was indented was previously not recognized, and instead parsed as a key named '#'. Now, indented comments are recognized as comments, which matches the behaviour for unindented lines.
2023-12-15 13:27:03 +02:00
N.N
aa5b193746 Exlude DamageTypes from HarvesterNotifier 2023-12-15 13:04:36 +02:00
JovialFeline
adf515d50b Fix D2k objectives, alerts in Ha2, Or4, At5 2023-12-15 12:36:05 +02:00
abcdefg30
da507b2eed Add a lint check to ensure no actor names are conflicting with script names
Only scripted maps will have the need to use named actors, so we can
assume that there will be a Lua script used in maps with such actors.
2023-12-15 11:58:42 +02:00
abcdefg30
adf6a81862 Rename the Radar Dome in Soviets08a to avoid a crash 2023-12-15 11:58:42 +02:00
dnqbob
264564d006 Allow WeatherOverlay fade in/out when enabled/disabled 2023-12-15 11:48:54 +02:00
penev92
02d31a2f2c Fix documentation workflow always trying to commit
Don't try to commit and push if there is nothing to commit, because git will exit with 1, failing the workflow.
A continuation of ca6b87d05e.
2023-12-13 23:13:24 +02:00
JovialFeline
59473bdf9f Change bombers, remove Hard from Evacuation 2023-12-12 22:37:08 +01:00
Gustas
dd7441e0b4 Automate update rule. 2023-12-04 10:10:28 +02:00
Paul Chote
ad833a6fbb Add support for additional cloak styles and use native alpha in RA,D2k,TS. 2023-12-04 10:10:28 +02:00
Paul Chote
9f196f2693 Fix Cloak.UncloakSound not being used. 2023-12-04 10:10:28 +02:00
Gustas
ac6934405e Reinforce d2k carryalls on shellmap instead of spawning them on the ground 2023-12-03 19:27:02 +00:00
Gustas
d8100cb9f2 Simplify harvester's creation activity 2023-12-03 19:27:02 +00:00
Gustas
2733ed4b1c Fix war factory not opening its door properly 2023-12-03 19:27:02 +00:00
Gustas
018777472a Fix harvesters teleporting when produced
And allow to interrupt actor creation child activities
2023-12-03 19:27:02 +00:00
Gustas
20f6e01afe Fix crashing when transports are loaded via lua 2023-12-03 19:27:02 +00:00
Oliver Brakmann
3904576574 Draw border around capture area in ProximityCapturable 2023-12-03 17:14:47 +00:00
Oliver Brakmann
c4acd8b361 Add ability to draw a border around a set of adjacent cells. 2023-12-03 17:14:47 +00:00
Oliver Brakmann
8529512edb Add CellTrigger support to ProximityCapturable 2023-12-03 17:14:47 +00:00
Oliver Brakmann
c20cffad5c Add support for CPos[] fields to FieldLoader 2023-12-03 17:14:47 +00:00
Paul Chote
07ed6a889e Move ColorShift traits into the main repo. 2023-12-02 21:44:58 +02:00
Gustas
d67e0a4eef Allow harvester definitions to exist on non-mobile actors 2023-12-02 13:50:46 +01:00
Gustas
8e7fa26709 Add TransformsIntoDockClient 2023-12-02 13:50:46 +01:00
dnqbob
deacc7ad65 Fix InitialActor in Carryall not initialized correctly 2023-12-02 13:56:36 +02:00
Matthias Mailänder
65361ed8dc Add the Nod mobile stealth generator. 2023-12-02 13:30:11 +02:00
N.N
bb1e830264 Add initial delay for ActorSpawnManager 2023-12-02 11:36:42 +02:00
Pavel Penev
ca6b87d05e Fix documentation workflow always trying to commit
Don't try to commit and push if there is nothing to commit, because git will exit with 1, failing the workflow.
2023-11-29 17:59:12 +01:00
abcdefg30
855568cab7 Fix a compiler warning in MapCommand.cs 2023-11-27 18:39:51 +02:00
RoosterDragon
6b0db6699d Merge RefreshMap and UnpackMap commands. Add regex filename filter.
This provides a single utility command for interacting with maps, that takes an arg for the map operation. The filename filter allows all maps in the mod to be operated on by default, or a regex can be passed to limit the operation to certain maps.
2023-11-25 16:45:05 +01:00
RoosterDragon
ab9b393238 Compress all pngs, including within oramap files.
Reduces size used for png files from 13,366,660 bytes to 13,055,285 bytes in total. Changes size used for oramap files from 2,601,027 bytes to 2,605,779 bytes in total (contained PNGs are smaller, but the oramap zip wrapper didn't compress as well). This slight filesize improvement doesn't noticeably impact loading times.

zopfilpng is used for compression with the following command line:
'zopflipng.exe -y -m image.png image.png'

This follows on from 78bef8a98f and bc5e7d1497. Except now that the PNG decoder supports bit depths of 1, 2 or 4 we don't have to preserve the original bit depth of the image, allowing for more compression.

The oramap files were updated by:
- Running utility command "<mod> --unpack-map unpack" for each mod.
- Compressing the png files using the command above.
- Running utility command "<mod> --unpack-map repack" for each mod, except in Map.Save the line `if (!LockPreview) { var previewData = ...` is replaced with `if (false) { var previewData = ...` to save the existing optimized image on disk rather than generating a fresh preview.
2023-11-25 16:45:05 +01:00
RoosterDragon
61b124ddf5 Add UnpackMapCommand
This command allows either unpacking oramap files into folders, or packing folders into oramap files.

Example invocations:
"d2k --unpack-map unpack" to unpack maps of the d2k mod into folders.
"cnc --unpack-map repack" to repack maps of the cnc mod into oramap files (but will only pack folders that were unpacked previously).
2023-11-25 16:45:05 +01:00
RoosterDragon
678b238c1c Teach PNG decoder to handle indexed bit depths of 1, 2 or 4.
The PNG decoder, when dealing when indexed images with a palette, could only decode a bit depth of 8. Teach it to decode depths of 1, 2 and 4 as well. As the palette data is exposed to consumers of the PNG class, unpack the data into a 8 bit depth so consumers don't need to also handle the new bit depths.
2023-11-25 16:45:05 +01:00
N.N
304fc458eb fix Devastator AoE 2023-11-25 16:30:41 +01:00
Gustas
caad8ba44b Manual cleanup 2023-11-25 16:28:19 +01:00
Gustas
db8a28f2c0 Automated extraction 2023-11-25 16:28:19 +01:00
Gustas
0f5b78442b Extract unit names and descriptions 2023-11-25 16:28:19 +01:00
Gustas
a5e472dfe6 Add a utility command than extracts rule translations 2023-11-25 16:28:19 +01:00
Gustas
4b7036be0f Match better newline format 2023-11-25 16:28:19 +01:00
Gustas
6386e96134 Move chrome extraction utility to common and reuse code 2023-11-25 16:28:19 +01:00
Gustas
b267374d20 It doesn't make sense to put dots after file paths 2023-11-25 16:28:19 +01:00
Gustas
342fc5b0e9 Fix trait linting not providing trait and actor names 2023-11-25 16:28:19 +01:00
Pavel Penev
ff49411bc1 Updated wiki job to use prepare job output 2023-11-21 17:20:22 +02:00
Pavel Penev
c80020f451 Update documentation job to use prepare job output 2023-11-21 17:20:22 +02:00
Pavel Penev
54c2f7d2b4 Added a prepare job to documentation GH workflow 2023-11-21 17:20:22 +02:00
penev92
59ad9e3cd7 Automated documentation.yml running on bleed merge 2023-11-20 16:07:16 +02:00
penev92
ead6bdecb6 Automated documentation.yml running on git tag
For release and playtest tags.
2023-11-20 16:07:16 +02:00
penev92
104801cdca Parameterized duplicate code in documentation.yml 2023-11-20 16:07:16 +02:00
RoosterDragon
e6914f707a Introduce FirstOrDefault extensions method for Array.Find and List.Find.
This allows the LINQ spelling to be used, but benefits from the performance improvement of the specific methods for these classes that provide the same result.
2023-11-19 19:28:57 +02:00
RoosterDragon
acca837142 Fix RCS1246 2023-11-19 19:28:57 +02:00
RoosterDragon
330ca92045 Fix RCS1077 2023-11-19 19:28:57 +02:00
RoosterDragon
499efa1d0a Change CA2211 from suggestion (analyser default level) to silent.
The codebase has a lot of violations of this rule, reduce the amount of noise by reducing the severity.
2023-11-17 12:03:00 +02:00
Paul Chote
89e1d71aec Validate lobby option values. 2023-11-17 10:28:52 +02:00
Paul Chote
2faae285db Persist skirmish settings between sessions. 2023-11-17 10:28:52 +02:00
Paul Chote
bdef619803 Move skirmish bot creation to the server. 2023-11-17 10:28:52 +02:00
Paul Chote
3f4f9e7354 Introduce ServerType.Skirmish. 2023-11-17 10:28:52 +02:00
Paul Chote
3b67e425ed Add FilenamePattern support to sequences. 2023-11-16 15:06:10 +02:00
RoosterDragon
c8efc5fdd7 Fix CA1854 2023-11-16 09:29:17 +02:00
RoosterDragon
c2568ebd1f Fix CA1851 2023-11-16 09:29:17 +02:00
RoosterDragon
2996a1ddde Fix CA1868 2023-11-16 09:29:17 +02:00
RoosterDragon
2ea2106eca Fix CA1865 2023-11-16 09:29:17 +02:00
RoosterDragon
9f526610dd Fix CA1864 2023-11-16 09:29:17 +02:00
RoosterDragon
3259737774 Add new .NET 8 rules to editorconfig.
Don't enforce the rules yet, since we are still targeting .NET 6.
2023-11-16 09:29:17 +02:00
RoosterDragon
360f24f609 Fix IDE0055
This rule no longer appears to be buggy, so enforce it. Some of the automated fixes are adjusted in order to improve the result. #pragma directives have no option to control indentation, so remove them where possible.
2023-11-16 08:45:10 +02:00
Paul Chote
60cbf79c9b Add to ReplacePaletteModifiers upgrade rule. 2023-11-15 20:52:03 +02:00
Paul Chote
d98017c140 Fix trike icon. 2023-11-15 20:52:03 +02:00
Paul Chote
c0ae7ea497 Remove PaletteFromScaledPalette. 2023-11-15 20:52:03 +02:00
Paul Chote
ac53b89421 Remove D2kFogPalette. 2023-11-15 20:52:03 +02:00
Paul Chote
46ba8ef5dd Remove effect*alpha palettes. 2023-11-15 20:52:03 +02:00
Paul Chote
cc0f116194 Remove custom deviator gas palette. 2023-11-15 20:52:03 +02:00
Paul Chote
8dc255f401 Fix sand animations. 2023-11-15 20:52:03 +02:00
Paul Chote
dd4bbc3546 Fix move flash. 2023-11-15 20:52:03 +02:00
Paul Chote
db0aabcb88 Fix starport and repair pad lights. 2023-11-15 20:52:03 +02:00
Svetlin Georfiev
a086fdaa5b A simplification done according to de Morgan's laws. 2023-11-15 19:41:45 +02:00
Svetlin Georfiev
ee6f8ae45d Improvement of cyclomatic complexity by fewer nestings. 2023-11-15 19:41:45 +02:00
Jakub Vesely
91802e6f10 ImportGen2Map: Fix imports of malformed maps.
Fixes #21126
2023-11-15 19:20:45 +02:00
RoosterDragon
d1797a021f Disable CA2241 try_determine_additional_string_formatting_methods_automatically
This is creating some false warnings, so disable for now.
2023-11-15 19:13:17 +02:00
RoosterDragon
3ae617c55b Fix CA2208 2023-11-15 19:13:17 +02:00
RoosterDragon
f6614c1c58 Fix CA1860 2023-11-15 19:13:17 +02:00
RoosterDragon
889de5e08a Fix CA1822 2023-11-15 19:13:17 +02:00
RoosterDragon
b97d1a4c6c Fix IDE0090 2023-11-15 19:13:17 +02:00
RoosterDragon
cfde0d7867 Fix IDE0001 2023-11-15 19:13:17 +02:00
RoosterDragon
399cef8fb2 Reset FPS counter on game start.
This avoids this displayed counter being dragged down by lower FPS during loading prior to the game starting.
2023-11-15 19:04:35 +02:00
RoosterDragon
58e447d8d0 Change FPS counter behaviour.
Calculate a rolling average of FPS over the last second. This allows the FPS counter to be updated every frame - and in particular means it can display a rough figure immediately rather than needing to wait one second to collect information at the start of a game.
2023-11-15 19:04:35 +02:00
RoosterDragon
43f339b91e Fix FPS counter showing initial high figure.
When the widget is created, use the current frame as reference rather than always using zero. That avoids the first FPS reading from a new widget calculating as if all frames rendered since the game started occurred in the first second.
2023-11-15 19:04:35 +02:00
Gustas
9534443771 Add the ability for technician and rocket soldier to fire from a pillbox 2023-11-15 14:09:32 +02:00
Gustas
39755a2fce Bump update rules to release-20231010 2023-11-15 07:38:51 +02:00
Paul Chote
73be3641ea Make Rectangle a readonly struct. 2023-11-14 20:33:36 +02:00
Paul Chote
03b413a892 Replace Rectangle widget bounds with a new WidgetBounds struct. 2023-11-14 20:33:36 +02:00
RoosterDragon
31c37662cf Play game started audio notifications just as the game starts.
Previously the StartGameNotification and MusicPlaylist traits used the IWorldLoaded interface to play an audio notification and begin music when the game started. However this interface is used by many traits to perform initial loading whilst the load screen was visible, and this loading can take time. Since the traits could run in any order, then audio notification might fire before another trait with a long loading time. This is not ideal as we want the time between the audio notification occurring and the player being able to interact to be as short and reliable as possible.

Now, we introduce a new IPostWorldLoaded which runs after all other loading activity, and we switch StartGameNotification and MusicPlaylist to use it. This allows timing sensitive traits that want to run right at the end of loading to fire reliably and with minimal delay. The player perception of hearing the notification and being able to interact is now much snappier.
2023-11-12 20:18:41 +02:00
RoosterDragon
57a452a705 Ensure PerfHistory is reset when starting a new game.
Ensure stale perf history data, to ensure the data is useful and the perf graph widget displays useful information.
- Remove stale data from the previous game when starting a new game. This avoids the graph showing values from the previous game when a new game starts.
- Remove data that was collected during loading. This avoids displaying data points that were collected whilst the loading screen was visible. Data collected whilst loading is not relevant to the in-game performance graph.

The performance graph when starting a new game will now display accurate information from the first tick of the game, whereas previously it displayed some stale information as well.
2023-11-12 20:18:41 +02:00
Paul Chote
9d174cd87d Add a button to reset lobby options to default. 2023-11-12 12:04:05 +02:00
RoosterDragon
9a3c39878d Fix RCS1236 2023-11-10 10:38:41 +02:00
RoosterDragon
498c6e3d8b Fix RCS1205 2023-11-10 10:38:41 +02:00
RoosterDragon
25cb3728ca Fix RCS1170 2023-11-10 10:38:41 +02:00
RoosterDragon
fbe147ce61 Fix RCS1118 2023-11-10 10:38:41 +02:00
RoosterDragon
eb287d9b8d Fix RCS1089 2023-11-10 10:38:41 +02:00
RoosterDragon
4dd787be13 Fix RCS1061 2023-11-10 10:38:41 +02:00
RoosterDragon
5d91b678bb Use spans to improve performance in StreamExts.
Also avoid ReadBytes calls that allocate a buffer by either updating the stream position (if not interested in the bytes), by reusing an input buffer (if interested in the bytes), or using a stackalloc buffer to avoid the allocation (for small reads).
2023-11-10 10:25:39 +02:00
Paul Chote
b3ee3551ca Prevent incompatible maps from being displayed in the map chooser. 2023-11-05 15:42:35 +02:00
Paul Chote
2e5ef7f059 Show the server map pool in the client map chooser.
Maps that aren't installed are queried from the resource center.
2023-11-05 15:42:35 +02:00
Paul Chote
72646fc7ff Add Server.MapPool setting for dedicated servers.
This takes a list of map UIDs which may be locally installed or hosted
on the resource center. If any maps aren't found, startup will be
delayed by up to 10 seconds while it attempts to query the resource
center.
2023-11-05 15:42:35 +02:00
Daniil Hayrapetyan
01fec1ae02 Fix buildings assigned ro wrong bases in harkonnen09a.lua
Update harkonnen09a.lua
Apply suggestions from code review

Co-Authored-By: JovialFeline <jms.happycat@gmail.com>
2023-11-04 21:02:47 +01:00
Jakub Vesely
3be1de230c Installers: Fix Steam library manifest parsing. Fixes #21129 2023-11-04 18:54:02 +02:00
RoosterDragon
e83e580f23 Don't clear/reset shroud when using the /all debug command.
Disabling the shroud is sufficient to allow seeing the map. This fixes a game with the "Explored Map" option enabled. Previously using the `/all` command twice to toggle it on and off again would also reset the shroud, causing the map to no longer be explored. Now, using it twice will cause the map to remain explored, as intended when the "Explored Map" option is enabled.
2023-11-04 18:46:08 +02:00
RoosterDragon
8e80117eb8 Use single dictionary call in Shroud.AddSource, Shroud.RemoveSource. 2023-11-04 18:46:08 +02:00
RoosterDragon
0c2d060d43 Use Array.IndexOf to speed up Shroud.Tick.
As the `touched` cell layer uses Boolean values, Array.IndexOf is able to use a fast vectorised search. Most values in the array are false, so the search is able to significantly improve the performance of finding the next true value in the array.
2023-11-04 18:46:08 +02:00
RoosterDragon
5157bc375d Add domain checks to HierarchicalPathFinder.
The domains in HierarchicalPathFinder can be compared to find disjoint areas. For example islands on a water map will belong to different domains. Use these domains in path searches to allow us to bail out early if a path is impossible, e.g. trying to path between different islands. Keeping the domains updated via the RebuildDomains method adds some cost to the average path search, but that savings from path searches that can bail early pays for this many times over.
2023-11-03 15:04:49 +02:00
abcdefg30
b35b560ca1 Add an Offset field to WithDamageOverlayInfo 2023-10-31 20:55:26 +02:00
Gustas
c0da41a18a Increase sound source pool size to the maximum 2023-10-31 00:43:47 +01:00
abcdefg30
d9f5588a1f Fix warnings about NREs in WithEmbeddedTurretSpriteBody 2023-10-30 23:37:52 +02:00
abcdefg30
61c3c252ea Remove an unnecessary variable assignment
The info is already set with the same name in the constructor
2023-10-30 23:37:52 +02:00
abcdefg30
ed3ca78667 Use TryGetValue instead of ContainsKey followed by indexing 2023-10-30 23:37:52 +02:00
abcdefg30
6fb7bb1c08 Silence warnings about multiple enumerations in AIUtils
This method only every receives a list as parameter
2023-10-30 23:37:52 +02:00
abcdefg30
57cef527ba Use Array.Find and List.Find instead of LINQ's FirstOrDefault 2023-10-30 23:37:52 +02:00
abcdefg30
48a2a75211 Use StringBuilder instead of manually appending strings in FieldSaver 2023-10-30 23:37:52 +02:00
abcdefg30
3f0159cd89 Index at 0 instead of using LINQ's First 2023-10-30 23:37:52 +02:00
abcdefg30
7baae40b2d Use Array.Exists and List.Exists instead of LINQ's Any 2023-10-30 23:37:52 +02:00
RoosterDragon
fc0bdce151 Fix RCS1239 2023-10-30 23:31:33 +02:00
RoosterDragon
64de28427c Fix RCS1227 2023-10-30 23:31:33 +02:00
RoosterDragon
c4ca3ca743 Fix RCS1226 2023-10-30 23:31:33 +02:00
RoosterDragon
724511e244 Fix RCS1225 2023-10-30 23:31:33 +02:00
RoosterDragon
e3646595ab Fix RCS1218 2023-10-30 23:31:33 +02:00
RoosterDragon
d2ecd0c777 Fix RCS1216 2023-10-30 23:31:33 +02:00
RoosterDragon
a24308baa5 Fix RCS1214 2023-10-30 23:31:33 +02:00
RoosterDragon
aa8e85fbf4 Fix RCS1192 2023-10-30 23:31:33 +02:00
RoosterDragon
11a892f991 Fix RCS1191 2023-10-30 23:31:33 +02:00
RoosterDragon
cf255fc78e Fix RCS1190 2023-10-30 23:31:33 +02:00
RoosterDragon
258de7a6fd Fix RCS1179 2023-10-30 23:31:33 +02:00
RoosterDragon
fcfee31972 Fix RCS1134 2023-10-30 23:31:33 +02:00
RoosterDragon
11b59b0a65 Fix RCS1132 2023-10-30 23:31:33 +02:00
RoosterDragon
0bb2bc651b Fix RCS1112 2023-10-30 23:31:33 +02:00
RoosterDragon
c63788b686 Fix RCS1099 2023-10-30 23:31:33 +02:00
RoosterDragon
60e86f563c Fix RCS1084 2023-10-30 23:31:33 +02:00
RoosterDragon
ce39e97b86 Fix RCS1080 2023-10-30 23:31:33 +02:00
RoosterDragon
06aa378dfd Fix RCS1074 2023-10-30 23:31:33 +02:00
RoosterDragon
43ebb93ff6 Fix RCS1071 2023-10-30 23:31:33 +02:00
RoosterDragon
4fe2ed3df0 Fix RCS1068 2023-10-30 23:31:33 +02:00
RoosterDragon
1a299d10ed Fix RCS1058 2023-10-30 23:31:33 +02:00
RoosterDragon
d1dc6293e8 Fix RCS1049 2023-10-30 23:31:33 +02:00
RoosterDragon
9f1ea57d3c Fix RCS1041 2023-10-30 23:31:33 +02:00
RoosterDragon
917b0512bf Enable Roslynator
Remove existing rules which were not enforced and have some existing violations. Enforce a suite of useful rules that have no existing violations.
2023-10-30 15:30:10 +01:00
michaeldgg2
b9b5b90330 Allow changing ZOffset of renderables in ActorPreviewPlaceBuildingPreview 2023-10-30 15:15:21 +01:00
RoosterDragon
216758dbc7 Fix Locomotor.CanMoveFreelyInto when using ignoreSelf.
The ignoreSelf flag is intended to allow the current actor to be ignored when checking for blocking actors. This check worked correctly for cells occupied by a single actor. When a cell was occupied by multiple actors, the check was only working if the current actor happened to be the first actor. This is incorrect, if the current actor is anywhere in the cell then this flag should apply.

This flag failing to be as effective as intended meant that checks in methods such as PathFinder.FindPathToTargetCells would consider the source cell inaccessible, when it should have considered the cell accessible. This is a disaster for performance as an inaccessible cell requires a slow fallback path that performs a local path search. This means pathfinding was unexpectedly slow when this occurred. One scenario is force attacking with a group of infantry sharing the same cell. They should benefit from this check to do a fast path search, but failed to benefit from this check and the search would be slow instead.

Applying the flag correctly resolves the performance impact.
2023-10-30 11:33:54 +02:00
Paul Chote
96dc085b35 Make lobby option tooltips work the same as factions. 2023-10-30 00:25:07 +02:00
Paul Chote
b28a3b6a5a Fix lobby faction tooltip rendering. 2023-10-30 00:25:07 +02:00
Paul Chote
500ee54f04 Fix margins of TD ingame menu panels. 2023-10-29 20:31:50 +02:00
Paul Chote
dd95b199b7 Fix a collection of mission browser UI issues. 2023-10-29 20:25:30 +02:00
Matthias Mailänder
3d9ac5a85e Update DiscordRichPresence to version 1.2.1.24. 2023-10-27 13:34:14 +03:00
Paul Chote
8503678fc7 Support loading sprites with pre-multiplied alpha. 2023-10-27 13:20:07 +03:00
Paul Chote
37ce5e447f Replace custom factpdox sprite with dynamically rendered vortex. 2023-10-27 10:37:28 +03:00
Paul Chote
44d7903a4b Add dynamic ChronoVortexRenderable. 2023-10-27 10:37:28 +03:00
Paul Chote
a3c0cee2cc Fix IRenderPostProcessPass texture unit binding. 2023-10-25 12:28:24 +03:00
Oliver Brakmann
4cc9b1be2b Allow actors to target terrain without force-fire 2023-10-24 22:13:43 +03:00
Paul Chote
f1fba1ed14 Fix shader type conversion. 2023-10-24 22:03:43 +03:00
Paul Chote
3bb42522b8 Pack vertex attributes and palette into a single integer bitfield. 2023-10-23 22:42:33 +03:00
Paul Chote
143cd8f856 Add support for signed and unsigned integer vertex attributes. 2023-10-23 22:42:33 +03:00
Paul Chote
4547f3c2b9 Change PaletteReference.TextureIndex to an integer. 2023-10-23 22:42:33 +03:00
Paul Chote
c3ff5d954a Ensure consistent state in the world texture before rendering. 2023-10-23 22:42:33 +03:00
Paul Chote
43ddee5d30 Simplify post-processing shaders. 2023-10-23 22:42:33 +03:00
Paul Chote
813a1984f9 Fix shader type conversion. 2023-10-22 22:20:23 +03:00
Paul Chote
9a5f5f9f8f Remove legacy OpenGL support. 2023-10-22 19:51:46 +03:00
Paul Chote
cb55039ec9 Replace GlobalLightingPaletteEffect with a post-processing shader. 2023-10-22 19:34:05 +03:00
Paul Chote
a51a9700cf Replace FlashPaletteEffect with a post-processing shader. 2023-10-22 19:34:05 +03:00
Paul Chote
59d40c8b4e Replace ChronoshiftPaletteEffect with a post-processing shader. 2023-10-22 19:34:05 +03:00
Paul Chote
7adcba5b7f Enable start/end fades in D2k. 2023-10-22 19:34:05 +03:00
Paul Chote
fe6de396f2 Replace MenuPaletteEffect with a post-processing shader. 2023-10-22 19:34:05 +03:00
Paul Chote
47af7a9023 Add IPostProcessWorldShader for custom effect render passes. 2023-10-22 19:34:05 +03:00
dnqbob
b1f5367822 Allow mission use LobbyOptions as options and remove unused translation 2023-10-22 13:51:25 +02:00
dnqbob
cd40d150c1 TS: Hover MLRS simplify 2023-10-21 22:21:12 +03:00
dnqbob
98160512b8 Fix LeavesTrails add effect at where actor removed 2023-10-21 22:21:12 +03:00
Gustas
9a235f2256 Manual fixup 2023-10-21 19:35:00 +02:00
Gustas
754e7845f3 Automated translation extraction 2023-10-21 19:35:00 +02:00
Gustas
cbd6b67456 Add automated chrome string extractor. 2023-10-21 19:35:00 +02:00
Gustas
1f0e73906e Fix static linting 2023-10-21 19:35:00 +02:00
Gustas
f4d1c924d7 Remove model slider from common 2023-10-21 19:35:00 +02:00
dnqbob
1a98312595 TS Service Depot: allow sell unit when repairing 2023-10-21 19:47:28 +03:00
dnqbob
3bc4a6c9dc Add GrantConditionWhenDock pair 2023-10-21 19:47:28 +03:00
dnqbob
8b96b75960 LeavesTrails only works when actor inworld 2023-10-21 18:43:45 +02:00
dnqbob
d69dbd2793 FloatingSpriteEmitter only works when actor inworld 2023-10-21 18:43:45 +02:00
Jakub Vesely
cd5eb89ebc TS: EMP Cannon should only be able to fire via the support power. Fixes #20828 2023-10-21 18:36:48 +02:00
Paul Chote
20c683fb4f Enforce stricter checks on sequence Facings. 2023-10-21 18:23:37 +03:00
dnqbob
c427e24360 DetectCloaked: actor should be in world 2023-10-17 20:17:26 +03:00
Gustas
feced5505a Remove the possibility of ReloadDelay becoming 0 with modifiers 2023-10-17 14:18:35 +02:00
Pavel Penev
806eebd269 Deprecated DateTimeGlobal.IsHalloween 2023-10-17 14:04:39 +02:00
Pavel Penev
b394e15998 Added current datetime properties to the Lua API
Also deprecated the IsHalloween property in favour of them.
2023-10-17 14:04:39 +02:00
Pavel Penev
13d446e27e Fixed some bogus space indentation 2023-10-17 14:04:13 +02:00
Pavel Penev
85d62f7e5e Extended indentation rules to more file types
This reflects OpenRA ModSDK PR 189.
2023-10-17 14:04:13 +02:00
Pavel Penev
7515c180b9 Added missing deprecation notices to Lua docs 2023-10-16 19:48:03 +02:00
abcdefg30
36d44925cb Move Voxel assets browser preview definitions from common to ts 2023-10-15 19:29:15 +02:00
Pavel Penev
c0f3f97811 Suppressed unused function parameter warning 2023-10-14 22:12:47 +02:00
Pavel Penev
4e72026ff9 Fixed table fields all being treated as readonly
A recent update in the Lua extension makes it consider all fields that are defined as table entries annotated with @type to be readonly (providing a somewhat misleading warning saying that they don't exist). Defining them as @field annotations on the class makes it tread them normally.
This affects ScriptActorProperties and ScriptPlayerProperties.
2023-10-14 22:12:47 +02:00
Pavel Penev
74df2d22da Fixed initTable warnings about missing properties
The Lua extension would report missing/uninitialized fields on actor creation because it thought they were required. This makes them all optional, except for OwnerInit, which is special.
2023-10-14 22:12:47 +02:00
dnqbob
68d053336b Fix AutoCrusher uneffective. 2023-10-14 20:39:12 +03:00
abcdefg30
876b66b295 Fix AutoCrusher not being conditional 2023-10-13 15:38:36 +03:00
abcdefg30
5eb6ba6e5c Revert "Remove an outdated comment from AutoCrusher.cs"
This reverts commit c8779e2a6b
2023-10-13 15:38:36 +03:00
abcdefg30
1dc14ed9f1 Make AutoCrusher aware of Cloak and Disguise 2023-10-13 15:38:36 +03:00
abcdefg30
72bb6c4c99 Restore the light source settings which were previously in effect 2023-10-13 15:29:46 +03:00
abcdefg30
a960eb471b Fix the normal palette not being used if ModelWidget has no player palette 2023-10-13 15:29:46 +03:00
abcdefg30
e76d89f0db Clean the caching inside ModelWidget up 2023-10-13 15:29:46 +03:00
abcdefg30
d2fdd3c753 Fix no light source being defined in ModelWidget 2023-10-13 15:29:46 +03:00
abcdefg30
30de1cdf5d Remove the unused preview variable from ModelWidget 2023-10-13 15:29:46 +03:00
abcdefg30
6b151e6be5 Remove an unnecessary null check from ModelWidget 2023-10-13 15:29:46 +03:00
dnqbob
f5450cdf50 Fix D2k airdrop visual 2023-10-13 14:41:23 +03:00
dnqbob
4b9de8ac42 CNC: Fix Nod airdrop offset 2023-10-13 14:41:23 +03:00
dnqbob
13a6e027ef Add LandOffset for ProductionAirdrop 2023-10-13 14:41:23 +03:00
dnqbob
fc77c3ce48 Add LandingTick to ProductionAirdrop 2023-10-13 14:41:23 +03:00
abcdefg30
85c8f6c446 Fix ProductionBar visually glitching for units without value 2023-10-11 12:10:05 +03:00
Christoffer Olofsson
d349209dc9 Update README.md 2023-10-10 17:40:47 +03:00
abcdefg30
c8779e2a6b Remove an outdated comment from AutoCrusher.cs 2023-10-10 14:44:00 +03:00
dnqbob
b55606c37f ReinforceWithTransport: no hardcoded land facing 2023-10-09 19:16:56 +03:00
michaeldgg2
12fb091bbc Added callback in Passenger during unload from cargo just before the actor is added back to the world 2023-10-09 18:21:04 +03:00
dnqbob
bc37d7169d GrantConditionOnDeployWithCharge requires no IMove 2023-10-09 17:59:49 +03:00
michaeldgg2
9ae26f2645 FireWarheads: play weapon report sound in Tick() not in FrameEndTask 2023-10-09 17:55:31 +03:00
michaeldgg2
6367729f98 Remove redundant dependency of FireWarheads on IMove 2023-10-09 17:55:31 +03:00
Matthias Mailänder
b8b93af977 Update Linguini. 2023-10-09 17:50:02 +03:00
abcdefg30
9f96d0c772 Add NotBefore<SpawnStartingUnitsInfo> to LuaScriptInfo 2023-10-06 15:01:46 +03:00
Gustas
d5c940ba4c Close the ingame menu upon voting 2023-09-27 10:41:13 +03:00
Gustas
144e716cdf Add vote kick 2023-09-27 10:41:13 +03:00
Gustas
686040a316 Turn ModelRenderer and VoxelCache into traits 2023-09-23 19:12:51 +02:00
Gustas
d427072cc9 Extract StoresResources from Harvester 2023-09-23 19:06:07 +02:00
Gustas
60a446123b Fix TakeOffOnCreation 2023-09-23 18:39:58 +02:00
Gustas
c009f58980 Clear up the projection definition 2023-09-23 16:46:45 +02:00
Gustas
79b10ba9a5 Remove unused 4th dimension 2023-09-23 16:46:45 +02:00
Gustas
d05e0f23ea Remove unused tint attribute from model shader 2023-09-23 16:46:45 +02:00
Gustas
26b6118f50 Extract vertex attributes 2023-09-23 16:46:45 +02:00
Gustas
0a90c2a95e Remove Vertex from PlatformInterfaces 2023-09-23 16:46:45 +02:00
Gustas
d77fd5c13e Simplify weapon yaml definitions 2023-09-23 14:33:27 +02:00
Gustas
4dec79a5fb Fix Armament not working properly with value 0 in BurstDelays 2023-09-23 14:33:27 +02:00
RoosterDragon
b7e0ed9b87 Improve lookups of nodes by key in MiniYaml.
When handling the Nodes collection in MiniYaml, individual nodes are located via one of two methods:

// Lookup a single key with linear search.
var node = yaml.Nodes.FirstOrDefault(n => n.Key == "SomeKey");

// Convert to dictionary, expecting many key lookups.
var dict = nodes.ToDictionary();

// Lookup a single key in the dictionary.
var node = dict["SomeKey"];

To simplify lookup of individual keys via linear search, provide helper methods NodeWithKeyOrDefault and NodeWithKey. These helpers do the equivalent of Single{OrDefault} searches. Whilst this requires checking the whole list, it provides a useful correctness check. Two duplicated keys in TS yaml are fixed as a result. We can also optimize the helpers to not use LINQ, avoiding allocation of the delegate to search for a key.

Adjust existing code to use either lnear searches or dictionary lookups based on whether it will be resolving many keys. Resolving few keys can be done with linear searches to avoid building a dictionary. Resolving many keys should be done with a dictionary to avoid quaradtic runtime from repeated linear searches.
2023-09-23 14:31:04 +02:00
Gustas
0ab7caedd9 Fix CandidateMouseoverCells being incorrectly calculated for Rectangular grid 2023-09-23 14:13:53 +02:00
Gustas
3824a591d5 Fix CandidateMouseoverCells not accounting for tile scale 2023-09-23 14:13:53 +02:00
Gustas
3e6123f6f6 Add index buffer SpriteRenderer 2023-09-23 14:10:35 +02:00
Gustas
2763e1502b Add quadIndexBuffer to Renderer 2023-09-23 14:10:35 +02:00
Gustas
0b90622251 Add index buffer to TerrainSpriteLayer 2023-09-23 14:10:35 +02:00
Gustas
9b8895df39 Add glDrawElements 2023-09-23 14:10:35 +02:00
Gustas
f6c1453b5b Add StaticIndexBuffer 2023-09-23 14:10:35 +02:00
Gustas
7e9619b41b VertexBuffer should be disposable 2023-09-23 14:10:35 +02:00
Gustas
90aeb38427 Fix potential crash if attempted to unload outside of the map 2023-09-23 13:34:44 +02:00
Gustas
6040187844 Fix CurrentAdjacentCells cache not acting as a cache 2023-09-23 13:34:44 +02:00
Gustas
e72d0ed2c6 Nudge self after being ejected 2023-09-23 13:34:44 +02:00
Gustas
c3b4e2b237 Fix EjectOnDeath checks 2023-09-23 13:34:44 +02:00
Rudy Alex Kohn
7769764b0b added new method to convert byte array to lower case hex-string
added unit test

update ToHex(byte[]) to support mono

added punctuations to unit test summary and parameter description

Replaced with Convert.ToHexString(), public ToHex() + use from Color.ToString()

Adjusted back to a simpler mono compatible version only, with lowered allocation
2023-09-23 10:14:44 +03:00
Gustas
b25146265d Fix units considering terrain when entering other actors 2023-09-22 17:06:00 +02:00
JovialFeline
e0df59464e Disable flak truck in Soviet-13, others 2023-09-22 12:26:27 +03:00
RoosterDragon
a67320e431 When serializing terrain positions for an order, serialize a 0-length array in a way that roundtrips.
Previously, a 0 length array would not roundtrip and would deserialize as a center position instead.
2023-09-19 11:44:49 +03:00
abcdefg30
e41279fe6b Fix terrain positions for targets not being serialized for Orders 2023-09-19 11:44:49 +03:00
Gustas
29eaab59be Add backup ExplicitSequenceFilenames to update rules 2023-09-18 11:05:19 +03:00
penev92
541d53127a Bumped Eluant NuGet version
The new version fixes the windows 32-bit build not working.
2023-09-16 20:07:22 +02:00
Avlas
bdcf754d34 Bullet explodes on impact when hitting target 2023-09-14 16:39:02 +03:00
RoosterDragon
a67e85e092 Improve AI squad pathing and regrouping behavior.
Ensure the target location can be pathed to by all units in the squad, so the squad won't get stuck if some units can't make it. Improve the choice of leader for the squad. We attempt to a choose a leader whose locomotor is the most restrictive in terms of passable terrain. This maximises the chance that the squad will be able to follow the leader along the path to the target. We also keep this choice of leader as the squad advances, this avoids the squad constantly switching leaders and regrouping backwards in some cases.
2023-09-11 14:56:59 +03:00
dnqbob
24536fa296 Fix Air Squad danger detection broken in RA 2023-09-11 14:33:32 +03:00
dnqbob
38ed21edd2 StateBase: More accurate way to check rearming 2023-09-11 14:33:32 +03:00
dnqbob
5d2f2bdd1d Add TraitLocation to all bot modules. 2023-09-11 14:33:32 +03:00
dnqbob
6515403ae6 Fix wrong target types in MinelayerBotModule of ra mod 2023-09-11 14:33:32 +03:00
Gustas
2f696b2ce7 Increase Iron Curtain's footprint 2023-09-09 18:45:04 +02:00
Matthias Mailänder
61d51d971c Remove misplaced bridge actors. 2023-09-09 18:41:04 +02:00
Gustas
90c7680743 Fix DropPodsPower triggering radar pings upon failure 2023-09-09 17:09:08 +02:00
Gustas
b59bb998eb Fix DropPods only using definitions only of the first drop pod
Cache permanent variables
2023-09-09 17:09:08 +02:00
Gustas
9845306b68 Cache unitTypes
And rename variables to names that more sense
2023-09-09 17:09:08 +02:00
Gustas
4eb683ab46 Add TS mobile EMP 2023-09-09 16:53:22 +02:00
Gustas
9d7feb176a Add offset to WithVoxelBody 2023-09-09 16:53:22 +02:00
dnqbob
eab0bf8f82 Fix bug that AI producion pause when there is too many unit in UnitDelays 2023-09-09 15:15:08 +03:00
Gustas
085a4c421b Add back to editor button 2023-09-09 13:46:35 +02:00
Gustas
4fc4fb2fb3 Add Play button to map editor 2023-09-09 13:46:35 +02:00
Gustas
0e5ed6a30c Extract ExitMapEditor in IngameMenuLogic 2023-09-09 13:46:35 +02:00
Gustas
5cc59ae3ac Move ValidRelations from Capturable to Captures
To better match weapon definitions
2023-09-09 13:24:33 +02:00
Gustas
161f4cbdff Fix inconsistent ordering 2023-09-09 13:24:33 +02:00
dnqbob
5b0f69b411 Fix the inaccuracy used when lock on in Missile. 2023-09-08 13:49:12 +03:00
RoosterDragon
23f3f8d90c Add helper methods to locate actors that can be reached via a path.
Previously, the ClosestTo and PositionClosestTo existed to perform a simple distance based check to choose the closest location from a choice of locations to a single other location. For some functions this is sufficient, but for many functions we want to then move between the locations. If the location selected is in fact unreachable (e.g. on another island) then we would not want to consider it.

We now introduce ClosestToIgnoringPath for checks where we don't care about a path existing, e.g. weapons hitting nearby targets. When we do care about paths, we introduce ClosestToWithPathFrom and ClosestToWithPathTo which will check that a path exists. The PathFrom check will make sure one of the actors from the list can make it to the single target location. The PathTo check will make sure the single actor can make it to one of the target locations. This difference allows us to specify which actor will be doing the moving. This is important as a path might exists for one actor, but not another. Consider two islands with a hovercraft on one and a tank on the other. The hovercraft can path to the tank, but the tank cannot path to the hovercraft.

We also introduce WithPathFrom and WithPathTo. These will perform filtering by checking for valid paths, but won't select the closest location.

By employing the new methods that filter for paths, we fix various behaviour that would cause actors to get confused. Imagine an islands map, by checking for paths we ensure logic will locate reachable locations on the island, rather than considering a location on a nearby island that is physically closer but unreachable. This fixes AI squad automation, and other automated behaviours such as rearming.
2023-09-07 17:46:35 +03:00
RoosterDragon
2ac855488b Validate order targets when resolving orders. 2023-09-07 17:46:35 +03:00
Bryan Quigley
c08ddb61b3 Better Naval AI
I noticed even on a naval only map, the naval AI doesn't necessarily beat a Normal AI. This makes it much more likely that it will.

 - Drop number of ore refineries and ore trucks. As Naval AI is mostly suited for islands I haven't found a map that really needs as many as the other AIs.
 - Reduce number of ground based base defenses - and delay Tesla coil a lot.
 - Reduce number of migs as yaks more useful if they just get blown up.
 - Add Flak trucks and v2s for base defense for Soviet
 - Add Jeep and Arty for base defense for Allied
 - Add delay for building ore truck so now chance of building one first from War Factory
 - A service depot is not useful for this AI except for building an MCV so delay it a lot.

Tested with Ukraine and Germany and can consistently beat normal on island map.
2023-09-07 17:00:04 +03:00
dnqbob
fb55f2824e UnitBuilderBotModule and BaseBuilderBotModule fix on muti-queue performance:
1. Only allow new item being queued when cash above a certain number

2. Only tick one kind of queues at one tick, reduce the pressure on the actived tick

3. 'BaseBuilderBotModule' will check all buildings in producing, avoid queue mutiple same buildings.
2023-09-07 16:40:57 +03:00
dnqbob
1b0c93e5ff Fix new NewProductionCashThreshold check ignore player cash. 2023-09-07 16:40:57 +03:00
dnqbob
19c8c36030 Replace Cash + Resources with GetCashAndResources() 2023-09-07 16:40:57 +03:00
dnqbob
931118e1d8 Add GetCashAndResources() to PlayerResources, to get overall credits. 2023-09-07 16:40:57 +03:00
RIP-webmaster
61f1660b38 Update OpenRA.Mods.Common.csproj 2023-09-06 08:59:23 +03:00
RIP-webmaster
634cf900e6 Remove reference to obsolete package 2023-09-06 08:59:23 +03:00
Gustas
a148f30070 Simplify matrix utils 2023-09-03 22:58:04 +02:00
dnqbob
3e0daa62c4 Fix Target.Invalid comparion bug in AutoTarget 2023-09-01 20:28:20 +03:00
RoosterDragon
aac1bae899 Prefer ReadUInt8 over ReadByte.
The former will throw when the end of the stream is reached, rather than requiring the caller to check for -1.
2023-08-29 16:17:27 +02:00
RoosterDragon
f5f2f58664 Use Stream.Write(int) extension method where possible. 2023-08-29 16:17:27 +02:00
Matthias Mailänder
f428a44bfc This is not just about difficulty. 2023-08-28 23:34:48 +03:00
Matthias Mailänder
ce412e4404 The description is optional so don't crash when it is null. 2023-08-28 23:34:48 +03:00
JovialFeline
7bd4b4558e Add text fix, polish to Controlled Burn 2023-08-28 19:32:18 +02:00
Gustas
619fb6633a Cache uniform locations 2023-08-28 19:18:05 +02:00
Matthias Mailänder
bf64339890 Automatically move blockers when transform deploying. 2023-08-26 20:43:50 +03:00
Gustas
d9787b168d Add shuriken island 2023-08-25 21:11:52 +02:00
Gustas
4a81d9b6f7 Remove haos ridges 2023-08-25 21:11:52 +02:00
michaeldgg2
4370c47f6e Make FloatingSprite public 2023-08-23 23:40:11 +03:00
RoosterDragon
f69e6289b5 Handle re-entrant RunUnsynced correctly.
If nested calls to RunUnsynced are running, then using a bool would cause the flag to be reset once the inner function completes, but an outer function may still be running and not yet ready for the flag to be reset. To correctly handle nested calls, we track a count and only reset the flag once all functions have completed.
2023-08-23 20:56:20 +03:00
Gustas
bfd0cd7108 Report all OpenGL errors 2023-08-22 20:18:44 +02:00
RoosterDragon
df534736a1 Don't enforce style rules that require .NET 7.
As the solution currently targets .NET 6, a variety of style rules only introduced in .NET 7 are not suitable for enforcing as warnings (which are treated as errors in the CI pipeline). Anybody compiling locally with a .NET 6 SDK won't be able to trigger these rules locally, but the Linux CI agent comes with the .NET 7 SDK and will trigger these rules. This provides a poor dev experience as the CI run will report errors that don't reproduce locally.

To remove this developer friction, reduce the severity of these rules to avoid CI runs failing.
2023-08-22 18:22:19 +02:00
RoosterDragon
93a97d5d6f Fix CA1851, assume_method_enumerates_parameters = true 2023-08-20 20:41:27 +02:00
RoosterDragon
3275875ae5 Fix CA1851 2023-08-20 20:41:27 +02:00
abcdefg30
88f830a9e5 Fix Folder.GetStream using FileNotFoundExceptions to detect if a file exists 2023-08-20 17:44:31 +03:00
Matthias Mailänder
c609c4af14 Extract text feedback messages. 2023-08-19 20:46:04 +03:00
Matthias Mailänder
94c8339e17 Allow for optional localised text notifications. 2023-08-19 20:46:04 +03:00
Matthias Mailänder
b742a776eb Refactor LocalizedMessage. 2023-08-19 20:46:04 +03:00
Matthias Mailänder
1899eed839 Add localisation support to transient lines. 2023-08-19 20:46:04 +03:00
Matthias Mailänder
43d1a20d8c Fix missing init-only modifier. 2023-08-19 20:46:04 +03:00
dnqbob
1692f32ffc Make aircraftInfo in carryall private 2023-08-19 11:55:27 +03:00
dnqbob
e07869e71f Autocarryall put down unit if destination is cancelled when picking up 2023-08-19 11:55:27 +03:00
dnqbob
c9dfb215ae Auto carry action can be controlled by condition 2023-08-18 20:47:48 +03:00
Matthias Mailänder
98896f9a75 Make Cargo and Carryall conditional. 2023-08-13 18:38:17 +03:00
michaeldgg2
a14cc8cc4d Make Bullet projectile extensible 2023-08-13 18:00:16 +03:00
abcdefg30
e1940eec77 Remove a bogus CanDeploy check from order resolving for charge deploys 2023-08-11 20:21:58 +03:00
RoosterDragon
a1dfb42812 Fix IDE0251 2023-08-11 15:51:53 +02:00
RoosterDragon
3b2fad6ea8 Add and enforce new Code Style Rules (IDEXXXX) 2023-08-11 15:51:53 +02:00
RoosterDragon
d9df27d574 Reorder Code Style Rules to match newer documentation. 2023-08-11 15:51:53 +02:00
Gustas
ae45707c84 Fix ProximityExternalCondition ignoring the owner change event 2023-08-10 19:48:04 +02:00
Gustas
e22d7b31f9 Fix selected map in server creation panel not updating 2023-08-10 19:31:38 +02:00
Gustas
0dcb341059 Make MapPreviewLogic initialisers optional 2023-08-10 19:31:38 +02:00
Gustas
3ecb267594 Delay AI's radar dome 2023-08-10 19:06:57 +02:00
Matthias Mailänder
2744b44d93 Move mine layer AI to common and polish. 2023-08-08 18:15:42 +03:00
Matthias Mailänder
0528ef58b2 Extract hard-coded FPS limiter with parameter. 2023-08-08 17:16:58 +03:00
Matthias Mailänder
2a223363b8 Avoid Fluent syntax for highlighted text. 2023-08-08 17:16:58 +03:00
Matthias Mailänder
de9a5eb71e More descriptive IDs that match between mods. 2023-08-08 17:16:58 +03:00
dnqbob
2b0afd6acb Add MinelayerBotModule 2023-08-08 16:15:43 +02:00
Gustas
3ab421cbe3 Allow queueing up scatter and move Nudge to an activity 2023-08-08 16:10:53 +02:00
Gustas
54dac39e83 Fix crates spawning subcell incorrectly and spawned actors not crushing crates/mines 2023-08-08 16:04:35 +02:00
Gustas
2de212710a Fix crates spawning actors inside other actors 2023-08-08 16:04:35 +02:00
Gustas
60fbecd4a7 Added manual Saboteur cloaking 2023-08-08 14:56:18 +02:00
Gustas
82458b5f7e Add INotifyClientMoving interface 2023-08-08 14:48:59 +02:00
Gustas
d0974cfdd2 Abstract docking logic from Harvester and Refinery 2023-08-08 14:48:59 +02:00
Gustas
da16e4ed99 Rename docking activities
HarvesterDockSequence -> GenericDockSequence
DeliverResources -> MoveToDock
2023-08-08 14:48:59 +02:00
Gustas
55536bba4c Remove unused variables
Redundant since 2013
PR: # 3407
Commit: 1eb04a70a5
2023-08-08 14:48:59 +02:00
RoosterDragon
388222c5c7 Remove Exts.WithDefault 2023-08-07 21:38:09 +02:00
RoosterDragon
169c60883b Fix CA2249, CA2251 2023-08-07 21:38:09 +02:00
RoosterDragon
285443f10f Fix CA1310, CA1311 2023-08-07 21:38:09 +02:00
RoosterDragon
d83e579dfe Fix CA1305 2023-08-07 21:38:09 +02:00
RoosterDragon
486a07602b Fix CA1304 2023-08-07 21:38:09 +02:00
RoosterDragon
949ba589c0 MiniYaml becomes an immutable data structure.
This changeset is motivated by a simple concept - get rid of the MiniYaml.Clone and MiniYamlNode.Clone methods to avoid deep copying yaml trees during merging. MiniYaml becoming immutable allows the merge function to reuse existing yaml trees rather than cloning them, saving on memory and improving merge performance. On initial loading the YAML for all maps is processed, so this provides a small reduction in initial loading time.

The rest of the changeset is dealing with the change in the exposed API surface. Some With* helper methods are introduced to allow creating new YAML from existing YAML. Areas of code that generated small amounts of YAML are able to transition directly to the immutable model without too much ceremony. Some use cases are far less ergonomic even with these helper methods and so a MiniYamlBuilder is introduced to retain mutable creation functionality. This allows those areas to continue to use the old mutable structures. The main users are the update rules and linting capabilities.
2023-08-07 21:57:10 +03:00
Matthias Mailänder
b6a5d19871 Evaluate read only dictionaries. 2023-08-06 17:12:34 +03:00
Gustas
ce002ce8c1 Fix gen1 map importer crashing on invalid tiles 2023-08-06 13:53:22 +02:00
Gustas
9c3e366d03 Fix out of bounds cells not being randomised 2023-08-06 13:53:22 +02:00
Gustas
bb96e22e64 Fix low power notification never triggering 2023-08-05 19:03:15 +02:00
Gustas
a691f2ebac Give husks the ability to crush 2023-08-05 14:27:51 +02:00
Gustas
7638822e49 Disable force start panel start button when unable to start the game 2023-08-05 14:18:15 +02:00
Gustas
a9cf728ee1 Refactor MapPreviewLogic
and add a states for updating map via MapCache.GetUpdatedMap
2023-08-05 14:18:15 +02:00
Gustas
2c4a135c2b Grant condition to units closest to the crate 2023-08-05 13:32:51 +02:00
Gustas
d686634c0b Fix aircraft jittering 2023-08-05 13:27:32 +02:00
Gustas
32b0003a72 Fix misaligned TD combat observer tab 2023-08-05 13:20:33 +02:00
Matthias Mailänder
c234b4c78f Send the join message/ping also in skirmish. 2023-08-04 21:47:28 +03:00
Matthias Mailänder
f2a242b09a Let all lobby sounds be optional. 2023-08-04 21:47:28 +03:00
Matthias Mailänder
a1efb28f0b Add lobby sounds for leave, join and option change 2023-08-04 21:47:28 +03:00
Smittytron
d217ab39c2 Add Soviet13b 2023-08-03 16:22:42 +02:00
Gustas
31840328b7 Exit game save with escape 2023-08-03 15:49:33 +02:00
Gustas
54547a11d0 Trigger a button sound when saving a game with enter 2023-08-03 15:49:33 +02:00
Gustas
f99db8d754 Fix lua sanity check crashing on dedicated servers 2023-08-03 15:34:05 +02:00
Vapre
1ce916182d RingBuffer primitive. 2023-08-02 19:42:31 +03:00
abcdefg30
09ba09f4e3 Fix RA assets installation from the Steam C&C:R version 2023-08-01 22:28:32 +03:00
dnqbob
2ac85ac61d Add InstantlyRepairsProperties 2023-08-01 12:21:19 +02:00
dnqbob
44e024a94e Make InstantRepair public 2023-08-01 12:21:19 +02:00
Gustas
462a3ef3c0 Make yaml node resolving optional 2023-07-30 20:39:21 +02:00
Gustas
d4e6815f64 Fix AutoTarget ignoring frozen actor bot targeting hack
Mirrors check of the function Recalculate which is found it TargetExtensions class
2023-07-30 20:32:18 +02:00
Gustas
723ffdf33d Revert Hunt to move within 2 cells of the target
Otherwise infantry get stuck within weapon range but outside of vision range
2023-07-30 20:26:41 +02:00
Gustas
8376b09129 Normalise depth charges 2023-07-29 14:22:30 -05:00
Gustas
fa9ce3e2ac Normalise TD anti-air values 2023-07-29 14:22:30 -05:00
Gustas
605681b252 Normalise RA anti-air values 2023-07-29 14:22:30 -05:00
dnqbob
a3c5945f2a Set BackwardDuration to -1 means ignore the time and set MaxBackwardCells to -1 means ignore the distance. 2023-07-29 18:01:40 +03:00
dnqbob
d7ef22d64f Add MaxBackwardCells for moving backward control 2023-07-29 18:01:40 +03:00
michaeldgg2
d907192be0 Added GrantConditionOnMinelaying trait
Uses INotifyMineLaying callbacks
2023-07-28 12:55:06 +03:00
michaeldgg2
74f8db0578 LayMines: render minefield cells only if the planned minefield has more than 1 cell 2023-07-28 12:55:06 +03:00
Smittytron
9dca6ef1c3 Add Soviet13a 2023-07-27 22:09:30 +03:00
Gustas
c093e7c90b Fix hunt incorrectly pathing to uncrushable targets 2023-07-27 16:14:29 +03:00
michaeldgg2
66cf912da0 LayMines: fixed occasional incorrect mine position when using BeginMinefield order
When laying mine with PreLayDelay > 0, end activity's tick immediately. That means don't try to immediately move to next cell.

This change unifies the behavior with scenario when a mine is laid without any PreLayDelay.
2023-07-26 22:05:42 +03:00
Gustas
42baa0c42f Rename update rule folder to 20230225 2023-07-26 20:00:54 +02:00
Gustas
305ba1c567 Add missing rule to UpdatePath 2023-07-26 20:00:54 +02:00
Gustas
a84b7591f6 Fix invalid color adjuster not considering saturation and V as mutable 2023-07-26 20:00:54 +02:00
Gustas
9d8f0634b1 Revert color validator 2023-07-26 20:00:54 +02:00
Gustas
4cd4e1f8ea Move PlayerExperience from Infiltrates to InfiltrateFor 2023-07-25 21:15:14 +02:00
Gustas
3207d01cf2 Consider AutoTarget ScanRadius when attack moving 2023-07-25 19:48:57 +02:00
Gustas
9fc0f79703 Add a description for AttackBomber 2023-07-25 13:11:17 +02:00
michaeldgg2
1a2d43fc99 WorldRenderer: use string.IsNullOrEmpty for check in Palette method
Unified usage of WorldRenderer.Palette method when it comes to appending player name (in case of player palette)
2023-07-25 13:33:11 +03:00
EoralMilk
b944b21325 Aircraft won't take off on terrain height change when idle 2023-07-25 12:48:50 +03:00
Gustas
c7e0bc4c08 Add missing carryall checks 2023-07-25 10:02:46 +03:00
Gustas
a69417f0a6 Fix caryall not removing influence when cargo dies 2023-07-25 10:02:46 +03:00
Gustas
c36609cc9f Don't call DetachCarryable every tick 2023-07-25 10:02:46 +03:00
Gustas
1edf313090 Don't calculate range when it is unused 2023-07-25 10:02:46 +03:00
RoosterDragon
813d48dd70 Ensure save file is closed after saving.
If you attempt to load after saving, this prevents an exception from the file being "in use" due to the unclosed file handle.
2023-07-18 23:44:40 +02:00
Gustas
9caf12d133 Add an option to order maps by size 2023-07-17 23:34:38 +02:00
Gustas
de5bcbbca5 Add an option to order maps by title 2023-07-17 23:34:38 +02:00
michaeldgg2
8aa548f70c Minelayer: extract creating BeginMinefield order so it can be triggered from outside 2023-07-17 20:18:52 +02:00
michaeldgg2
ce6e73dc92 Minelayer: supports specifying both mine laying and post laying delays 2023-07-17 20:18:52 +02:00
RoosterDragon
4a02e6c6cc Improve Exts.GetOrAdd method to avoid multiple lookups.
Use CollectionsMarshal to hold a ref to the dictionary entry. When the value needs to be added this allows us to set the value directly into it without having to locate the entry a second time.
2023-07-17 20:12:48 +02:00
RoosterDragon
d6a31bb0cc MiniYaml performance tweaks.
- Seal the classes, and make SourceLocation a readonly struct.
- In ToDictionary, use TryAdd to avoid a try-catch.
- In Merge, use ToList to ensure sources is only enumerated once.
2023-07-17 20:12:48 +02:00
RoosterDragon
f5daa19a1c Improve MiniYaml MergePartial performance.
- Track plain keys in a set, to avoid quadratic searches for plain node keys.
- Avoid the Concat iterator by looping twice instead.
2023-07-17 20:12:48 +02:00
RoosterDragon
a96e445e4d Handle duplicate nodes key checks in MiniYaml in a better place.
Moving the key duplication check allows a redundant check on top-level nodes to be avoided. Add tests to ensure key checks are functioning as expected.
2023-07-17 20:12:48 +02:00
RoosterDragon
30b1f926f2 Improve performance of MiniYaml inheritance tree tracking.
Use an ImmutableDictionary to avoid having to clone the inheritance tree in ResolveInherits. This avoids a lot of dictionary clones.
2023-07-17 20:12:48 +02:00
RoosterDragon
58e8b123db Avoid some allocations during loading.
- In FieldLoader, cache boxed bools and some boxed ints.
- In FieldLoader, presize collections when parsing a List, HashSet or Dictionary.
- In FieldLoader, don't allocate a list of missing items until required.
- In FieldLoader, when a string value is passed, avoid wrapping this in a MiniYaml object by allowing both strings and yaml to be passed in the GetValue overload that does the real work.
- In Animation, avoid allocating no-op actions.
- In VxlReader, use EnsureCapcity to better size the Dictionary.
- In VxlReader change VxlElement to a struct.
- In Locomotor, presize TerrainSpeeds dictionary.
2023-07-16 23:21:20 +02:00
RoosterDragon
be04d232c0 Avoid some allocations on the large object heap during loading.
- In MixFile, the Distinct call doesn't presize the HashSet it uses internally. As we know we will enumerate all results, create the HashSet ourselves so that is it presized correctly.
- In ObjectCreator, stream the assembly when hashing rather than reading all bytes into memory.

These changes avoid some allocations on the large object heap, in turn this means the GC can avoid performing unnecessary Gen 2 collections just to clear down the LOH.
2023-07-16 23:21:20 +02:00
Gustas
659ec5e335 Make phase transport uncloak on loading cargo 2023-07-14 17:25:30 +02:00
Paul Chote
7f37454666 Include Linux DE in OS sysinfo string. 2023-07-13 14:34:38 +03:00
michaeldgg2
433d69af7a Make Voiced trait conditional 2023-07-10 18:05:56 +03:00
Matthias Mailänder
21c21e4963 Update OpenAL. 2023-07-07 23:47:32 +02:00
RoosterDragon
1c0885c636 Improve loading performance for loader/compression classes.
- In RLEZerosCompression use dedicated Array.Clear method instead of open-coded loop.
- In VoxelLoader.GenerateSlicePlanes.Get use TryGetValue to avoid repeated array and dictionary lookups.
- In TmpTSLoader.UnpackTileData use ReadBytes to populate array with less overhead compared to repeated one byte reads.
- Resolve TODO in VqaVideo.
2023-07-07 22:36:54 +03:00
michaeldgg2
dccb3ce9ce Added extensibility points to LayMines activity:
Provides two callbacks using INotifyMineLaying interface: MineLaying (just before laying a mine), MineLaid after mine actor has been created (in FrameEndTask)
2023-07-04 23:52:25 +02:00
michaeldgg2
22b39f35aa Mine + related classes are public 2023-07-04 23:52:25 +02:00
michaeldgg2
5ab3276a2d Moved Minelaying related traits and activity to Common 2023-07-04 23:52:25 +02:00
penev92
36420114e0 Fixed packaging for Windows missing assembly info
Apparently building/publishing/packaging for Windows on Linux using .NET 6 sets some of the assembly / Portable Executable information for the generated DLL, but nothing for the generated EXE (which isn't the case when building on Windows).
2023-07-04 20:53:49 +01:00
penev92
7cda031888 Added product version to assembly info 2023-07-04 20:53:49 +01:00
penev92
fc85a4864d Added project information in Directory.Build.props 2023-07-04 20:53:49 +01:00
Gustas
99226c3df5 Always have ActorReference string on optional arrays instantiated 2023-07-01 18:29:59 +02:00
Gustas
ed395c8ace Fix linter crashing on null actor array references 2023-07-01 18:29:59 +02:00
Vapre
edbded8f0a PerfTickLogger, reduce overhead of logging long ticks. 2023-07-01 18:07:27 +02:00
abcdefg30
c095690619 Fix combined sequences using frames being broken 2023-07-01 12:56:50 +02:00
dnqbob
628cc837ef Fix a crash when RallyPoint creates RallyPointIndicator 2023-07-01 12:51:54 +02:00
RoosterDragon
0c32fca6c0 Fix slow saving of map previews.
Avoid quadratic behaviour when searching through actors by creating a dictionary lookup outside the loop.
2023-07-01 12:48:32 +02:00
IceReaper
56b5ace109 WSA use a Framerate of 15, source: FFMPEG. 2023-06-26 19:49:51 +02:00
IceReaper
5572650da2 Video FrameRate is now taken into account when video has no audio. 2023-06-26 19:49:51 +02:00
IceReaper
41669d246f Videos no longer use more vram then required. 2023-06-26 19:49:51 +02:00
abcdefg30
49c837e7d0 Fix \r\n-style line endings not being properly handled for script errors 2023-06-26 19:36:47 +02:00
RoosterDragon
bc5e7d1497 Compress all pngs within oramap files.
Reduces size used for oramap files from 2,774,405 bytes to 2,614,332 bytes in total. The smaller files also improve loading times as the zlib decoder has less bytes to schlep through.

The PNG decoder shipped with OpenRA only supports a bit depth of 8 for images with a palette, so must must ensure that other depths (1,2 or 4) are not used.

zopfilpng is used for compression with the following command line:
'zopflipng.exe --keepcolortype -y -m image.png image.png'

The keepcolortype flag ensures the bit depth is not changed.

The oramap files were updated by:
- Saving as a folder, unpacking the data in loose files.
- Compressing the png files using the command above.
- Saving back as an oramap, except instead of regenerating the map preview on save we save the existing version on disk.
2023-06-26 19:28:06 +02:00
RoosterDragon
78bef8a98f Compress all pngs.
Reduces size used for png files from 19,034,446 bytes to 13,305,798 bytes in total. The smaller files also improve loading times as the zlib decoder has less bytes to schlep through.

The PNG decoder shipped with OpenRA only supports a bit depth of 8 for images with a palette, so must must ensure that other depths (1,2 or 4) are not used.

zopfilpng is used for compression with the following command line:
'zopflipng.exe --keepcolortype -y -m image.png image.png'

The keepcolortype flag ensures the bit depth is not changed.
2023-06-26 19:28:06 +02:00
RoosterDragon
4f3d8f4caa Fix Png parsing.
A regression from 06df75ffee causes this parsing to fail depending on how the image was compressed.
2023-06-26 19:28:06 +02:00
Paul Chote
0369f7516d Work around Gnome 44 titlebar bug. 2023-06-26 19:21:51 +02:00
Gustas
3b0415678c RA Naval balance 2023-06-26 19:13:41 +02:00
Gustas
69867b6c06 Fix EMP'ed units turning 2023-06-21 20:55:52 +02:00
RoosterDragon
231bf01f18 Fix CA1854 2023-06-20 17:57:40 +02:00
RoosterDragon
56fe08cb00 Disable CA1845 2023-06-20 17:57:40 +02:00
RoosterDragon
f752e04b03 Fix CA1816 2023-06-20 17:57:40 +02:00
RoosterDragon
a50e72f68d Fix CA1802 2023-06-20 17:57:40 +02:00
RoosterDragon
0958197df2 Fix CA1052 2023-06-20 17:57:40 +02:00
RoosterDragon
f336a956cf Fix CA1012 2023-06-20 17:57:40 +02:00
darkademic
19fa03435b Only consider non-paused armaments (if any exist) when determining min/max range. 2023-06-20 16:46:21 +02:00
RoosterDragon
fa65e7fd3f Bump Linguini.Bundle to 0.5.0.
This version contains performance improvements for the parser, improving the loading time of translations.
2023-06-15 17:48:37 +02:00
RoosterDragon
f794cf69f9 In TypeDictionary.TrimExcess, also TrimExcess on the internal data dictionary size.
As TypeDictionary instances tend to live a long time without edits after being initially populated, this will reduce their long term memory footprint.
2023-06-13 23:52:44 +02:00
RoosterDragon
366dc5383c In HierarchicalPathFinder.BuildGrid, presize and reuse accessible cell set.
Most cells are accessible, so presizing to the full size of the grid is sensible to avoid allocations to resize as it is filled up. The set can also be reused across all layers to avoid allocating it many times.
2023-06-13 23:52:44 +02:00
michaeldgg2
fd2b14f464 DrawLineToTarget: made palette for rendering sprites customizable (and thus optional too) 2023-06-12 21:10:52 +03:00
Matthias Mailänder
590976a8e7 Add support for Wayland. 2023-06-12 19:57:35 +02:00
Paul Chote
bdbb651b98 Remove unused sequences and an awkward Combine. 2023-06-11 22:04:43 +02:00
Paul Chote
703618be19 Remove obsolete x64 and x64process sysinfo columns. 2023-06-11 13:18:49 +02:00
Paul Chote
1f37728ecf Return proper sysinfo OS names for Linux/macOS. 2023-06-11 13:18:49 +02:00
penev92
d955efff14 Updated configure-system-libraries for new OpenAL 2023-06-11 10:41:02 +02:00
penev92
3fdee06dc7 Updated other referenced NuGet packages 2023-06-11 10:41:02 +02:00
penev92
c0cd7259b3 Updated native dependencies NuGet packages 2023-06-11 10:41:02 +02:00
Matthias Mailänder
c31f2abfc9 Add sanity checks to the Lua script trait. 2023-06-11 11:03:38 +03:00
RoosterDragon
06df75ffee Improve PNG parsing performance.
Switch on the filter once per row rather than once per byte. This allows each row to be processed with a much tighter loop.
2023-06-10 16:20:02 +02:00
Matthias Mailänder
855e839b77 Try to fix chocolatey. 2023-06-09 19:25:36 +02:00
Gustas
06437df9b0 Fix CA1852 2023-06-06 14:13:04 +03:00
RoosterDragon
f4af5c1764 Fix CA1852 2023-06-06 11:51:47 +03:00
RoosterDragon
277699cbd5 Fix CA1822 2023-06-06 11:51:47 +03:00
Gustas
e4cac1fffc Add more varied pre-selected colors to player colors palette 2023-06-05 13:16:26 +02:00
Matthias Mailänder
e164e48aae Fix invalid integer expression syntax. 2023-06-04 21:29:15 +03:00
Matthias Mailänder
5eec9d29cb Add a lint check for invalid integer expression syntax. 2023-06-04 21:29:15 +03:00
Matthias Mailänder
94abd8a928 Revert "Revert "Replace legacy Evaluator with IntegerExpressions.""
This reverts commit 4f16b0d464.
2023-06-04 21:29:15 +03:00
JovialFeline
b18c2fe855 Add Mousetrap (scu33ea). 2023-06-03 21:45:18 +02:00
Gustas
02a7ff87db Fix MoveAdjacent activities cancelling queued activities 2023-06-03 13:43:44 +02:00
Vapre
7c0f6ead3a ActorMap, avoid IPositionable trait lookup. 2023-06-03 13:41:14 +02:00
Vapre
d72b1ffd49 ActorMap, do not look up influence node up to three times in cell layer. 2023-06-03 13:06:38 +02:00
Gustas
c82be175e1 Add RemoveNegativeSequenceLength update rule 2023-06-02 11:59:22 +02:00
michaeldgg2
dac35a60ad WithDecoration: fixed crash when Palette is null 2023-06-02 11:49:59 +02:00
abcdefg30
ee02af3605 Don't play low power notifications in the beginning of twist-of-fate 2023-06-01 20:42:39 +03:00
abcdefg30
74ed202b29 Support enabling and disabling the low power notification via Lua 2023-06-01 20:42:39 +03:00
RoosterDragon
300281695a Deserialize mod rules only once when loading all maps.
This avoids loading, parsing and merging YAML rules for the mod during loading of each individual map. This saves significant time resolving custom rules on each map loaded.
2023-06-01 12:59:53 +02:00
Matthias Mailänder
de22556153 Move unit testing into the make script. 2023-06-01 12:43:10 +02:00
Matthias Mailänder
5bcb1a678c Fix fluent plural forms. 2023-06-01 12:43:10 +02:00
Matthias Mailänder
8a9426a0d4 Add a test case for Fluent plural forms. 2023-06-01 12:43:10 +02:00
Matthias Mailänder
dd9ab16401 Run unit tests on Linux. 2023-06-01 12:43:10 +02:00
abcdefg30
12e6932930 Fix dedicated servers crashing on startup due to missing translations 2023-06-01 10:04:06 +03:00
abcdefg30
95f18d4bc3 Fix the ExplicitSequenceFilenames updating sequences twice 2023-05-30 16:31:49 +03:00
abcdefg30
8c9cc93185 Fix the ExplicitSequenceFilenames rule breaking when updating single maps 2023-05-30 16:31:49 +03:00
Matthias Mailänder
b58a8aaa0f Fix benchmark CSV export on non-US systems. 2023-05-28 23:11:25 +03:00
Gustas
c5b7728ac9 RA Naval balance 2023-05-25 23:35:34 +02:00
Matthias Mailänder
c9dddc342c Extract editor brush texts. 2023-05-23 19:45:15 +02:00
Matthias Mailänder
8433bc0948 Throw early when Lua function parameters are null. 2023-05-23 17:30:03 +03:00
abcdefg30
52a916012f Remove references to the deleted Lua directory from Windows packaging 2023-05-21 18:23:04 +03:00
RoosterDragon
2fe7e1bff9 Fix HierarchicalPathFinder pathing from inaccessible source locations.
When a search is initiated from an inaccessible source location, a path is still allowed if there is an adjacent, accessible location the unit can move into. The local pathfinder and HierarchicalPathFinder already account for this logic, but HPF has some bugs.

Firstly, when the HierarchicalPathFinder heuristic is being evaluated, it assumes all cells being explored are accessible - this is important for performance as it avoids constantly rechecking the accessibility of cells. Although this fact holds true for cells explored by the path search, it does not hold true for cells being added as the initial starting points of the search.

Secondly, when checking for adjacent locations to an inaccessible source cell, we checked only if these were still on the map. This is insufficient - we need to know if movement between the source cell and the adjacent cell is possible.

The fixes resolve this by:
- Teaching the heuristic an extra parameter to know if the location is known to be accessible. This allow an accessibility check to be performed for starting locations which stops HPF mistakenly assuming the abstract node for that location is the one we need to consider, and to correctly check the adjacent locations and their abstract nodes instead. The parameter means will can still skip the accessibility check in the typical case where the path search is being expanded, preserving performance.
- When adjacent cells are considered we now check if movement to them is possible. This stops HPF from allowing jumps over height discontinuities (i.e. no magically jumping up or down cliffs) and thinking a path is possible when it is not.
2023-05-21 16:56:16 +02:00
abcdefg30
0788e5ff3e Adjust the ValidTargets of DuplicateUnitCrateAction in RA 2023-05-21 16:34:58 +02:00
Matthias Mailänder
0fb5853b7a Fallback to normal difficulty when none is set. 2023-05-21 15:46:20 +03:00
Matthias Mailänder
b30285e38d Officially deprecate this function. 2023-05-21 15:46:20 +03:00
Brenton Horne
02e4bfba95 Remove reference to deleted lua directory
Lua directory was deleted a few commits ago and this just updates packaging/functions.sh to reflect this change.
2023-05-21 13:13:13 +02:00
abcdefg30
b5f5d5f9d5 Fix ScriptContext crashing without a WorldLoaded function 2023-05-21 13:11:17 +02:00
abcdefg30
f2b3a9f837 Fix ScriptContext crashing without a Tick function 2023-05-21 13:11:17 +02:00
dnqbob
6af14c16c9 Fix a rare crash when actor in IBotRespondToAttack is dead 2023-05-21 10:40:21 +02:00
Gustas
211f7160dc Remove \r
we use \n everywhere else in the engine
2023-05-20 20:47:56 +03:00
Smittytron
8a1463a471 Add Allies10b 2023-05-20 20:36:54 +03:00
abcdefg30
b623214e04 Fix IdlingUnits definitions for multiple reinforcements in D2k missions 2023-05-20 18:56:58 +02:00
abcdefg30
6b536ca88a Add a Lua function to concat two tables 2023-05-20 18:56:58 +02:00
Matthias Mailänder
445b736885 Replace sandbox wrapper scripts. 2023-05-20 13:19:48 +02:00
dnqbob
e8dd85419f add Autocrusher for baby visc 2023-05-20 12:53:29 +02:00
yamismo
b1fd392486 Add GDI Covert Operations - Twist of Fate - scg41ea 2023-05-19 18:55:17 +02:00
Gustas
e487c3366d Allow EMP cannon to EMP itself 2023-05-19 17:29:17 +02:00
Gustas
ade27ad8b9 Fail CI on lint warnings 2023-05-19 17:25:03 +02:00
Gustas
34bcae9abb Translation keys should not be required 2023-05-19 17:25:03 +02:00
Gustas
8894fdeaf9 Add map translation parse error linting 2023-05-19 17:25:03 +02:00
Gustas
ff14b75e1b Fix duplicate translations 2023-05-19 17:25:03 +02:00
Gustas
b5ef9c29cf Improve spawnpoint lint 2023-05-19 17:25:03 +02:00
Gustas
f344ccb714 Improve visibility lint 2023-05-19 17:25:03 +02:00
Gustas
9b71317280 Improve lint error wording 2023-05-19 17:25:03 +02:00
Gustas
1ac6912c2a Fix lint error formatting 2023-05-19 17:25:03 +02:00
Gustas
3188532e59 Add punctuation to lint comments 2023-05-19 17:25:03 +02:00
abcdefg30
d9d8c23c63 Enable the restart button when we encounter a script error 2023-05-19 17:02:06 +02:00
abcdefg30
dfe0c15399 Remove wrong definitions from TD's notifications.yaml 2023-05-19 16:39:42 +02:00
IceReaper
0751b30d33 Fixed duplicate asset selection in asset browser. 2023-05-19 10:26:32 +03:00
Gustas
f4dc29f9db Add missing balance changes 2023-05-18 18:30:53 -05:00
abcdefg30
ce7f9e71c1 DefaultSpriteSequence: Avoid an extra allocation and LINQ when adding shadow frames 2023-05-16 22:57:33 +03:00
abcdefg30
05f21fcbe2 DefaultSpriteSequence: Merge if statements for readability 2023-05-16 22:57:33 +03:00
abcdefg30
00f2ba1a53 DefaultSpriteSequence: Remove redundant modulo operations
frame is already bounded by length.Value
2023-05-16 22:57:33 +03:00
abcdefg30
991e0a4c9a DefaultSpriteSequence: Use .Count == 0 over !.Any() 2023-05-16 22:57:33 +03:00
abcdefg30
aa28881726 DefaultSpriteSequence: Use List.Find instead of LINQ's FirstOrDefault 2023-05-16 22:57:33 +03:00
RoosterDragon
6d288aba2f Track keys during MiniYaml Merge.
This improves performance by avoiding repeated linear scans over the nodes to match the keys.
2023-05-15 23:48:39 +02:00
dnqbob
0980856072 Set up Hover MRLS moving style 2023-05-11 17:27:18 +02:00
dnqbob
0d98405bdc Make ActorFacingModifier privately set 2023-05-11 17:27:18 +02:00
dnqbob
69441a4fee Add TurnsWhileMoving to Mobile 2023-05-11 17:27:18 +02:00
dnqbob
a65bb17d68 Cache the notifyAttacks in AttackGarrisoned 2023-05-09 22:49:41 +02:00
Matthias Mailänder
6e6bf1ca81 Translate labels with parameters. 2023-05-09 20:14:52 +03:00
Matthias Mailänder
474463111f Remove unused CamelCase UI string. 2023-05-09 20:14:52 +03:00
RoosterDragon
0b4a54ab54 TechTree.GatherOwnedPrerequisites performance improvements.
- Consuming methods cared only about the count and not the actual actors, so only counts the actors rather that creating lists.
- ProvidesPrerequisites implementations return cached objects rather then allocating new enumerables on each call.
2023-05-06 20:07:35 +03:00
Matthias Mailänder
65c0cf1065 Deprecate string format shorthand. 2023-05-05 19:03:09 +02:00
Matthias Mailänder
1c2ce0dcc0 Deprecate string format log shorthand. 2023-05-05 19:03:09 +02:00
Matthias Mailänder
e251126dd4 Remove unused log proxy. 2023-05-05 19:03:09 +02:00
Matthias Mailänder
f2a4e7b984 Remove null sprite workaround from Tiberian Sun. 2023-05-04 22:13:59 +03:00
Matthias Mailänder
9e659cacf2 Fix rally point sprites not being truely optional. 2023-05-04 22:13:59 +03:00
Matthias Mailänder
c5e9567875 Fix a null reference exception upon empty Fluent strings. 2023-05-02 22:25:51 +02:00
Gustas
44f1af7059 Move TileScale to MapGrid 2023-05-02 16:37:30 +03:00
Nate Nichols
8f511a3bb6 Added ability to use Mouse 4 and Mouse 5 as hotkeys 2023-04-28 15:22:25 +03:00
Matthias Mailänder
4f7a01a291 Localize difficulty settings. 2023-04-25 21:33:02 +03:00
Matthias Mailänder
af6330b1bd Allow localisation of dictionary values. 2023-04-25 21:33:02 +03:00
Matthias Mailänder
55ff0ac1f4 Inline variables. 2023-04-25 21:33:02 +03:00
abcdefg30
51bbfc39b0 Fix DischargeableSupportPowerInstance crashing without instances 2023-04-25 21:15:06 +03:00
JovialFeline
90bb2db349 Fix OnAllKilled crash for allies05b and allies05c 2023-04-23 23:49:10 +02:00
Matthias Mailänder
0d36bc19c6 Document radar appear trait and fields. 2023-04-22 23:24:24 +02:00
Gustas
a9a7777293 Report linguini parse errors
Reports duplicate keys
2023-04-22 19:23:41 +02:00
Gustas
bf66068557 Add per map linting 2023-04-22 19:23:41 +02:00
Matthias Mailänder
68eec52cef Add TranslationProvider 2023-04-22 19:23:41 +02:00
Gustas
a065e6a47c Fix map level lobby options not being translated 2023-04-22 19:23:41 +02:00
Gustas
efe135e38b Add Translations to MapPreview 2023-04-22 19:23:41 +02:00
Gustas
07e47b6a28 Extract common rules translations 2023-04-22 19:23:41 +02:00
Gustas
8f5d8de1c2 Allow empty translation keys 2023-04-22 19:23:41 +02:00
Matthias Mailänder
2867334c00 Less verbose logging for untranslated strings. 2023-04-22 19:23:41 +02:00
Gustas
dc390a7301 Add IMove.MoveOntoTarget interface
In `TraitsInterfaces` we expose offset as WPos instead of CPos. In an upcoming PR we'll translate the same change to yaml.
2023-04-21 18:29:43 +02:00
Gustas
ad683d9226 Add MoveOnto Activity
No functional changes to `MoveWithinRange` nor `MoveAdjacentTo`. I've just
moved around code to for allow better overwriting.
2023-04-21 18:29:43 +02:00
Gustas
1c2eaa2654 Use nameof for ContrailEndColor description 2023-04-21 17:58:42 +02:00
Gustas
7ef1dccdcf Add ContrailEndWidth 2023-04-21 17:58:42 +02:00
Gustas
ff488b77b5 Implement elite radar invisibility 2023-04-21 16:59:11 +02:00
Gustas
92cceea2b8 Enemy veterancy should always be visible 2023-04-21 16:59:11 +02:00
yamismo
8b522680e3 Added Nod Covert Operations - Eviction Notice - scb30ea 2023-04-19 21:52:30 -05:00
RoosterDragon
7507333cd3 Fix missing Flags attribute on CABFlags enum. 2023-04-17 00:05:12 +02:00
RoosterDragon
f470f9ab91 Fix CA2216 2023-04-17 00:05:12 +02:00
RoosterDragon
a120b9d37e Fix CA2208 2023-04-17 00:05:12 +02:00
RoosterDragon
1b1b9dc29b Fix CA2215 2023-04-17 00:05:12 +02:00
RoosterDragon
ef04e2e1e8 Fix CA2019 2023-04-17 00:05:12 +02:00
RoosterDragon
321d4b8afd Fix CA1850 2023-04-17 00:05:12 +02:00
RoosterDragon
c3e6c4685f Fix CA1849 2023-04-17 00:05:12 +02:00
RoosterDragon
6362bbd176 Fix CA1846 2023-04-17 00:05:12 +02:00
RoosterDragon
07fb5e8027 Fix CA1841 2023-04-17 00:05:12 +02:00
RoosterDragon
8fe82ed976 Fix CA1839 2023-04-17 00:05:12 +02:00
RoosterDragon
25b8e7fefc Fix CA1834 2023-04-17 00:05:12 +02:00
RoosterDragon
ad4a443fc2 Fix CA1066 2023-04-17 00:05:12 +02:00
RoosterDragon
01eaa6b228 Fix CA1064 2023-04-17 00:05:12 +02:00
RoosterDragon
c442bd83f8 Fix CA1036 2023-04-17 00:05:12 +02:00
RoosterDragon
ff799303b0 Fix CA1018 2023-04-17 00:05:12 +02:00
RoosterDragon
f09241d263 Fix CA1010 2023-04-17 00:05:12 +02:00
Gustas
1db982276a Fix actors being added to world while world actors are being iterated 2023-04-15 17:26:02 +02:00
Paul Chote
718c6d03cc Downgrade SDL to 2.0.22. 2023-04-15 16:14:12 +02:00
Thomas Christlieb
57fba4e18e Rename JoinChatDelay 2023-04-15 12:50:06 +02:00
ThomasChr
b219731173 Allow overriding of Server.Map via the dedicated server scripts 2023-04-15 12:50:06 +02:00
ThomasChr
a0eea7bcc0 fix path to wiki page 2023-04-15 12:50:06 +02:00
RoosterDragon
0c3071b9c6 Adjust MiniYaml node merging when removals are present.
# Example Scenario
MockString:
	CollectionOfStrings:
		StringA: A

MockString:
	CollectionOfStrings:
		StringB: B

MockString:
	-CollectionOfStrings:

MockString:
	CollectionOfStrings:
		StringC: C

MockString:
	CollectionOfStrings:
		StringD: D

# Previous MergePartial result
# The CollectionOfStrings is merged into a single unit, so the C and D items are dragged upwards and jump ahead of the Removal
# When this is processed, the final result removes CollectionOfStrings entirely

MockString:
	CollectionOfStrings:
		StringA: A
		StringB: B
		StringC: C
		StringD: D
	-CollectionOfStrings:

# New MergePartial result
# When merging nodes, we no longer allow merges to jump an intervening removal node.
# This means we can have multiple of a certain key (CollectionOfStrings in this example) which was not the case previously.
# When this is processed, the final result includes C/D but not A/B.

MockString:
	CollectionOfStrings:
		StringA: A
		StringB: B
	-CollectionOfStrings:
	CollectionOfStrings:
		StringC: C
		StringD: D
2023-04-14 23:57:26 +03:00
RoosterDragon
0066010792 Added MiniYAML merging unit tests
MockString:
	CollectionOfStrings:
		StringA: A

MockString:
	CollectionOfStrings:
		StringB: B

MockString:
	-CollectionOfStrings:

MockString:
	CollectionOfStrings:
		StringC: C

MockString:
	CollectionOfStrings:
		StringD: D

MockString:
	CollectionOfStrings:
		StringA: A
		StringB: B
		StringC: C
		StringD: D
	-CollectionOfStrings:

MockString:
	CollectionOfStrings:
		StringA: A
		StringB: B
	-CollectionOfStrings:
	CollectionOfStrings:
		StringC: C
		StringD: D
2023-04-14 23:57:26 +03:00
penev92
0b61954e39 Reimported some TS maps
Could not find the source for most of the maps, so reimported only those that were part of the original game's "official map pool" + 3 custom ones.
2023-04-14 20:50:08 +03:00
Paul Chote
a0cd008da6 Fixed gen2 map importer Bounds calculations 2023-04-14 20:50:08 +03:00
penev92
3d2ba9d5bf Added abstract base class ImportGen2MapCommand
Split out ImportGen2MapCommand from ImportTSMapCommand, to also serve as a base for a RA2 map importer.
Also renamed TS importer to ImportTiberianSunMapCommand.
2023-04-14 20:50:08 +03:00
penev92
01e6babd54 Moved ImportLegacyMapCommand to OpenRA.Mods.Cnc
Also renamed to `ImportGen1MapCommand`.
Also moved Extensions.DistinctBy().
2023-04-14 20:50:08 +03:00
2166 changed files with 59568 additions and 31051 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -15,22 +15,23 @@ jobs:
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.0.x'
- name: Check Code
run: |
make check
make tests
- name: Check Mods
run: |
sudo apt-get install lua5.1
make check-scripts
make test
make TREAT_WARNINGS_AS_ERRORS=true test
linux-mono:
name: Linux (mono)
@@ -38,7 +39,7 @@ jobs:
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Check Code
run: |
@@ -48,7 +49,7 @@ jobs:
- name: Check Mods
run: |
# check-scripts does not depend on .net/mono, so is not needed here
make RUNTIME=mono test
make RUNTIME=mono TREAT_WARNINGS_AS_ERRORS=true test
windows:
name: Windows (.NET 6.0)
@@ -56,10 +57,10 @@ jobs:
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.0.x'
@@ -69,12 +70,12 @@ jobs:
# Work around runtime failures on the GH Actions runner
dotnet nuget add source https://api.nuget.org/v3/index.json -n nuget.org
.\make.ps1 check
dotnet build OpenRA.Test\OpenRA.Test.csproj -c Debug --nologo -p:TargetPlatform=win-x64
dotnet test bin\OpenRA.Test.dll --test-adapter-path:.
.\make.ps1 tests
- name: Check Mods
run: |
chocolatey install lua --version 5.1.5.52
choco install lua --version 5.1.5.52
$ENV:Path = $ENV:Path + ";C:\Program Files (x86)\Lua\5.1\"
$ENV:TREAT_WARNINGS_AS_ERRORS = "true"
.\make.ps1 check-scripts
.\make.ps1 test

View File

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

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -40,10 +40,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.0.x'
@@ -70,10 +70,10 @@ jobs:
runs-on: macos-11
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.0.x'
@@ -105,10 +105,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.0.x'

View File

@@ -120,6 +120,7 @@ Also thanks to:
* Mike Gagné (AngryBirdz)
* Muh
* Mustafa Alperen Seki (MustaphaTR)
* Nathan Nichols (cracksmoka420)
* Neil Shivkar (havok13888)
* Nikolay Fomin (netnazgul)
* Nooze
@@ -149,6 +150,7 @@ Also thanks to:
* Teemu Nieminen (Temeez)
* Thomas Christlieb (ThomasChr)
* Tim Mylemans (gecko)
* Tinix
* Tirili
* Tomas Einarsson (Mesacer)
* Tom van Leth (tovl)

View File

@@ -13,6 +13,8 @@
<ExternalConsole>false</ExternalConsole>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<Nullable>disable</Nullable>
<Product>OpenRA</Product>
<Copyright>Copyright (c) The OpenRA Developers and Contributors</Copyright>
</PropertyGroup>
<PropertyGroup>
@@ -31,7 +33,7 @@
<Optimize>false</Optimize>
<!-- Enable only for Debug builds to improve compile-time performance for Release builds -->
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<!-- Enabling GenerateDocumentationFile is required for IDE0005 (Remove unnecessary import)
<!-- Enabling GenerateDocumentationFile is required for IDE0005 (Remove unnecessary import)
rule to run in command line builds. https://github.com/dotnet/roslyn/issues/41640
Enable only for Debug builds to improve compile-time performance for Release builds -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -49,8 +51,10 @@
</ItemGroup>
</Target>
<!-- StyleCop -->
<!-- StyleCop/Roslynator -->
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
<!-- Roslynator analyzers fail to run under Mono (AD0001) -->
<PackageReference Include="Roslynator.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
</ItemGroup>
</Project>

View File

@@ -130,7 +130,7 @@ endif
check-scripts:
@echo
@echo "Checking for Lua syntax errors..."
@find lua/ mods/*/{maps,scripts}/ -iname "*.lua" -print0 | xargs -0n1 luac -p
@find mods/*/maps/ mods/*/scripts/ -iname "*.lua" -print0 | xargs -0n1 luac -p
test: all
@echo
@@ -146,6 +146,11 @@ test: all
@echo "Testing Red Alert mod MiniYAML..."
@./utility.sh ra --check-yaml
tests:
@dotnet build OpenRA.Test/OpenRA.Test.csproj -c Debug --nologo -p:TargetPlatform=$(TARGETPLATFORM)
@echo
@dotnet test bin/OpenRA.Test.dll --test-adapter-path:.
############# LOCAL INSTALLATION AND DOWNSTREAM PACKAGING ##############
#
version: VERSION mods/ra/mod.yaml mods/cnc/mod.yaml mods/d2k/mod.yaml mods/ts/mod.yaml mods/modcontent/mod.yaml mods/all/mod.yaml
@@ -180,7 +185,7 @@ help:
@echo ' make [RUNTIME=net6] TARGETPLATFORM=unix-generic'
@echo
@echo 'to check the official mods for erroneous yaml files, run:'
@echo ' make [RUNTIME=net6] test'
@echo ' make [RUNTIME=net6] [TREAT_WARNINGS_AS_ERRORS=false] test'
@echo
@echo 'to check the engine and official mod dlls for code style violations, run:'
@echo ' make [RUNTIME=net6] check'

View File

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

View File

@@ -35,6 +35,9 @@ namespace OpenRA
public sealed class Actor : IScriptBindable, IScriptNotifyBind, ILuaTableBinding, ILuaEqualityBinding, ILuaToStringBinding, IEquatable<Actor>, IDisposable
{
/// <summary>Value used to represent an invalid token.</summary>
public const int InvalidConditionToken = -1;
internal readonly struct SyncHash
{
public readonly ISync Trait;
@@ -68,7 +71,12 @@ namespace OpenRA
public IEffectiveOwner EffectiveOwner { get; }
public IOccupySpace OccupiesSpace { get; }
public ITargetable[] Targetables { get; }
public IEnumerable<ITargetablePositions> EnabledTargetablePositions { get; private set; }
public IEnumerable<ITargetablePositions> EnabledTargetablePositions { get; }
readonly ICrushable[] crushables;
public ICrushable[] Crushables
{
get => crushables ?? throw new InvalidOperationException($"Crushables for {Info.Name} are not initialized.");
}
public bool IsIdle => CurrentActivity == null;
public bool IsDead => Disposed || (health != null && health.IsDead);
@@ -78,10 +86,7 @@ namespace OpenRA
public WRot Orientation => facing?.Orientation ?? WRot.None;
/// <summary>Value used to represent an invalid token.</summary>
public static readonly int InvalidConditionToken = -1;
class ConditionState
sealed class ConditionState
{
/// <summary>Delegates that have registered to be notified when this condition changes.</summary>
public readonly List<VariableObserverNotifier> Notifiers = new();
@@ -155,6 +160,7 @@ namespace OpenRA
var targetablesList = new List<ITargetable>();
var targetablePositionsList = new List<ITargetablePositions>();
var syncHashesList = new List<SyncHash>();
var crushablesList = new List<ICrushable>();
foreach (var traitInfo in Info.TraitsInConstructOrder())
{
@@ -181,6 +187,7 @@ namespace OpenRA
{ if (trait is ITargetable t) targetablesList.Add(t); }
{ if (trait is ITargetablePositions t) targetablePositionsList.Add(t); }
{ if (trait is ISync t) syncHashesList.Add(new SyncHash(t)); }
{ if (trait is ICrushable t) crushablesList.Add(t); }
}
resolveOrders = resolveOrdersList.ToArray();
@@ -195,6 +202,7 @@ namespace OpenRA
EnabledTargetablePositions = targetablePositions.Where(Exts.IsTraitEnabled);
enabledTargetableWorldPositions = EnabledTargetablePositions.SelectMany(tp => tp.TargetablePositions(this));
SyncHashes = syncHashesList.ToArray();
crushables = crushablesList.ToArray();
}
}

View File

@@ -61,14 +61,14 @@ namespace OpenRA
public static readonly CVec[] Directions =
{
new CVec(-1, -1),
new CVec(-1, 0),
new CVec(-1, 1),
new CVec(0, -1),
new CVec(0, 1),
new CVec(1, -1),
new CVec(1, 0),
new CVec(1, 1),
new(-1, -1),
new(-1, 0),
new(-1, 1),
new(0, -1),
new(0, 1),
new(1, -1),
new(1, 0),
new(1, 1),
};
#region Scripting interface

View File

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

View File

@@ -66,6 +66,7 @@ namespace OpenRA
// Several types of support directory types are available, depending on
// how the player has installed and launched the game.
// Read registration metadata from all of them
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
foreach (var source in GetSupportDirs(ModRegistration.User | ModRegistration.System))
{
var metadataPath = Path.Combine(source, "ModMetadata");
@@ -76,13 +77,13 @@ namespace OpenRA
{
try
{
var yaml = MiniYaml.FromStream(File.OpenRead(path), path).First().Value;
var yaml = MiniYaml.FromFile(path, stringPool: stringPool).First().Value;
LoadMod(yaml, path);
}
catch (Exception e)
{
Log.Write("debug", "Failed to parse mod metadata file '{0}'", path);
Log.Write("debug", e.ToString());
Log.Write("debug", $"Failed to parse mod metadata file '{path}'");
Log.Write("debug", e);
}
}
}
@@ -94,17 +95,17 @@ namespace OpenRA
if (sheetBuilder != null)
{
var iconNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon");
var iconNode = yaml.NodeWithKeyOrDefault("Icon");
if (iconNode != null && !string.IsNullOrEmpty(iconNode.Value.Value))
using (var stream = new MemoryStream(Convert.FromBase64String(iconNode.Value.Value)))
mod.Icon = sheetBuilder.Add(new Png(stream));
var icon2xNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon2x");
var icon2xNode = yaml.NodeWithKeyOrDefault("Icon2x");
if (icon2xNode != null && !string.IsNullOrEmpty(icon2xNode.Value.Value))
using (var stream = new MemoryStream(Convert.FromBase64String(icon2xNode.Value.Value)))
mod.Icon2x = sheetBuilder.Add(new Png(stream), 1f / 2);
var icon3xNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon3x");
var icon3xNode = yaml.NodeWithKeyOrDefault("Icon3x");
if (icon3xNode != null && !string.IsNullOrEmpty(icon3xNode.Value.Value))
using (var stream = new MemoryStream(Convert.FromBase64String(icon3xNode.Value.Value)))
mod.Icon3x = sheetBuilder.Add(new Png(stream), 1f / 3);
@@ -122,7 +123,7 @@ namespace OpenRA
return;
var key = ExternalMod.MakeKey(mod);
var yaml = new MiniYamlNode("Registration", new MiniYaml("", new List<MiniYamlNode>()
var yaml = new MiniYamlNode("Registration", new MiniYaml("", new[]
{
new MiniYamlNode("Id", mod.Id),
new MiniYamlNode("Version", mod.Metadata.Version),
@@ -131,17 +132,21 @@ namespace OpenRA
new MiniYamlNode("LaunchArgs", new[] { "Game.Mod=" + mod.Id }.Concat(launchArgs).JoinWith(", "))
}));
var iconNodes = new List<MiniYamlNode>();
using (var stream = mod.Package.GetStream("icon.png"))
if (stream != null)
yaml.Value.Nodes.Add(new MiniYamlNode("Icon", Convert.ToBase64String(stream.ReadAllBytes())));
iconNodes.Add(new MiniYamlNode("Icon", Convert.ToBase64String(stream.ReadAllBytes())));
using (var stream = mod.Package.GetStream("icon-2x.png"))
if (stream != null)
yaml.Value.Nodes.Add(new MiniYamlNode("Icon2x", Convert.ToBase64String(stream.ReadAllBytes())));
iconNodes.Add(new MiniYamlNode("Icon2x", Convert.ToBase64String(stream.ReadAllBytes())));
using (var stream = mod.Package.GetStream("icon-3x.png"))
if (stream != null)
yaml.Value.Nodes.Add(new MiniYamlNode("Icon3x", Convert.ToBase64String(stream.ReadAllBytes())));
iconNodes.Add(new MiniYamlNode("Icon3x", Convert.ToBase64String(stream.ReadAllBytes())));
yaml = yaml.WithValue(yaml.Value.WithNodesAppended(iconNodes));
var sources = new HashSet<string>();
if (registration.HasFlag(ModRegistration.System))
@@ -174,7 +179,7 @@ namespace OpenRA
catch (Exception e)
{
Log.Write("debug", "Failed to register current mod metadata");
Log.Write("debug", e.ToString());
Log.Write("debug", e);
}
}
}
@@ -201,7 +206,7 @@ namespace OpenRA
string modKey = null;
try
{
var yaml = MiniYaml.FromStream(File.OpenRead(path), path).First().Value;
var yaml = MiniYaml.FromFile(path).First().Value;
var m = FieldLoader.Load<ExternalMod>(yaml);
modKey = ExternalMod.MakeKey(m);
@@ -213,8 +218,8 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to parse mod metadata file '{0}'", path);
Log.Write("debug", e.ToString());
Log.Write("debug", $"Failed to parse mod metadata file '{path}'");
Log.Write("debug", e);
}
// Remove from the ingame mod switcher
@@ -225,12 +230,12 @@ namespace OpenRA
try
{
File.Delete(path);
Log.Write("debug", "Removed invalid mod metadata file '{0}'", path);
Log.Write("debug", $"Removed invalid mod metadata file '{path}'");
}
catch (Exception e)
{
Log.Write("debug", "Failed to remove mod metadata file '{0}'", path);
Log.Write("debug", e.ToString());
Log.Write("debug", $"Failed to remove mod metadata file '{path}'");
Log.Write("debug", e);
}
}
}
@@ -251,13 +256,13 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to remove mod metadata file '{0}'", path);
Log.Write("debug", e.ToString());
Log.Write("debug", $"Failed to remove mod metadata file '{path}'");
Log.Write("debug", e);
}
}
}
IEnumerable<string> GetSupportDirs(ModRegistration registration)
static IEnumerable<string> GetSupportDirs(ModRegistration registration)
{
var sources = new HashSet<string>(4);
if (registration.HasFlag(ModRegistration.System))

View File

@@ -22,20 +22,14 @@ namespace OpenRA
{
public static class Exts
{
public static bool IsUppercase(this string str)
public static string FormatInvariant(this string format, params object[] args)
{
return string.Compare(str.ToUpperInvariant(), str, false) == 0;
return string.Format(CultureInfo.InvariantCulture, format, args);
}
public static string F(this string format, params object[] args)
public static string FormatCurrent(this string format, params object[] args)
{
return string.Format(format, args);
}
public static T WithDefault<T>(T def, Func<T> f)
{
try { return f(); }
catch { return def; }
return string.Format(CultureInfo.CurrentCulture, format, args);
}
public static Lazy<T> Lazy<T>(Func<T> p) { return new Lazy<T>(p); }
@@ -114,23 +108,57 @@ namespace OpenRA
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, V v)
{
#if NET5_0_OR_GREATER
// SAFETY: Dictionary cannot be modified whilst the ref is alive.
ref var value = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(d, k, out var exists);
if (!exists)
value = v;
return value;
#else
if (!d.TryGetValue(k, out var ret))
d.Add(k, ret = v);
return ret;
#endif
}
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, Func<K, V> createFn)
{
// Cannot use CollectionsMarshal.GetValueRefOrAddDefault here,
// the creation function could mutate the dictionary which would invalidate the ref.
if (!d.TryGetValue(k, out var ret))
d.Add(k, ret = createFn(k));
return ret;
}
public static T GetOrAdd<T>(this HashSet<T> set, T value)
{
if (!set.TryGetValue(value, out var ret))
set.Add(ret = value);
return ret;
}
public static T GetOrAdd<T>(this HashSet<T> set, T value, Func<T, T> createFn)
{
if (!set.TryGetValue(value, out var ret))
set.Add(ret = createFn(value));
return ret;
}
public static int IndexOf<T>(this T[] array, T value)
{
return Array.IndexOf(array, value);
}
public static T FirstOrDefault<T>(this T[] array, Predicate<T> match)
{
return Array.Find(array, match);
}
public static T FirstOrDefault<T>(this List<T> list, Predicate<T> match)
{
return list.Find(match);
}
public static T Random<T>(this IEnumerable<T> ts, MersenneTwister r)
{
return Random(ts, r, true);
@@ -143,7 +171,7 @@ namespace OpenRA
static T Random<T>(IEnumerable<T> ts, MersenneTwister r, bool throws)
{
var xs = ts as ICollection<T>;
var xs = ts as IReadOnlyCollection<T>;
xs ??= ts.ToList();
if (xs.Count == 0)
{
@@ -298,9 +326,9 @@ namespace OpenRA
// Adjust for other rounding modes
if (round == ISqrtRoundMode.Nearest && remainder > root)
root += 1;
root++;
else if (round == ISqrtRoundMode.Ceiling && root * root < number)
root += 1;
root++;
return root;
}
@@ -339,9 +367,9 @@ namespace OpenRA
// Adjust for other rounding modes
if (round == ISqrtRoundMode.Nearest && remainder > root)
root += 1;
root++;
else if (round == ISqrtRoundMode.Ceiling && root * root < number)
root += 1;
root++;
return root;
}
@@ -351,6 +379,11 @@ namespace OpenRA
return number * 46341 / 32768;
}
public static int MultiplyBySqrtTwoOverTwo(int number)
{
return (int)(number * 23170L / 32768L);
}
public static int IntegerDivisionRoundingAwayFromZero(int dividend, int divisor)
{
var quotient = Math.DivRem(dividend, divisor, out var remainder);
@@ -488,17 +521,22 @@ namespace OpenRA
return result;
}
public static int ParseIntegerInvariant(string s)
{
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static byte ParseByte(string s)
public static byte ParseByteInvariant(string s)
{
return byte.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static bool TryParseIntegerInvariant(string s, out int i)
public static short ParseInt16Invariant(string s)
{
return short.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static int ParseInt32Invariant(string s)
{
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static bool TryParseInt32Invariant(string s, out int i)
{
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
@@ -508,6 +546,26 @@ namespace OpenRA
return long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
public static string ToStringInvariant(this byte i)
{
return i.ToString(NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this byte i, string format)
{
return i.ToString(format, NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this int i)
{
return i.ToString(NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this int i, string format)
{
return i.ToString(format, NumberFormatInfo.InvariantInfo);
}
public static bool IsTraitEnabled<T>(this T trait)
{
return trait is not IDisabledTrait disabledTrait || !disabledTrait.IsTraitDisabled;
@@ -571,7 +629,7 @@ namespace OpenRA
Current = default;
}
public LineSplitEnumerator GetEnumerator() => this;
public readonly LineSplitEnumerator GetEnumerator() => this;
public bool MoveNext()
{

View File

@@ -87,6 +87,7 @@ namespace OpenRA
{ typeof(WAngle), ParseWAngle },
{ typeof(WRot), ParseWRot },
{ typeof(CPos), ParseCPos },
{ typeof(CPos[]), ParseCPosArray },
{ typeof(CVec), ParseCVec },
{ typeof(CVec[]), ParseCVecArray },
{ typeof(BooleanExpression), ParseBooleanExpression },
@@ -112,10 +113,19 @@ namespace OpenRA
{ typeof(Nullable<>), ParseNullable },
};
static readonly object BoxedTrue = true;
static readonly object BoxedFalse = false;
static readonly object[] BoxedInts = Exts.MakeArray(33, i => (object)i);
static object ParseInt(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (Exts.TryParseIntegerInvariant(value, out var res))
if (Exts.TryParseInt32Invariant(value, out var res))
{
if (res >= 0 && res < BoxedInts.Length)
return BoxedInts[res];
return res;
}
return InvalidValueAction(value, fieldType, fieldName);
}
@@ -186,11 +196,11 @@ namespace OpenRA
if (value != null)
{
var parts = value.Split(SplitComma);
if (parts.Length == 3)
{
if (WDist.TryParse(parts[0], out var rx) && WDist.TryParse(parts[1], out var ry) && WDist.TryParse(parts[2], out var rz))
return new WVec(rx, ry, rz);
}
if (parts.Length == 3
&& WDist.TryParse(parts[0], out var rx)
&& WDist.TryParse(parts[1], out var ry)
&& WDist.TryParse(parts[2], out var rz))
return new WVec(rx, ry, rz);
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -210,8 +220,8 @@ namespace OpenRA
for (var i = 0; i < vecs.Length; ++i)
{
if (WDist.TryParse(parts[3 * i], out var rx)
&& WDist.TryParse(parts[3 * i + 1], out var ry)
&& WDist.TryParse(parts[3 * i + 2], out var rz))
&& WDist.TryParse(parts[3 * i + 1], out var ry)
&& WDist.TryParse(parts[3 * i + 2], out var rz))
vecs[i] = new WVec(rx, ry, rz);
}
@@ -226,13 +236,11 @@ namespace OpenRA
if (value != null)
{
var parts = value.Split(SplitComma);
if (parts.Length == 3)
{
if (WDist.TryParse(parts[0], out var rx)
&& WDist.TryParse(parts[1], out var ry)
&& WDist.TryParse(parts[2], out var rz))
return new WPos(rx, ry, rz);
}
if (parts.Length == 3
&& WDist.TryParse(parts[0], out var rx)
&& WDist.TryParse(parts[1], out var ry)
&& WDist.TryParse(parts[2], out var rz))
return new WPos(rx, ry, rz);
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -240,7 +248,7 @@ namespace OpenRA
static object ParseWAngle(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (Exts.TryParseIntegerInvariant(value, out var res))
if (Exts.TryParseInt32Invariant(value, out var res))
return new WAngle(res);
return InvalidValueAction(value, fieldType, fieldName);
}
@@ -250,13 +258,11 @@ namespace OpenRA
if (value != null)
{
var parts = value.Split(SplitComma);
if (parts.Length == 3)
{
if (Exts.TryParseIntegerInvariant(parts[0], out var rr)
&& Exts.TryParseIntegerInvariant(parts[1], out var rp)
&& Exts.TryParseIntegerInvariant(parts[2], out var ry))
return new WRot(new WAngle(rr), new WAngle(rp), new WAngle(ry));
}
if (parts.Length == 3
&& Exts.TryParseInt32Invariant(parts[0], out var rr)
&& Exts.TryParseInt32Invariant(parts[1], out var rp)
&& Exts.TryParseInt32Invariant(parts[2], out var ry))
return new WRot(new WAngle(rr), new WAngle(rp), new WAngle(ry));
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -269,10 +275,33 @@ namespace OpenRA
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 3)
return new CPos(
Exts.ParseIntegerInvariant(parts[0]),
Exts.ParseIntegerInvariant(parts[1]),
Exts.ParseByte(parts[2]));
return new CPos(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
Exts.ParseInt32Invariant(parts[0]),
Exts.ParseInt32Invariant(parts[1]),
Exts.ParseByteInvariant(parts[2]));
return new CPos(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseCPosArray(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(SplitComma);
if (parts.Length % 2 != 0)
return InvalidValueAction(value, fieldType, fieldName);
var vecs = new CPos[parts.Length / 2];
for (var i = 0; i < vecs.Length; i++)
{
if (int.TryParse(parts[2 * i], out var rx)
&& int.TryParse(parts[2 * i + 1], out var ry))
vecs[i] = new CPos(rx, ry);
}
return vecs;
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -283,7 +312,7 @@ namespace OpenRA
if (value != null)
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new CVec(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
return new CVec(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -361,7 +390,7 @@ namespace OpenRA
static object ParseBool(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (bool.TryParse(value.ToLowerInvariant(), out var result))
return result;
return result ? BoxedTrue : BoxedFalse;
return InvalidValueAction(value, fieldType, fieldName);
}
@@ -376,7 +405,7 @@ namespace OpenRA
var ints = new int2[parts.Length / 2];
for (var i = 0; i < ints.Length; i++)
ints[i] = new int2(Exts.ParseIntegerInvariant(parts[2 * i]), Exts.ParseIntegerInvariant(parts[2 * i + 1]));
ints[i] = new int2(Exts.ParseInt32Invariant(parts[2 * i]), Exts.ParseInt32Invariant(parts[2 * i + 1]));
return ints;
}
@@ -389,7 +418,7 @@ namespace OpenRA
if (value != null)
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new Size(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
return new Size(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -403,7 +432,7 @@ namespace OpenRA
if (parts.Length != 2)
return InvalidValueAction(value, fieldType, fieldName);
return new int2(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
return new int2(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -451,10 +480,10 @@ namespace OpenRA
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new Rectangle(
Exts.ParseIntegerInvariant(parts[0]),
Exts.ParseIntegerInvariant(parts[1]),
Exts.ParseIntegerInvariant(parts[2]),
Exts.ParseIntegerInvariant(parts[3]));
Exts.ParseInt32Invariant(parts[0]),
Exts.ParseInt32Invariant(parts[1]),
Exts.ParseInt32Invariant(parts[2]),
Exts.ParseInt32Invariant(parts[3]));
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -469,11 +498,11 @@ namespace OpenRA
static object ParseHashSetOrList(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
{
var set = Activator.CreateInstance(fieldType);
if (value == null)
return set;
return Activator.CreateInstance(fieldType);
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
var set = Activator.CreateInstance(fieldType, parts.Length);
var arguments = fieldType.GetGenericArguments();
var addMethod = fieldType.GetMethod(nameof(List<object>.Add), arguments);
var addArgs = new object[1];
@@ -488,7 +517,10 @@ namespace OpenRA
static object ParseDictionary(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
{
var dict = Activator.CreateInstance(fieldType);
if (yaml == null)
return Activator.CreateInstance(fieldType);
var dict = Activator.CreateInstance(fieldType, yaml.Nodes.Length);
var arguments = fieldType.GetGenericArguments();
var addMethod = fieldType.GetMethod(nameof(Dictionary<object, object>.Add), arguments);
var addArgs = new object[2];
@@ -519,7 +551,7 @@ namespace OpenRA
if (string.IsNullOrEmpty(value))
return null;
var innerType = fieldType.GetGenericArguments().First();
var innerType = fieldType.GetGenericArguments()[0];
var innerValue = GetValue("Nullable<T>", innerType, value, field);
return fieldType.GetConstructor(new[] { innerType }).Invoke(new[] { innerValue });
}
@@ -527,7 +559,7 @@ namespace OpenRA
public static void Load(object self, MiniYaml my)
{
var loadInfo = TypeLoadInfo[self.GetType()];
var missing = new List<string>();
List<string> missing = null;
Dictionary<string, MiniYaml> md = null;
@@ -542,6 +574,7 @@ namespace OpenRA
val = fli.Loader(my);
else
{
missing ??= new List<string>();
missing.Add(fli.YamlName);
continue;
}
@@ -551,7 +584,11 @@ namespace OpenRA
if (!TryGetValueFromYaml(fli.YamlName, fli.Field, md, out val))
{
if (fli.Attribute.Required)
{
missing ??= new List<string>();
missing.Add(fli.YamlName);
}
continue;
}
}
@@ -559,7 +596,7 @@ namespace OpenRA
fli.Field.SetValue(self, val);
}
if (missing.Count > 0)
if (missing != null)
throw new MissingFieldsException(missing.ToArray());
}
@@ -620,12 +657,17 @@ namespace OpenRA
public static object GetValue(string fieldName, Type fieldType, string value, MemberInfo field)
{
return GetValue(fieldName, fieldType, new MiniYaml(value), field);
return GetValue(fieldName, fieldType, value, null, field);
}
public static object GetValue(string fieldName, Type fieldType, MiniYaml yaml, MemberInfo field)
{
var value = yaml.Value?.Trim();
return GetValue(fieldName, fieldType, yaml.Value, yaml, field);
}
static object GetValue(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
{
value = value?.Trim();
if (fieldType.IsGenericType)
{
if (GenericTypeParsers.TryGetValue(fieldType.GetGenericTypeDefinition(), out var parseFuncGeneric))

View File

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

View File

@@ -16,6 +16,7 @@ using System.Linq;
using System.Net;
using System.Text;
using ICSharpCode.SharpZipLib.Checksum;
using ICSharpCode.SharpZipLib.Zip.Compression;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using OpenRA.Graphics;
using OpenRA.Primitives;
@@ -45,12 +46,13 @@ namespace OpenRA.FileFormats
var data = new List<byte>();
Type = SpriteFrameType.Rgba32;
byte bitDepth = 8;
while (true)
{
var length = IPAddress.NetworkToHostOrder(s.ReadInt32());
var type = Encoding.UTF8.GetString(s.ReadBytes(4));
var type = s.ReadASCII(4);
var content = s.ReadBytes(length);
/*var crc = */s.ReadInt32();
s.ReadInt32(); // crc
if (!headerParsed && type != "IHDR")
throw new InvalidDataException("Invalid PNG file - header does not appear first.");
@@ -66,8 +68,8 @@ namespace OpenRA.FileFormats
Width = IPAddress.NetworkToHostOrder(ms.ReadInt32());
Height = IPAddress.NetworkToHostOrder(ms.ReadInt32());
var bitDepth = ms.ReadUInt8();
var colorType = (PngColorType)ms.ReadByte();
bitDepth = ms.ReadUInt8();
var colorType = (PngColorType)ms.ReadUInt8();
if (IsPaletted(bitDepth, colorType))
Type = SpriteFrameType.Indexed8;
else if (colorType == PngColorType.Color)
@@ -75,9 +77,9 @@ namespace OpenRA.FileFormats
Data = new byte[Width * Height * PixelStride];
var compression = ms.ReadByte();
/*var filter = */ms.ReadByte();
var interlace = ms.ReadByte();
var compression = ms.ReadUInt8();
ms.ReadUInt8(); // filter
var interlace = ms.ReadUInt8();
if (compression != 0)
throw new InvalidDataException("Compression method not supported");
@@ -92,10 +94,10 @@ namespace OpenRA.FileFormats
case "PLTE":
{
Palette = new Color[256];
for (var i = 0; i < length / 3; i++)
Palette = new Color[length / 3];
for (var i = 0; i < Palette.Length; i++)
{
var r = ms.ReadByte(); var g = ms.ReadByte(); var b = ms.ReadByte();
var r = ms.ReadUInt8(); var g = ms.ReadUInt8(); var b = ms.ReadUInt8();
Palette[i] = Color.FromArgb(r, g, b);
}
@@ -108,7 +110,7 @@ namespace OpenRA.FileFormats
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
for (var i = 0; i < length; i++)
Palette[i] = Color.FromArgb(ms.ReadByte(), Palette[i]);
Palette[i] = Color.FromArgb(ms.ReadUInt8(), Palette[i]);
break;
}
@@ -136,22 +138,77 @@ namespace OpenRA.FileFormats
{
var pxStride = PixelStride;
var rowStride = Width * pxStride;
var pixelsPerByte = 8 / bitDepth;
var sourceRowStride = Exts.IntegerDivisionRoundingAwayFromZero(rowStride, pixelsPerByte);
var prevLine = new byte[rowStride];
Span<byte> prevLine = new byte[rowStride];
for (var y = 0; y < Height; y++)
{
var filter = (PngFilter)ds.ReadByte();
var line = ds.ReadBytes(rowStride);
var filter = (PngFilter)ds.ReadUInt8();
ds.ReadBytes(Data, y * rowStride, sourceRowStride);
var line = Data.AsSpan(y * rowStride, rowStride);
for (var i = 0; i < rowStride; i++)
line[i] = i < pxStride
? UnapplyFilter(filter, line[i], 0, prevLine[i], 0)
: UnapplyFilter(filter, line[i], line[i - pxStride], prevLine[i], prevLine[i - pxStride]);
// If the source has a bit depth of 1, 2 or 4 it packs multiple pixels per byte.
// Unpack to bit depth of 8, yielding 1 pixel per byte.
// This makes life easier for consumers of palleted data.
if (bitDepth < 8)
{
var mask = 0xFF >> (8 - bitDepth);
for (var i = sourceRowStride - 1; i >= 0; i--)
{
var packed = line[i];
for (var j = 0; j < pixelsPerByte; j++)
{
var dest = i * pixelsPerByte + j;
if (dest < line.Length) // Guard against last byte being only partially packed
line[dest] = (byte)(packed >> (8 - (j + 1) * bitDepth) & mask);
}
}
}
Array.Copy(line, 0, Data, y * rowStride, rowStride);
switch (filter)
{
case PngFilter.None:
break;
case PngFilter.Sub:
for (var i = pxStride; i < rowStride; i++)
line[i] += line[i - pxStride];
break;
case PngFilter.Up:
for (var i = 0; i < rowStride; i++)
line[i] += prevLine[i];
break;
case PngFilter.Average:
for (var i = 0; i < pxStride; i++)
line[i] += Average(0, prevLine[i]);
for (var i = pxStride; i < rowStride; i++)
line[i] += Average(line[i - pxStride], prevLine[i]);
break;
case PngFilter.Paeth:
for (var i = 0; i < pxStride; i++)
line[i] += Paeth(0, prevLine[i], 0);
for (var i = pxStride; i < rowStride; i++)
line[i] += Paeth(line[i - pxStride], prevLine[i], prevLine[i - pxStride]);
break;
default:
throw new InvalidOperationException("Unsupported Filter");
}
prevLine = line;
}
static byte Average(byte a, byte b) => (byte)((a + b) / 2);
static byte Paeth(byte a, byte b, byte c)
{
var p = a + b - c;
var pa = Math.Abs(p - a);
var pb = Math.Abs(p - b);
var pc = Math.Abs(p - c);
return (pa <= pb && pa <= pc) ? a :
(pb <= pc) ? b : c;
}
}
}
@@ -228,38 +285,13 @@ namespace OpenRA.FileFormats
return isPng;
}
static byte UnapplyFilter(PngFilter f, byte x, byte a, byte b, byte c)
{
switch (f)
{
case PngFilter.None: return x;
case PngFilter.Sub: return (byte)(x + a);
case PngFilter.Up: return (byte)(x + b);
case PngFilter.Average: return (byte)(x + (a + b) / 2);
case PngFilter.Paeth: return (byte)(x + Paeth(a, b, c));
default:
throw new InvalidOperationException("Unsupported Filter");
}
}
static byte Paeth(byte a, byte b, byte c)
{
var p = a + b - c;
var pa = Math.Abs(p - a);
var pb = Math.Abs(p - b);
var pc = Math.Abs(p - c);
return (pa <= pb && pa <= pc) ? a :
(pb <= pc) ? b : c;
}
[Flags]
enum PngColorType { Indexed = 1, Color = 2, Alpha = 4 }
enum PngFilter { None, Sub, Up, Average, Paeth }
enum PngColorType : byte { Indexed = 1, Color = 2, Alpha = 4 }
enum PngFilter : byte { None, Sub, Up, Average, Paeth }
static bool IsPaletted(byte bitDepth, PngColorType colorType)
{
if (bitDepth == 8 && colorType == (PngColorType.Indexed | PngColorType.Color))
if (bitDepth <= 8 && colorType == (PngColorType.Indexed | PngColorType.Color))
return true;
if (bitDepth == 8 && colorType == (PngColorType.Color | PngColorType.Alpha))
@@ -271,16 +303,16 @@ namespace OpenRA.FileFormats
throw new InvalidDataException("Unknown pixel format");
}
void WritePngChunk(Stream output, string type, Stream input)
static void WritePngChunk(Stream output, string type, Stream input)
{
input.Position = 0;
var typeBytes = Encoding.ASCII.GetBytes(type);
output.Write(IPAddress.HostToNetworkOrder((int)input.Length));
output.WriteArray(typeBytes);
output.Write(typeBytes);
var data = input.ReadAllBytes();
output.WriteArray(data);
output.Write(data);
var crc32 = new Crc32();
crc32.Update(typeBytes);
@@ -292,7 +324,7 @@ namespace OpenRA.FileFormats
{
using (var output = new MemoryStream())
{
output.WriteArray(Signature);
output.Write(Signature);
using (var header = new MemoryStream())
{
header.Write(IPAddress.HostToNetworkOrder(Width));
@@ -340,13 +372,14 @@ namespace OpenRA.FileFormats
using (var data = new MemoryStream())
{
using (var compressed = new DeflaterOutputStream(data))
using (var compressed = new DeflaterOutputStream(data, new Deflater(Deflater.BEST_COMPRESSION)))
{
var rowStride = Width * PixelStride;
for (var y = 0; y < Height; y++)
{
// Write uncompressed scanlines for simplicity
compressed.WriteByte(0);
// Assuming no filtering for simplicity
const byte FilterType = 0;
compressed.WriteByte(FilterType);
compressed.Write(Data, y * rowStride, rowStride);
}
@@ -361,7 +394,7 @@ namespace OpenRA.FileFormats
{
using (var text = new MemoryStream())
{
text.WriteArray(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
text.Write(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
WritePngChunk(output, "tEXt", text);
}
}

View File

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

View File

@@ -83,14 +83,14 @@ namespace OpenRA.FileSystem
public void Mount(string name, string explicitName = null)
{
var optional = name.StartsWith("~", StringComparison.Ordinal);
var optional = name.StartsWith('~');
if (optional)
name = name[1..];
try
{
IReadOnlyPackage package;
if (name.StartsWith("$", StringComparison.Ordinal))
if (name.StartsWith('$'))
{
name = name[1..];
@@ -109,10 +109,8 @@ namespace OpenRA.FileSystem
Mount(package, explicitName);
}
catch
catch when (optional)
{
if (!optional)
throw;
}
}
@@ -161,9 +159,7 @@ namespace OpenRA.FileSystem
explicitMounts.Remove(key);
// Mod packages aren't owned by us, so we shouldn't dispose them
if (modPackages.Contains(package))
modPackages.Remove(package);
else
if (!modPackages.Remove(package))
package.Dispose();
}
else
@@ -190,6 +186,12 @@ namespace OpenRA.FileSystem
UnmountAll();
foreach (var kv in manifest.Packages)
Mount(kv.Key, kv.Value);
mountedPackages.TrimExcess();
explicitMounts.TrimExcess();
modPackages.TrimExcess();
foreach (var packages in fileIndex.Values)
packages.TrimExcess();
}
Stream GetFromCache(string filename)
@@ -226,14 +228,11 @@ namespace OpenRA.FileSystem
public bool TryOpen(string filename, out Stream s)
{
var explicitSplit = filename.IndexOf('|');
if (explicitSplit > 0)
if (explicitSplit > 0 && explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
{
if (explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
{
s = explicitPackage.GetStream(filename[(explicitSplit + 1)..]);
if (s != null)
return true;
}
s = explicitPackage.GetStream(filename[(explicitSplit + 1)..]);
if (s != null)
return true;
}
s = GetFromCache(filename);
@@ -262,10 +261,10 @@ namespace OpenRA.FileSystem
public bool Exists(string filename)
{
var explicitSplit = filename.IndexOf('|');
if (explicitSplit > 0)
if (explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
if (explicitPackage.Contains(filename[(explicitSplit + 1)..]))
return true;
if (explicitSplit > 0 &&
explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage) &&
explicitPackage.Contains(filename[(explicitSplit + 1)..]))
return true;
return fileIndex.ContainsKey(filename);
}
@@ -295,7 +294,7 @@ namespace OpenRA.FileSystem
public static string ResolveAssemblyPath(string path, Manifest manifest, InstalledMods installedMods)
{
var explicitSplit = path.IndexOf('|');
if (explicitSplit > 0 && !path.StartsWith("^"))
if (explicitSplit > 0 && !path.StartsWith('^'))
{
var parent = path[..explicitSplit];
var filename = path[(explicitSplit + 1)..];
@@ -304,7 +303,7 @@ namespace OpenRA.FileSystem
if (parentPath == null)
return null;
if (parentPath.StartsWith("$", StringComparison.Ordinal))
if (parentPath.StartsWith('$'))
{
if (!installedMods.TryGetValue(parentPath[1..], out var mod))
return null;

View File

@@ -41,7 +41,11 @@ namespace OpenRA.FileSystem
public Stream GetStream(string filename)
{
try { return File.OpenRead(Path.Combine(Name, filename)); }
var combined = Path.Combine(Name, filename);
if (!File.Exists(combined))
return null;
try { return File.OpenRead(combined); }
catch { return null; }
}
@@ -80,7 +84,7 @@ namespace OpenRA.FileSystem
// in FileSystem.OpenPackage. Their internal name therefore contains the
// full parent path too. We need to be careful to not add a second path
// prefix to these hacked packages.
var filePath = filename.StartsWith(Name) ? filename : Path.Combine(Name, filename);
var filePath = filename.StartsWith(Name, StringComparison.Ordinal) ? filename : Path.Combine(Name, filename);
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
using (var s = File.Create(filePath))
@@ -94,7 +98,7 @@ namespace OpenRA.FileSystem
// in FileSystem.OpenPackage. Their internal name therefore contains the
// full parent path too. We need to be careful to not add a second path
// prefix to these hacked packages.
var filePath = filename.StartsWith(Name) ? filename : Path.Combine(Name, filename);
var filePath = filename.StartsWith(Name, StringComparison.Ordinal) ? filename : Path.Combine(Name, filename);
if (Directory.Exists(filePath))
Directory.Delete(filePath, true);
else if (File.Exists(filePath))

View File

@@ -19,9 +19,9 @@ namespace OpenRA.FileSystem
{
public class ZipFileLoader : IPackageLoader
{
static readonly uint ZipSignature = 0x04034b50;
const uint ZipSignature = 0x04034b50;
class ReadOnlyZipFile : IReadOnlyPackage
public class ReadOnlyZipFile : IReadOnlyPackage
{
public string Name { get; protected set; }
protected ZipFile pkg;
@@ -68,6 +68,7 @@ namespace OpenRA.FileSystem
public void Dispose()
{
pkg?.Close();
GC.SuppressFinalize(this);
}
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
@@ -93,7 +94,7 @@ namespace OpenRA.FileSystem
}
}
sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage
public sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage
{
readonly MemoryStream pkgStream = new();
@@ -117,10 +118,7 @@ namespace OpenRA.FileSystem
void Commit()
{
var pos = pkgStream.Position;
pkgStream.Position = 0;
File.WriteAllBytes(Name, pkgStream.ReadBytes((int)pkgStream.Length));
pkgStream.Position = pos;
File.WriteAllBytes(Name, pkgStream.ToArray());
}
public void Update(string filename, byte[] contents)
@@ -147,7 +145,7 @@ namespace OpenRA.FileSystem
public ZipFolder(ReadOnlyZipFile parent, string path)
{
if (path.EndsWith("/", StringComparison.Ordinal))
if (path.EndsWith('/'))
path = path[..^1];
Name = path;
@@ -190,7 +188,7 @@ namespace OpenRA.FileSystem
public void Dispose() { /* nothing to do */ }
}
class StaticStreamDataSource : IStaticDataSource
sealed class StaticStreamDataSource : IStaticDataSource
{
readonly Stream s;
public StaticStreamDataSource(Stream s)

View File

@@ -86,8 +86,9 @@ namespace OpenRA
static void JoinInner(OrderManager om)
{
// Refresh TextNotificationsManager before the game starts.
// Refresh static classes before the game starts.
TextNotificationsManager.Clear();
UnitOrders.Clear();
// HACK: The shellmap World and OrderManager are owned by the main menu's WorldRenderer instead of Game.
// This allows us to switch Game.OrderManager from the shellmap to the new network connection when joining
@@ -179,6 +180,7 @@ namespace OpenRA
}
public static event Action BeforeGameStart = () => { };
public static event Action AfterGameStart = () => { };
internal static void StartGame(string mapUID, WorldType type)
{
// Dispose of the old world before creating a new one.
@@ -222,6 +224,12 @@ namespace OpenRA
// Much better to clean up now then to drop frames during gameplay for GC pauses.
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
// PostLoadComplete is designed for anything that should trigger at the very end of loading.
// e.g. audio notifications that the game is starting.
OrderManager.World.PostLoadComplete(worldRenderer);
AfterGameStart();
}
public static void RestartGame()
@@ -415,7 +423,7 @@ namespace OpenRA
// Sanitize input from platform-specific launchers
// Process.Start requires paths to not be quoted, even if they contain spaces
if (launchPath != null && launchPath.First() == '"' && launchPath.Last() == '"')
if (launchPath != null && launchPath[0] == '"' && launchPath.Last() == '"')
launchPath = launchPath[1..^1];
// Metadata registration requires an explicit launch path
@@ -523,10 +531,11 @@ namespace OpenRA
.Where(m => m.Status == MapStatus.Available && m.Visibility.HasFlag(MapVisibility.Shellmap))
.Select(m => m.Uid);
if (!shellmaps.Any())
var shellmap = shellmaps.RandomOrDefault(CosmeticRandom);
if (shellmap == null)
throw new InvalidDataException("No valid shellmaps available");
return shellmaps.Random(CosmeticRandom);
return shellmap;
}
public static void SwitchToExternalMod(ExternalMod mod, string[] launchArguments = null, Action onFailed = null)
@@ -581,7 +590,7 @@ namespace OpenRA
Log.Write("debug", "Taking screenshot " + path);
Renderer.SaveScreenshot(path);
TextNotificationsManager.Debug(ModData.Translation.GetString(SavedScreenshot, Translation.Arguments("filename", filename)));
TextNotificationsManager.Debug(TranslationProvider.GetString(SavedScreenshot, Translation.Arguments("filename", filename)));
}
}
@@ -600,7 +609,10 @@ namespace OpenRA
if (orderManager.LastTickTime.ShouldAdvance(tick))
{
using (new PerfSample("tick_time"))
if (orderManager.GameStarted && orderManager.LocalFrameNumber == 0)
PerfHistory.Reset(); // Remove history that occurred whilst the new game was loading.
using (var sample = new PerfSample("tick_time"))
{
orderManager.LastTickTime.AdvanceTickTime(tick);
@@ -609,7 +621,11 @@ namespace OpenRA
Sync.RunUnsynced(world, orderManager.TickImmediate);
if (world == null)
{
if (orderManager.GameStarted)
PerfHistory.Reset(); // Remove old history when a new game starts.
return;
}
if (orderManager.TryTick())
{
@@ -663,7 +679,7 @@ namespace OpenRA
// Prepare renderables (i.e. render voxels) before calling BeginFrame
using (new PerfSample("render_prepare"))
{
Renderer.WorldModelRenderer.BeginFrame();
worldRenderer?.BeginFrame();
// World rendering is disabled while the loading screen is displayed
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
@@ -673,7 +689,7 @@ namespace OpenRA
}
Ui.PrepareRenderables();
Renderer.WorldModelRenderer.EndFrame();
worldRenderer?.EndFrame();
}
// worldRenderer is null during the initial install/download screen
@@ -777,7 +793,7 @@ namespace OpenRA
var logicWorld = worldRenderer?.World;
// ReplayTimestep = 0 means the replay is paused: we need to keep logicInterval as UI.Timestep to avoid breakage
if (logicWorld != null && !(logicWorld.IsReplay && logicWorld.ReplayTimestep == 0))
if (logicWorld != null && (!logicWorld.IsReplay || logicWorld.ReplayTimestep != 0))
logicInterval = logicWorld == OrderManager.World ? OrderManager.SuggestedTimestep : logicWorld.Timestep;
// Ideal time between screen updates
@@ -912,15 +928,15 @@ namespace OpenRA
{
var endpoints = new List<IPEndPoint>
{
new IPEndPoint(IPAddress.IPv6Any, settings.ListenPort),
new IPEndPoint(IPAddress.Any, settings.ListenPort)
new(IPAddress.IPv6Any, settings.ListenPort),
new(IPAddress.Any, settings.ListenPort)
};
server = new Server.Server(endpoints, settings, ModData, ServerType.Multiplayer);
return server.GetEndpointForLocalConnection();
}
public static ConnectionTarget CreateLocalServer(string map)
public static ConnectionTarget CreateLocalServer(string map, bool isSkirmish = false)
{
var settings = new ServerSettings()
{
@@ -934,9 +950,9 @@ namespace OpenRA
// This would break the Restart button, which relies on the PlayerIndex always being the same for local servers
var endpoints = new List<IPEndPoint>
{
new IPEndPoint(IPAddress.Loopback, 0)
new(IPAddress.Loopback, 0)
};
server = new Server.Server(endpoints, settings, ModData, ServerType.Local);
server = new Server.Server(endpoints, settings, ModData, isSkirmish ? ServerType.Skirmish : ServerType.Local);
return server.GetEndpointForLocalConnection();
}

View File

@@ -49,13 +49,13 @@ namespace OpenRA
playersByRuntime = new Dictionary<OpenRA.Player, Player>();
}
public static GameInformation Deserialize(string data)
public static GameInformation Deserialize(string data, string path)
{
try
{
var info = new GameInformation();
var nodes = MiniYaml.FromString(data);
var nodes = MiniYaml.FromString(data, path);
foreach (var node in nodes)
{
var keyParts = node.Key.Split('@');
@@ -85,7 +85,7 @@ namespace OpenRA
{
var nodes = new List<MiniYamlNode>
{
new MiniYamlNode("Root", FieldSaver.Save(this))
new("Root", FieldSaver.Save(this))
};
for (var i = 0; i < Players.Count; i++)

View File

@@ -22,7 +22,7 @@ namespace OpenRA
/// </summary>
public class ActorInfo
{
public const string AbstractActorPrefix = "^";
public const char AbstractActorPrefix = '^';
public const char TraitInstanceSeparator = '@';
/// <summary>
@@ -130,6 +130,7 @@ namespace OpenRA
// Continue resolving traits as long as possible.
// Each time we resolve some traits, this means dependencies for other traits may then be possible to satisfy in the next pass.
#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection
var readyToResolve = more.ToList();
while (readyToResolve.Count != 0)
{
@@ -138,29 +139,30 @@ namespace OpenRA
readyToResolve.Clear();
readyToResolve.AddRange(more);
}
#pragma warning restore CA1851
if (unresolved.Count != 0)
{
var exceptionString = "ActorInfo(\"" + Name + "\") failed to initialize because of the following:\r\n";
var exceptionString = "ActorInfo(\"" + Name + "\") failed to initialize because of the following:\n";
var missing = unresolved.SelectMany(u => u.Dependencies.Where(d => !source.Any(s => AreResolvable(d, s.Type)))).Distinct();
exceptionString += "Missing:\r\n";
exceptionString += "Missing:\n";
foreach (var m in missing)
exceptionString += m + " \r\n";
exceptionString += m + " \n";
exceptionString += "Unresolved:\r\n";
exceptionString += "Unresolved:\n";
foreach (var u in unresolved)
{
var deps = u.Dependencies.Where(d => !resolved.Exists(r => r.Type == d));
var optDeps = u.OptionalDependencies.Where(d => !resolved.Exists(r => r.Type == d));
var allDeps = string.Join(", ", deps.Select(o => o.ToString()).Concat(optDeps.Select(o => $"[{o}]")));
exceptionString += $"{u.Type}: {{ {allDeps} }}\r\n";
exceptionString += $"{u.Type}: {{ {allDeps} }}\n";
}
throw new YamlException(exceptionString);
}
constructOrderCache = resolved.Select(r => r.Trait).ToList();
constructOrderCache = resolved.ConvertAll(r => r.Trait);
return constructOrderCache;
}
@@ -185,7 +187,7 @@ namespace OpenRA
public bool HasTraitInfo<T>() where T : ITraitInfoInterface { return traits.Contains<T>(); }
public T TraitInfo<T>() where T : ITraitInfoInterface { return traits.Get<T>(); }
public T TraitInfoOrDefault<T>() where T : ITraitInfoInterface { return traits.GetOrDefault<T>(); }
public IEnumerable<T> TraitInfos<T>() where T : ITraitInfoInterface { return traits.WithInterface<T>(); }
public IReadOnlyCollection<T> TraitInfos<T>() where T : ITraitInfoInterface { return traits.WithInterface<T>(); }
public BitSet<TargetableType> GetAllTargetTypes()
{

View File

@@ -28,14 +28,14 @@ namespace OpenRA.GameRules
Title = value.Value;
var nd = value.ToDictionary();
if (nd.ContainsKey("Hidden"))
bool.TryParse(nd["Hidden"].Value, out Hidden);
if (nd.TryGetValue("Hidden", out var yaml))
bool.TryParse(yaml.Value, out Hidden);
if (nd.ContainsKey("VolumeModifier"))
VolumeModifier = FieldLoader.GetValue<float>("VolumeModifier", nd["VolumeModifier"].Value);
if (nd.TryGetValue("VolumeModifier", out yaml))
VolumeModifier = FieldLoader.GetValue<float>("VolumeModifier", yaml.Value);
var ext = nd.ContainsKey("Extension") ? nd["Extension"].Value : "aud";
Filename = (nd.ContainsKey("Filename") ? nd["Filename"].Value : key) + "." + ext;
var ext = nd.TryGetValue("Extension", out yaml) ? yaml.Value : "aud";
Filename = (nd.TryGetValue("Filename", out yaml) ? yaml.Value : key) + "." + ext;
}
public void Load(IReadOnlyFileSystem fileSystem)

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ namespace OpenRA.Graphics
bool backwards;
bool tickAlways;
int timeUntilNextFrame;
Action tickFunc = () => { };
Action tickFunc;
public Animation(World world, string name)
: this(world, name, () => WAngle.Zero) { }
@@ -164,7 +164,7 @@ namespace OpenRA.Graphics
if (frame >= CurrentSequence.Length)
{
frame = CurrentSequence.Length - 1;
tickFunc = () => { };
tickFunc = null;
after?.Invoke();
}
};
@@ -212,13 +212,13 @@ namespace OpenRA.Graphics
public void Tick(int t)
{
if (tickAlways)
tickFunc();
tickFunc?.Invoke();
else
{
timeUntilNextFrame -= t;
while (timeUntilNextFrame <= 0)
{
tickFunc();
tickFunc?.Invoke();
timeUntilNextFrame += CurrentSequenceTickOrDefault();
}
}

View File

@@ -77,11 +77,12 @@ namespace OpenRA.Graphics
cachedPanelSprites = new Dictionary<string, Sprite[]>();
cachedCollectionSheets = new Dictionary<Collection, (Sheet, int)>();
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var chrome = MiniYaml.Merge(modData.Manifest.Chrome
.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s)));
.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool)));
foreach (var c in chrome)
if (!c.Key.StartsWith("^", StringComparison.Ordinal))
if (!c.Key.StartsWith('^'))
LoadCollection(c.Key, c.Value);
}
@@ -234,7 +235,7 @@ namespace OpenRA.Graphics
{
// PERF: We don't need to search for images if there are no definitions.
// PERF: It's more efficient to send an empty array rather than an array of 9 nulls.
if (!collection.Regions.Any())
if (collection.Regions.Count == 0)
return Array.Empty<Sprite>();
// Support manual definitions for unusual dialog layouts
@@ -263,13 +264,13 @@ namespace OpenRA.Graphics
if (!collections.TryGetValue(collectionName, out var collection))
{
Log.Write("debug", "Could not find collection '{0}'", collectionName);
Log.Write("debug", $"Could not find collection '{collectionName}'");
return new Size(0, 0);
}
if (collection.PanelRegion == null || collection.PanelRegion.Length != 8)
{
Log.Write("debug", "Collection '{0}' does not define a valid PanelRegion", collectionName);
Log.Write("debug", $"Collection '{collectionName}' does not define a valid PanelRegion");
return new Size(0, 0);
}

View File

@@ -17,7 +17,7 @@ namespace OpenRA.Graphics
{
public sealed class CursorManager
{
class Cursor
sealed class Cursor
{
public string Name;
public int2 PaddedSize;

View File

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

View File

@@ -27,19 +27,20 @@ namespace OpenRA.Graphics
{
var d = info.ToDictionary();
Start = Exts.ParseIntegerInvariant(d["Start"].Value);
Start = Exts.ParseInt32Invariant(d["Start"].Value);
Palette = palette;
Name = name;
var cursorSprites = cache[cursorSrc];
Frames = cursorSprites.Skip(Start).ToArray();
if ((d.ContainsKey("Length") && d["Length"].Value == "*") || (d.ContainsKey("End") && d["End"].Value == "*"))
if ((d.TryGetValue("Length", out var yaml) && yaml.Value == "*") ||
(d.TryGetValue("End", out yaml) && yaml.Value == "*"))
Length = Frames.Length;
else if (d.ContainsKey("Length"))
Length = Exts.ParseIntegerInvariant(d["Length"].Value);
else if (d.ContainsKey("End"))
Length = Exts.ParseIntegerInvariant(d["End"].Value) - Start;
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;
@@ -51,15 +52,15 @@ namespace OpenRA.Graphics
if (Length > cursorSprites.Length)
throw new YamlException($"Cursor {name}: {nameof(Length)} is greater than the length of the sprite sequence.");
if (d.ContainsKey("X"))
if (d.TryGetValue("X", out yaml))
{
Exts.TryParseIntegerInvariant(d["X"].Value, out var x);
Exts.TryParseInt32Invariant(yaml.Value, out var x);
Hotspot = Hotspot.WithX(x);
}
if (d.ContainsKey("Y"))
if (d.TryGetValue("Y", out yaml))
{
Exts.TryParseIntegerInvariant(d["Y"].Value, out var y);
Exts.TryParseInt32Invariant(yaml.Value, out var y);
Hotspot = Hotspot.WithY(y);
}
}

View File

@@ -79,6 +79,9 @@ namespace OpenRA.Graphics
CopyPaletteToBuffer(index, p);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Performance", "CA1854:Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method",
Justification = "False positive - indexer is a set not a get.")]
public void ReplacePalette(string name, IPalette p)
{
if (mutablePalettes.ContainsKey(name))

View File

@@ -0,0 +1,56 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Linq;
using OpenRA.Primitives;
namespace OpenRA.Graphics
{
public class MarkerTileRenderable : IRenderable, IFinalizedRenderable
{
readonly CPos pos;
readonly Color color;
public MarkerTileRenderable(CPos pos, Color color)
{
this.pos = pos;
this.color = color;
}
public WPos Pos => WPos.Zero;
public int ZOffset => 0;
public bool IsDecoration => true;
public IRenderable WithZOffset(int newOffset) { return this; }
public IRenderable OffsetBy(in WVec vec)
{
return new MarkerTileRenderable(pos, color);
}
public IRenderable AsDecoration() { return this; }
public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; }
public void Render(WorldRenderer wr)
{
var map = wr.World.Map;
var r = map.Grid.Ramps[map.Ramp[pos]];
var wpos = map.CenterOfCell(pos) - new WVec(0, 0, r.CenterHeightOffset);
var corners = r.Corners.Select(corner => wr.Viewport.WorldToViewPx(wr.Screen3DPosition(wpos + corner))).ToList();
Game.Renderer.RgbaColorRenderer.FillRect(corners[0], corners[1], corners[2], corners[3], color);
}
public void RenderDebugGeometry(WorldRenderer wr) { }
public Rectangle ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; }
}
}

View File

@@ -10,9 +10,8 @@
#endregion
using System;
using System.Collections.Generic;
using OpenRA.FileSystem;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Graphics
{
@@ -30,6 +29,14 @@ namespace OpenRA.Graphics
Rectangle AggregateBounds { get; }
}
public interface IModelWidget
{
public string Palette { get; }
public float Scale { get; }
public void Setup(Func<bool> isVisible, Func<string> getPalette, Func<string> getPlayerPalette,
Func<float> getScale, Func<IModel> getVoxel, Func<WRot> getRotation);
}
public readonly struct ModelRenderData
{
public readonly int Start;
@@ -44,51 +51,13 @@ namespace OpenRA.Graphics
}
}
public interface IModelCache : IDisposable
public interface IModelCacheInfo : ITraitInfoInterface { }
public interface IModelCache
{
IModel GetModel(string model);
IModel GetModelSequence(string model, string sequence);
bool HasModelSequence(string model, string sequence);
IVertexBuffer<Vertex> VertexBuffer { get; }
}
public interface IModelSequenceLoader
{
Action<string> OnMissingModelError { get; set; }
IModelCache CacheModels(IReadOnlyFileSystem fileSystem, ModData modData, IReadOnlyDictionary<string, MiniYamlNode> modelDefinitions);
}
public class PlaceholderModelSequenceLoader : IModelSequenceLoader
{
public Action<string> OnMissingModelError { get; set; }
class PlaceholderModelCache : IModelCache
{
public IVertexBuffer<Vertex> VertexBuffer => throw new NotImplementedException();
public void Dispose() { }
public IModel GetModel(string model)
{
throw new NotImplementedException();
}
public IModel GetModelSequence(string model, string sequence)
{
throw new NotImplementedException();
}
public bool HasModelSequence(string model, string sequence)
{
throw new NotImplementedException();
}
}
public PlaceholderModelSequenceLoader(ModData modData) { }
public IModelCache CacheModels(IReadOnlyFileSystem fileSystem, ModData modData, IReadOnlyDictionary<string, MiniYamlNode> modelDefinitions)
{
return new PlaceholderModelCache();
}
IVertexBuffer<ModelVertex> VertexBuffer { get; }
}
}

View File

@@ -0,0 +1,53 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Runtime.InteropServices;
namespace OpenRA.Graphics
{
[StructLayout(LayoutKind.Sequential)]
public readonly struct ModelVertex
{
// 3d position
public readonly float X, Y, Z;
// Primary and secondary texture coordinates or RGBA color
public readonly float S, T, U, V;
// Palette and channel flags
public readonly float P, C;
public ModelVertex(in float3 xyz, float s, float t, float u, float v, float p, float c)
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, p, c) { }
public ModelVertex(float x, float y, float z, float s, float t, float u, float v, float p, float c)
{
X = x; Y = y; Z = z;
S = s; T = t;
U = u; V = v;
P = p; C = c;
}
}
public sealed class ModelShaderBindings : ShaderBindings
{
public ModelShaderBindings()
: base("model")
{ }
public override ShaderVertexAttribute[] Attributes { get; } = new[]
{
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
new ShaderVertexAttribute("aVertexTexMetadata", ShaderVertexAttributeType.Float, 2, 28),
};
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,64 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Runtime.InteropServices;
namespace OpenRA.Graphics
{
[StructLayout(LayoutKind.Sequential)]
public readonly struct RenderPostProcessPassVertex
{
public readonly float X, Y;
public RenderPostProcessPassVertex(float x, float y)
{
X = x; Y = y;
}
}
[StructLayout(LayoutKind.Sequential)]
public readonly struct RenderPostProcessPassTexturedVertex
{
// 3d position
public readonly float X, Y;
public readonly float S, T;
public RenderPostProcessPassTexturedVertex(float x, float y, float s, float t)
{
X = x; Y = y;
S = s; T = t;
}
}
public sealed class RenderPostProcessPassShaderBindings : ShaderBindings
{
public RenderPostProcessPassShaderBindings(string name)
: base("postprocess", "postprocess_" + name) { }
public override ShaderVertexAttribute[] Attributes { get; } = new[]
{
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0)
};
}
public sealed class RenderPostProcessPassTexturedShaderBindings : ShaderBindings
{
public RenderPostProcessPassTexturedShaderBindings(string name)
: base("postprocess_textured", "postprocess_textured_" + name)
{ }
public override ShaderVertexAttribute[] Attributes { get; } = new[]
{
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0),
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 2, 8),
};
}
}

View File

@@ -21,7 +21,7 @@ namespace OpenRA.Graphics
static readonly float3 Offset = new(0.5f, 0.5f, 0f);
readonly SpriteRenderer parent;
readonly Vertex[] vertices = new Vertex[6];
readonly Vertex[] vertices = new Vertex[4];
public RgbaColorRenderer(SpriteRenderer parent)
{
@@ -45,14 +45,12 @@ namespace OpenRA.Graphics
var eb = endColor.B / 255.0f;
var ea = endColor.A / 255.0f;
vertices[0] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0, 0);
vertices[1] = new Vertex(start + corner + Offset, sr, sg, sb, sa, 0, 0);
vertices[2] = new Vertex(end + corner + Offset, er, eg, eb, ea, 0, 0);
vertices[3] = new Vertex(end + corner + Offset, er, eg, eb, ea, 0, 0);
vertices[4] = new Vertex(end - corner + Offset, er, eg, eb, ea, 0, 0);
vertices[5] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0, 0);
vertices[0] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0);
vertices[1] = new Vertex(start + corner + Offset, sr, sg, sb, sa, 0);
vertices[2] = new Vertex(end + corner + Offset, er, eg, eb, ea, 0);
vertices[3] = new Vertex(end - corner + Offset, er, eg, eb, ea, 0);
parent.DrawRGBAVertices(vertices, blendMode);
parent.DrawRGBAQuad(vertices, blendMode);
}
public void DrawLine(in float3 start, in float3 end, float width, Color color, BlendMode blendMode = BlendMode.Alpha)
@@ -66,13 +64,11 @@ namespace OpenRA.Graphics
var b = color.B / 255.0f;
var a = color.A / 255.0f;
vertices[0] = new Vertex(start - corner + Offset, r, g, b, a, 0, 0);
vertices[1] = new Vertex(start + corner + Offset, r, g, b, a, 0, 0);
vertices[2] = new Vertex(end + corner + Offset, r, g, b, a, 0, 0);
vertices[3] = new Vertex(end + corner + Offset, r, g, b, a, 0, 0);
vertices[4] = new Vertex(end - corner + Offset, r, g, b, a, 0, 0);
vertices[5] = new Vertex(start - corner + Offset, r, g, b, a, 0, 0);
parent.DrawRGBAVertices(vertices, blendMode);
vertices[0] = new Vertex(start - corner + Offset, r, g, b, a, 0);
vertices[1] = new Vertex(start + corner + Offset, r, g, b, a, 0);
vertices[2] = new Vertex(end + corner + Offset, r, g, b, a, 0);
vertices[3] = new Vertex(end - corner + Offset, r, g, b, a, 0);
parent.DrawRGBAQuad(vertices, blendMode);
}
/// <summary>
@@ -80,7 +76,7 @@ namespace OpenRA.Graphics
/// Will behave badly if the lines are parallel.
/// Z position is the average of a and b (ignores actual intersection point if it exists).
/// </summary>
float3 IntersectionOf(in float3 a, in float3 da, in float3 b, in float3 db)
static float3 IntersectionOf(in float3 a, in float3 da, in float3 b, in float3 db)
{
var crossA = a.X * (a.Y + da.Y) - a.Y * (a.X + da.X);
var crossB = b.X * (b.Y + db.Y) - b.Y * (b.X + db.X);
@@ -157,13 +153,11 @@ namespace OpenRA.Graphics
var cd = closed || i < limit - 1 ? IntersectionOf(end - corner, dir, end - nextCorner, nextDir) : end - corner;
// Fill segment
vertices[0] = new Vertex(ca + Offset, r, g, b, a, 0, 0);
vertices[1] = new Vertex(cb + Offset, r, g, b, a, 0, 0);
vertices[2] = new Vertex(cc + Offset, r, g, b, a, 0, 0);
vertices[3] = new Vertex(cc + Offset, r, g, b, a, 0, 0);
vertices[4] = new Vertex(cd + Offset, r, g, b, a, 0, 0);
vertices[5] = new Vertex(ca + Offset, r, g, b, a, 0, 0);
parent.DrawRGBAVertices(vertices, blendMode);
vertices[0] = new Vertex(ca + Offset, r, g, b, a, 0);
vertices[1] = new Vertex(cb + Offset, r, g, b, a, 0);
vertices[2] = new Vertex(cc + Offset, r, g, b, a, 0);
vertices[3] = new Vertex(cd + Offset, r, g, b, a, 0);
parent.DrawRGBAQuad(vertices, blendMode);
// Advance line segment
end = next;
@@ -200,20 +194,6 @@ namespace OpenRA.Graphics
DrawPolygon(new[] { tl, tr, br, bl }, width, color, blendMode);
}
public void FillTriangle(in float3 a, in float3 b, in float3 c, Color color, BlendMode blendMode = BlendMode.Alpha)
{
color = Util.PremultiplyAlpha(color);
var cr = color.R / 255.0f;
var cg = color.G / 255.0f;
var cb = color.B / 255.0f;
var ca = color.A / 255.0f;
vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0);
vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0, 0);
vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
parent.DrawRGBAVertices(vertices, blendMode);
}
public void FillRect(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha)
{
var tr = new float3(br.X, tl.Y, tl.Z);
@@ -229,13 +209,11 @@ namespace OpenRA.Graphics
var cb = color.B / 255.0f;
var ca = color.A / 255.0f;
vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0);
vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0, 0);
vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
vertices[3] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
vertices[4] = new Vertex(d + Offset, cr, cg, cb, ca, 0, 0);
vertices[5] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0);
parent.DrawRGBAVertices(vertices, blendMode);
vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0);
vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0);
vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0);
vertices[3] = new Vertex(d + Offset, cr, cg, cb, ca, 0);
parent.DrawRGBAQuad(vertices, blendMode);
}
public void FillRect(in float3 a, in float3 b, in float3 c, in float3 d, Color topLeftColor, Color topRightColor, Color bottomRightColor, Color bottomLeftColor, BlendMode blendMode = BlendMode.Alpha)
@@ -243,11 +221,9 @@ namespace OpenRA.Graphics
vertices[0] = VertexWithColor(a + Offset, topLeftColor);
vertices[1] = VertexWithColor(b + Offset, topRightColor);
vertices[2] = VertexWithColor(c + Offset, bottomRightColor);
vertices[3] = VertexWithColor(c + Offset, bottomRightColor);
vertices[4] = VertexWithColor(d + Offset, bottomLeftColor);
vertices[5] = VertexWithColor(a + Offset, topLeftColor);
vertices[3] = VertexWithColor(d + Offset, bottomLeftColor);
parent.DrawRGBAVertices(vertices, blendMode);
parent.DrawRGBAQuad(vertices, blendMode);
}
static Vertex VertexWithColor(in float3 xyz, Color color)
@@ -258,7 +234,7 @@ namespace OpenRA.Graphics
var cb = color.B / 255.0f;
var ca = color.A / 255.0f;
return new Vertex(xyz, cr, cg, cb, ca, 0, 0);
return new Vertex(xyz, cr, cg, cb, ca, 0);
}
public void FillEllipse(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha)

View File

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

View File

@@ -0,0 +1,70 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.IO;
using System.Linq;
namespace OpenRA.Graphics
{
public enum ShaderVertexAttributeType
{
// Assign the underlying OpenGL type values
// to simplify enum use in the shader
Float = 0x1406, // GL_FLOAT
Int = 0x1404, // GL_INT
UInt = 0x1405 // GL_UNSIGNED_INT
}
public readonly struct ShaderVertexAttribute
{
public readonly string Name;
public readonly ShaderVertexAttributeType Type;
public readonly int Components;
public readonly int Offset;
public ShaderVertexAttribute(string name, ShaderVertexAttributeType type, int components, int offset)
{
Name = name;
Type = type;
Components = components;
Offset = offset;
}
}
public abstract class ShaderBindings : IShaderBindings
{
public string VertexShaderName { get; }
public string VertexShaderCode { get; }
public string FragmentShaderName { get; }
public string FragmentShaderCode { get; }
public int Stride { get; }
public abstract ShaderVertexAttribute[] Attributes { get; }
protected ShaderBindings(string name)
: this(name, name) { }
protected ShaderBindings(string vertexName, string fragmentName)
{
Stride = Attributes.Sum(a => a.Components * 4);
VertexShaderName = vertexName;
VertexShaderCode = GetShaderCode(VertexShaderName + ".vert");
FragmentShaderName = fragmentName;
FragmentShaderCode = GetShaderCode(FragmentShaderName + ".frag");
}
public static string GetShaderCode(string filename)
{
var filepath = Path.Combine(Platform.EngineDir, "glsl", filename);
return File.ReadAllText(filepath);
}
}
}

View File

@@ -82,16 +82,16 @@ namespace OpenRA.Graphics
this.margin = margin;
}
public Sprite Add(ISpriteFrame frame) { return Add(frame.Data, frame.Type, frame.Size, 0, frame.Offset); }
public Sprite Add(byte[] src, SpriteFrameType type, Size size) { return Add(src, type, size, 0, float3.Zero); }
public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset)
public Sprite Add(ISpriteFrame frame, bool premultiplied = false) { return Add(frame.Data, frame.Type, frame.Size, 0, frame.Offset, premultiplied); }
public Sprite Add(byte[] src, SpriteFrameType type, Size size, bool premultiplied = false) { return Add(src, type, size, 0, float3.Zero, premultiplied); }
public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset, bool premultiplied = false)
{
// Don't bother allocating empty sprites
if (size.Width == 0 || size.Height == 0)
return new Sprite(Current, Rectangle.Empty, 0, spriteOffset, CurrentChannel, BlendMode.Alpha);
var rect = Allocate(size, zRamp, spriteOffset);
Util.FastCopyIntoChannel(rect, src, type);
Util.FastCopyIntoChannel(rect, src, type, premultiplied);
Current.CommitBufferedData();
return rect;
}

View File

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

View File

@@ -18,17 +18,15 @@ using OpenRA.Primitives;
namespace OpenRA.Graphics
{
public class SpriteCache : IDisposable
public sealed class SpriteCache : IDisposable
{
public readonly Dictionary<SheetType, SheetBuilder> SheetBuilders;
readonly ISpriteLoader[] loaders;
readonly IReadOnlyFileSystem fileSystem;
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> spriteReservations = new();
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> frameReservations = new();
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location, Func<ISpriteFrame, ISpriteFrame> AdjustFrame, bool Premultiplied)> spriteReservations = new();
readonly Dictionary<string, List<int>> reservationsByFilename = new();
readonly Dictionary<int, ISpriteFrame[]> resolvedFrames = new();
readonly Dictionary<int, Sprite[]> resolvedSprites = new();
readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = new();
@@ -47,18 +45,10 @@ namespace OpenRA.Graphics
this.loaders = loaders;
}
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location, Func<ISpriteFrame, ISpriteFrame> adjustFrame = null, bool premultiplied = false)
{
var token = nextReservationToken++;
spriteReservations[token] = (frames?.ToArray(), location);
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
return token;
}
public int ReserveFrames(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
{
var token = nextReservationToken++;
frameReservations[token] = (frames?.ToArray(), location);
spriteReservations[token] = (frames?.ToArray(), location, adjustFrame, premultiplied);
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
return token;
}
@@ -84,60 +74,73 @@ namespace OpenRA.Graphics
foreach (var sb in SheetBuilders.Values)
sb.Current.CreateBuffer();
var spriteCache = new Dictionary<int, Sprite>();
var pendingResolve = new List<(
string Filename,
int FrameIndex,
bool Premultiplied,
Func<ISpriteFrame, ISpriteFrame> AdjustFrame,
ISpriteFrame Frame,
Sprite[] SpritesForToken)>();
foreach (var (filename, tokens) in reservationsByFilename)
{
modData.LoadScreen?.Display();
var loadedFrames = GetFrames(fileSystem, filename, loaders, out _);
foreach (var token in tokens)
{
if (frameReservations.TryGetValue(token, out var r))
{
if (loadedFrames != null)
{
if (r.Frames != null)
{
var resolved = new ISpriteFrame[loadedFrames.Length];
foreach (var i in r.Frames)
resolved[i] = loadedFrames[i];
resolvedFrames[token] = resolved;
}
else
resolvedFrames[token] = loadedFrames;
}
else
{
resolvedFrames[token] = null;
missingFiles[token] = (filename, r.Location);
}
}
if (spriteReservations.TryGetValue(token, out r))
if (spriteReservations.TryGetValue(token, out var rs))
{
if (loadedFrames != null)
{
var resolved = new Sprite[loadedFrames.Length];
var frames = r.Frames ?? Enumerable.Range(0, loadedFrames.Length);
foreach (var i in frames)
resolved[i] = spriteCache.GetOrAdd(i,
f => SheetBuilders[SheetBuilder.FrameTypeToSheetType(loadedFrames[f].Type)].Add(loadedFrames[f]));
resolvedSprites[token] = resolved;
var frames = rs.Frames ?? Enumerable.Range(0, loadedFrames.Length);
foreach (var i in frames)
{
var frame = loadedFrames[i];
if (rs.AdjustFrame != null)
frame = rs.AdjustFrame(frame);
pendingResolve.Add((filename, i, rs.Premultiplied, rs.AdjustFrame, frame, resolved));
}
}
else
{
resolvedSprites[token] = null;
missingFiles[token] = (filename, r.Location);
missingFiles[token] = (filename, rs.Location);
}
}
}
spriteCache.Clear();
}
spriteReservations.Clear();
frameReservations.Clear();
spriteReservations.TrimExcess();
reservationsByFilename.Clear();
reservationsByFilename.TrimExcess();
// When the sheet builder is adding sprites, it reserves height for the tallest sprite seen along the row.
// We can achieve better sheet packing by keeping sprites with similar heights together.
var orderedPendingResolve = pendingResolve.OrderBy(x => x.Frame.Size.Height);
var spriteCache = new Dictionary<(
string Filename,
int FrameIndex,
bool Premultiplied,
Func<ISpriteFrame, ISpriteFrame> AdjustFrame),
Sprite>(pendingResolve.Count);
foreach (var (filename, frameIndex, premultiplied, adjustFrame, frame, spritesForToken) in orderedPendingResolve)
{
// Premultiplied and non-premultiplied sprites must be cached separately
// to cover the case where the same image is requested in both versions.
spritesForToken[frameIndex] = spriteCache.GetOrAdd(
(filename, frameIndex, premultiplied, adjustFrame),
_ =>
{
var sheetBuilder = SheetBuilders[SheetBuilder.FrameTypeToSheetType(frame.Type)];
return sheetBuilder.Add(frame, premultiplied);
});
modData.LoadScreen?.Display();
}
foreach (var sb in SheetBuilders.Values)
sb.Current.ReleaseBuffer();
@@ -145,18 +148,11 @@ namespace OpenRA.Graphics
public Sprite[] ResolveSprites(int token)
{
var resolved = resolvedSprites[token];
resolvedSprites.Remove(token);
if (missingFiles.TryGetValue(token, out var r))
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);
if (!resolvedSprites.Remove(token, out var resolved))
throw new InvalidOperationException($"{nameof(token)} {token} has either already been resolved, or was never reserved via {nameof(ReserveSprites)}");
return resolved;
}
resolvedSprites.TrimExcess();
public ISpriteFrame[] ResolveFrames(int token)
{
var resolved = resolvedFrames[token];
resolvedFrames.Remove(token);
if (missingFiles.TryGetValue(token, out var r))
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);

View File

@@ -123,7 +123,7 @@ namespace OpenRA.Graphics
}
}
float2 Rotate(float2 v, float sina, float cosa, float2 offset)
static float2 Rotate(float2 v, float sina, float cosa, float2 offset)
{
return new float2(
v.X * cosa - v.Y * sina + offset.X,
@@ -427,7 +427,7 @@ namespace OpenRA.Graphics
}
}
class GlyphInfo
sealed class GlyphInfo
{
public float Advance;
public int2 Offset;

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@
#endregion
using System;
using System.Runtime.InteropServices;
using OpenRA.FileFormats;
using OpenRA.Primitives;
@@ -20,7 +21,17 @@ namespace OpenRA.Graphics
// yes, our channel order is nuts.
static readonly int[] ChannelMasks = { 2, 1, 0, 3 };
public static void FastCreateQuad(Vertex[] vertices, in float3 o, Sprite r, int2 samplers, float paletteTextureIndex, int nv,
public static uint[] CreateQuadIndices(int quads)
{
var indices = new uint[quads * 6];
ReadOnlySpan<uint> cornerVertexMap = stackalloc uint[] { 0, 1, 2, 2, 3, 0 };
for (var i = 0; i < indices.Length; i++)
indices[i] = cornerVertexMap[i % 6] + (uint)(4 * (i / 6));
return indices;
}
public static void FastCreateQuad(Vertex[] vertices, in float3 o, Sprite r, int2 samplers, int paletteTextureIndex, int nv,
in float3 size, in float3 tint, float alpha, float rotation = 0f)
{
float3 a, b, c, d;
@@ -62,7 +73,7 @@ namespace OpenRA.Graphics
public static void FastCreateQuad(Vertex[] vertices,
in float3 a, in float3 b, in float3 c, in float3 d,
Sprite r, int2 samplers, float paletteTextureIndex,
Sprite r, int2 samplers, int paletteTextureIndex,
in float3 tint, float alpha, int nv)
{
float sl = 0;
@@ -84,76 +95,33 @@ namespace OpenRA.Graphics
attribC |= samplers.Y << 9;
}
var fAttribC = (float)attribC;
vertices[nv] = new Vertex(a, r.Left, r.Top, sl, st, paletteTextureIndex, fAttribC, tint, alpha);
vertices[nv + 1] = new Vertex(b, r.Right, r.Top, sr, st, paletteTextureIndex, fAttribC, tint, alpha);
vertices[nv + 2] = new Vertex(c, r.Right, r.Bottom, sr, sb, paletteTextureIndex, fAttribC, tint, alpha);
vertices[nv + 3] = new Vertex(c, r.Right, r.Bottom, sr, sb, paletteTextureIndex, fAttribC, tint, alpha);
vertices[nv + 4] = new Vertex(d, r.Left, r.Bottom, sl, sb, paletteTextureIndex, fAttribC, tint, alpha);
vertices[nv + 5] = new Vertex(a, r.Left, r.Top, sl, st, paletteTextureIndex, fAttribC, tint, alpha);
attribC |= (paletteTextureIndex & 0xFFFF) << 16;
var uAttribC = (uint)attribC;
vertices[nv] = new Vertex(a, r.Left, r.Top, sl, st, uAttribC, tint, alpha);
vertices[nv + 1] = new Vertex(b, r.Right, r.Top, sr, st, uAttribC, tint, alpha);
vertices[nv + 2] = new Vertex(c, r.Right, r.Bottom, sr, sb, uAttribC, tint, alpha);
vertices[nv + 3] = new Vertex(d, r.Left, r.Bottom, sl, sb, uAttribC, tint, alpha);
}
public static void FastCopyIntoChannel(Sprite dest, byte[] src, SpriteFrameType srcType)
public static void FastCopyIntoChannel(Sprite dest, byte[] src, SpriteFrameType srcType, bool premultiplied = false)
{
var destData = dest.Sheet.GetData();
var stride = dest.Sheet.Size.Width;
var x = dest.Bounds.Left;
var y = dest.Bounds.Top;
var width = dest.Bounds.Width;
var height = dest.Bounds.Height;
if (dest.Channel == TextureChannel.RGBA)
{
var destStride = dest.Sheet.Size.Width;
unsafe
{
// Cast the data to an int array so we can copy the src data directly
fixed (byte* bd = &destData[0])
{
var data = (int*)bd;
var x = dest.Bounds.Left;
var y = dest.Bounds.Top;
var k = 0;
for (var j = 0; j < height; j++)
{
for (var i = 0; i < width; i++)
{
byte r, g, b, a;
switch (srcType)
{
case SpriteFrameType.Bgra32:
case SpriteFrameType.Bgr24:
{
b = src[k++];
g = src[k++];
r = src[k++];
a = srcType == SpriteFrameType.Bgra32 ? src[k++] : (byte)255;
break;
}
case SpriteFrameType.Rgba32:
case SpriteFrameType.Rgb24:
{
r = src[k++];
g = src[k++];
b = src[k++];
a = srcType == SpriteFrameType.Rgba32 ? src[k++] : (byte)255;
break;
}
default:
throw new InvalidOperationException($"Unknown SpriteFrameType {srcType}");
}
var cc = Color.FromArgb(a, r, g, b);
data[(y + j) * destStride + x + i] = PremultiplyAlpha(cc).ToArgb();
}
}
}
}
CopyIntoRgba(src, srcType, premultiplied, destData, x, y, width, height, stride);
}
else
{
var destStride = dest.Sheet.Size.Width * 4;
var destOffset = destStride * dest.Bounds.Top + dest.Bounds.Left * 4 + ChannelMasks[(int)dest.Channel];
// Copy into single channel of destination.
var destStride = stride * 4;
var destOffset = destStride * y + x * 4 + ChannelMasks[(int)dest.Channel];
var destSkip = destStride - 4 * width;
var srcOffset = 0;
@@ -170,56 +138,119 @@ namespace OpenRA.Graphics
}
}
static void CopyIntoRgba(
byte[] src, SpriteFrameType srcType, bool premultiplied, byte[] dest, int x, int y, int width, int height, int stride)
{
var si = 0;
var di = y * stride + x;
var d = MemoryMarshal.Cast<byte, uint>(dest);
// SpriteFrameType.Brga32 is a common source format, and it matches the destination format.
// Provide a fast past that just performs memory copies.
if (srcType == SpriteFrameType.Bgra32)
{
var s = MemoryMarshal.Cast<byte, uint>(src);
for (var h = 0; h < height; h++)
{
s[si..(si + width)].CopyTo(d[di..(di + width)]);
if (!premultiplied)
{
for (var w = 0; w < width; w++)
{
d[di] = PremultiplyAlpha(Color.FromArgb(d[di])).ToArgb();
di++;
}
di -= width;
}
si += width;
di += stride;
}
return;
}
for (var h = 0; h < height; h++)
{
for (var w = 0; w < width; w++)
{
byte r, g, b, a;
switch (srcType)
{
case SpriteFrameType.Bgra32:
case SpriteFrameType.Bgr24:
b = src[si++];
g = src[si++];
r = src[si++];
a = srcType == SpriteFrameType.Bgra32 ? src[si++] : byte.MaxValue;
break;
case SpriteFrameType.Rgba32:
case SpriteFrameType.Rgb24:
r = src[si++];
g = src[si++];
b = src[si++];
a = srcType == SpriteFrameType.Rgba32 ? src[si++] : byte.MaxValue;
break;
default:
throw new InvalidOperationException($"Unknown SpriteFrameType {srcType}");
}
var c = Color.FromArgb(a, r, g, b);
if (!premultiplied)
c = PremultiplyAlpha(c);
d[di++] = c.ToArgb();
}
di += stride - width;
}
}
public static void FastCopyIntoSprite(Sprite dest, Png src)
{
var destData = dest.Sheet.GetData();
var destStride = dest.Sheet.Size.Width;
var stride = dest.Sheet.Size.Width;
var x = dest.Bounds.Left;
var y = dest.Bounds.Top;
var width = dest.Bounds.Width;
var height = dest.Bounds.Height;
unsafe
var si = 0;
var di = y * stride + x;
var d = MemoryMarshal.Cast<byte, uint>(destData);
for (var h = 0; h < height; h++)
{
// Cast the data to an int array so we can copy the src data directly
fixed (byte* bd = &destData[0])
for (var w = 0; w < width; w++)
{
var data = (int*)bd;
var x = dest.Bounds.Left;
var y = dest.Bounds.Top;
var k = 0;
for (var j = 0; j < height; j++)
Color c;
switch (src.Type)
{
for (var i = 0; i < width; i++)
{
Color cc;
switch (src.Type)
{
case SpriteFrameType.Indexed8:
{
cc = src.Palette[src.Data[k++]];
break;
}
case SpriteFrameType.Indexed8:
c = src.Palette[src.Data[si++]];
break;
case SpriteFrameType.Rgba32:
case SpriteFrameType.Rgb24:
{
var r = src.Data[k++];
var g = src.Data[k++];
var b = src.Data[k++];
var a = src.Type == SpriteFrameType.Rgba32 ? src.Data[k++] : (byte)255;
cc = Color.FromArgb(a, r, g, b);
break;
}
case SpriteFrameType.Rgba32:
case SpriteFrameType.Rgb24:
var r = src.Data[si++];
var g = src.Data[si++];
var b = src.Data[si++];
var a = src.Type == SpriteFrameType.Rgba32 ? src.Data[si++] : byte.MaxValue;
c = Color.FromArgb(a, r, g, b);
break;
// Pngs don't support BGR[A], so no need to include them here
default:
throw new InvalidOperationException($"Unknown SpriteFrameType {src.Type}");
}
data[(y + j) * destStride + x + i] = PremultiplyAlpha(cc).ToArgb();
}
// PNGs don't support BGR[A], so no need to include them here
default:
throw new InvalidOperationException($"Unknown SpriteFrameType {src.Type}");
}
d[di++] = PremultiplyAlpha(c).ToArgb();
}
di += stride - width;
}
}
@@ -305,239 +336,5 @@ namespace OpenRA.Graphics
(int)((byte)(t * a2 * c2.G + 0.5f) + (1 - t) * (byte)(a1 * c1.G + 0.5f)),
(int)((byte)(t * a2 * c2.B + 0.5f) + (1 - t) * (byte)(a1 * c1.B + 0.5f))));
}
public static float[] IdentityMatrix()
{
return Exts.MakeArray(16, j => (j % 5 == 0) ? 1.0f : 0);
}
public static float[] ScaleMatrix(float sx, float sy, float sz)
{
var mtx = IdentityMatrix();
mtx[0] = sx;
mtx[5] = sy;
mtx[10] = sz;
return mtx;
}
public static float[] TranslationMatrix(float x, float y, float z)
{
var mtx = IdentityMatrix();
mtx[12] = x;
mtx[13] = y;
mtx[14] = z;
return mtx;
}
public static float[] MatrixMultiply(float[] lhs, float[] rhs)
{
var mtx = new float[16];
for (var i = 0; i < 4; i++)
for (var j = 0; j < 4; j++)
{
mtx[4 * i + j] = 0;
for (var k = 0; k < 4; k++)
mtx[4 * i + j] += lhs[4 * k + j] * rhs[4 * i + k];
}
return mtx;
}
public static float[] MatrixVectorMultiply(float[] mtx, float[] vec)
{
var ret = new float[4];
for (var j = 0; j < 4; j++)
{
ret[j] = 0;
for (var k = 0; k < 4; k++)
ret[j] += mtx[4 * k + j] * vec[k];
}
return ret;
}
public static float[] MatrixInverse(float[] m)
{
var mtx = new float[16];
mtx[0] = m[5] * m[10] * m[15] -
m[5] * m[11] * m[14] -
m[9] * m[6] * m[15] +
m[9] * m[7] * m[14] +
m[13] * m[6] * m[11] -
m[13] * m[7] * m[10];
mtx[4] = -m[4] * m[10] * m[15] +
m[4] * m[11] * m[14] +
m[8] * m[6] * m[15] -
m[8] * m[7] * m[14] -
m[12] * m[6] * m[11] +
m[12] * m[7] * m[10];
mtx[8] = m[4] * m[9] * m[15] -
m[4] * m[11] * m[13] -
m[8] * m[5] * m[15] +
m[8] * m[7] * m[13] +
m[12] * m[5] * m[11] -
m[12] * m[7] * m[9];
mtx[12] = -m[4] * m[9] * m[14] +
m[4] * m[10] * m[13] +
m[8] * m[5] * m[14] -
m[8] * m[6] * m[13] -
m[12] * m[5] * m[10] +
m[12] * m[6] * m[9];
mtx[1] = -m[1] * m[10] * m[15] +
m[1] * m[11] * m[14] +
m[9] * m[2] * m[15] -
m[9] * m[3] * m[14] -
m[13] * m[2] * m[11] +
m[13] * m[3] * m[10];
mtx[5] = m[0] * m[10] * m[15] -
m[0] * m[11] * m[14] -
m[8] * m[2] * m[15] +
m[8] * m[3] * m[14] +
m[12] * m[2] * m[11] -
m[12] * m[3] * m[10];
mtx[9] = -m[0] * m[9] * m[15] +
m[0] * m[11] * m[13] +
m[8] * m[1] * m[15] -
m[8] * m[3] * m[13] -
m[12] * m[1] * m[11] +
m[12] * m[3] * m[9];
mtx[13] = m[0] * m[9] * m[14] -
m[0] * m[10] * m[13] -
m[8] * m[1] * m[14] +
m[8] * m[2] * m[13] +
m[12] * m[1] * m[10] -
m[12] * m[2] * m[9];
mtx[2] = m[1] * m[6] * m[15] -
m[1] * m[7] * m[14] -
m[5] * m[2] * m[15] +
m[5] * m[3] * m[14] +
m[13] * m[2] * m[7] -
m[13] * m[3] * m[6];
mtx[6] = -m[0] * m[6] * m[15] +
m[0] * m[7] * m[14] +
m[4] * m[2] * m[15] -
m[4] * m[3] * m[14] -
m[12] * m[2] * m[7] +
m[12] * m[3] * m[6];
mtx[10] = m[0] * m[5] * m[15] -
m[0] * m[7] * m[13] -
m[4] * m[1] * m[15] +
m[4] * m[3] * m[13] +
m[12] * m[1] * m[7] -
m[12] * m[3] * m[5];
mtx[14] = -m[0] * m[5] * m[14] +
m[0] * m[6] * m[13] +
m[4] * m[1] * m[14] -
m[4] * m[2] * m[13] -
m[12] * m[1] * m[6] +
m[12] * m[2] * m[5];
mtx[3] = -m[1] * m[6] * m[11] +
m[1] * m[7] * m[10] +
m[5] * m[2] * m[11] -
m[5] * m[3] * m[10] -
m[9] * m[2] * m[7] +
m[9] * m[3] * m[6];
mtx[7] = m[0] * m[6] * m[11] -
m[0] * m[7] * m[10] -
m[4] * m[2] * m[11] +
m[4] * m[3] * m[10] +
m[8] * m[2] * m[7] -
m[8] * m[3] * m[6];
mtx[11] = -m[0] * m[5] * m[11] +
m[0] * m[7] * m[9] +
m[4] * m[1] * m[11] -
m[4] * m[3] * m[9] -
m[8] * m[1] * m[7] +
m[8] * m[3] * m[5];
mtx[15] = m[0] * m[5] * m[10] -
m[0] * m[6] * m[9] -
m[4] * m[1] * m[10] +
m[4] * m[2] * m[9] +
m[8] * m[1] * m[6] -
m[8] * m[2] * m[5];
var det = m[0] * mtx[0] + m[1] * mtx[4] + m[2] * mtx[8] + m[3] * mtx[12];
if (det == 0)
return null;
for (var i = 0; i < 16; i++)
mtx[i] *= 1 / det;
return mtx;
}
public static float[] MakeFloatMatrix(Int32Matrix4x4 imtx)
{
var multipler = 1f / imtx.M44;
return new[]
{
imtx.M11 * multipler,
imtx.M12 * multipler,
imtx.M13 * multipler,
imtx.M14 * multipler,
imtx.M21 * multipler,
imtx.M22 * multipler,
imtx.M23 * multipler,
imtx.M24 * multipler,
imtx.M31 * multipler,
imtx.M32 * multipler,
imtx.M33 * multipler,
imtx.M34 * multipler,
imtx.M41 * multipler,
imtx.M42 * multipler,
imtx.M43 * multipler,
imtx.M44 * multipler,
};
}
public static float[] MatrixAABBMultiply(float[] mtx, float[] bounds)
{
// Corner offsets
var ix = new uint[] { 0, 0, 0, 0, 3, 3, 3, 3 };
var iy = new uint[] { 1, 1, 4, 4, 1, 1, 4, 4 };
var iz = new uint[] { 2, 5, 2, 5, 2, 5, 2, 5 };
// Vectors to opposing corner
var ret = new[]
{
float.MaxValue, float.MaxValue, float.MaxValue,
float.MinValue, float.MinValue, float.MinValue
};
// Transform vectors and find new bounding box
for (var i = 0; i < 8; i++)
{
var vec = new[] { bounds[ix[i]], bounds[iy[i]], bounds[iz[i]], 1 };
var tvec = MatrixVectorMultiply(mtx, vec);
ret[0] = Math.Min(ret[0], tvec[0] / tvec[3]);
ret[1] = Math.Min(ret[1], tvec[1] / tvec[3]);
ret[2] = Math.Min(ret[2], tvec[2] / tvec[3]);
ret[3] = Math.Max(ret[3], tvec[0] / tvec[3]);
ret[4] = Math.Max(ret[4], tvec[1] / tvec[3]);
ret[5] = Math.Max(ret[5], tvec[2] / tvec[3]);
}
return ret;
}
}
}

View File

@@ -23,27 +23,42 @@ namespace OpenRA.Graphics
public readonly float S, T, U, V;
// Palette and channel flags
public readonly float P, C;
public readonly uint C;
// Color tint
public readonly float R, G, B, A;
public Vertex(in float3 xyz, float s, float t, float u, float v, float p, float c)
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, p, c, float3.Ones, 1f) { }
public Vertex(in float3 xyz, float s, float t, float u, float v, uint c)
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, c, float3.Ones, 1f) { }
public Vertex(in float3 xyz, float s, float t, float u, float v, float p, float c, in float3 tint, float a)
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, p, c, tint.X, tint.Y, tint.Z, a) { }
public Vertex(in float3 xyz, float s, float t, float u, float v, uint c, in float3 tint, float a)
: this(xyz.X, xyz.Y, xyz.Z, s, t, u, v, c, tint.X, tint.Y, tint.Z, a) { }
public Vertex(float x, float y, float z, float s, float t, float u, float v, float p, float c, in float3 tint, float a)
: this(x, y, z, s, t, u, v, p, c, tint.X, tint.Y, tint.Z, a) { }
public Vertex(float x, float y, float z, float s, float t, float u, float v, uint c, in float3 tint, float a)
: this(x, y, z, s, t, u, v, c, tint.X, tint.Y, tint.Z, a) { }
public Vertex(float x, float y, float z, float s, float t, float u, float v, float p, float c, float r, float g, float b, float a)
public Vertex(float x, float y, float z, float s, float t, float u, float v, uint c, float r, float g, float b, float a)
{
X = x; Y = y; Z = z;
S = s; T = t;
U = u; V = v;
P = p; C = c;
C = c;
R = r; G = g; B = b; A = a;
}
}
public sealed class CombinedShaderBindings : ShaderBindings
{
public CombinedShaderBindings()
: base("combined")
{ }
public override ShaderVertexAttribute[] Attributes { get; } = new[]
{
new ShaderVertexAttribute("aVertexPosition", ShaderVertexAttributeType.Float, 3, 0),
new ShaderVertexAttribute("aVertexTexCoord", ShaderVertexAttributeType.Float, 4, 12),
new ShaderVertexAttribute("aVertexAttributes", ShaderVertexAttributeType.UInt, 1, 28),
new ShaderVertexAttribute("aVertexTint", ShaderVertexAttributeType.Float, 4, 32)
};
}
}

View File

@@ -172,7 +172,7 @@ namespace OpenRA.Graphics
UpdateViewportZooms();
}
float CalculateMinimumZoom(float minHeight, float maxHeight)
static float CalculateMinimumZoom(float minHeight, float maxHeight)
{
var h = Game.Renderer.NativeResolution.Height;
@@ -282,11 +282,23 @@ namespace OpenRA.Graphics
IEnumerable<MPos> CandidateMouseoverCells(int2 world)
{
var map = worldRenderer.World.Map;
var tileScale = map.Grid.TileScale / 2;
var minPos = worldRenderer.ProjectedPosition(world);
// Find all the cells that could potentially have been clicked
var a = map.CellContaining(minPos - new WVec(1024, 0, 0)).ToMPos(map.Grid.Type);
var b = map.CellContaining(minPos + new WVec(512, 512 * map.Grid.MaximumTerrainHeight, 0)).ToMPos(map.Grid.Type);
// Find all the cells that could potentially have been clicked.
MPos a;
MPos b;
if (map.Grid.Type == MapGridType.RectangularIsometric)
{
// TODO: this generates too many cells.
a = map.CellContaining(minPos - new WVec(tileScale, 0, 0)).ToMPos(map.Grid.Type);
b = map.CellContaining(minPos + new WVec(tileScale, tileScale * map.Grid.MaximumTerrainHeight, 0)).ToMPos(map.Grid.Type);
}
else
{
a = map.CellContaining(minPos).ToMPos(map.Grid.Type);
b = map.CellContaining(minPos + new WVec(0, tileScale * map.Grid.MaximumTerrainHeight, 0)).ToMPos(map.Grid.Type);
}
for (var v = b.V; v >= a.V; v--)
for (var u = b.U; u >= a.U; u--)
@@ -299,10 +311,13 @@ namespace OpenRA.Graphics
public void Center(IEnumerable<Actor> actors)
{
if (!actors.Any())
var actorsCollection = actors as IReadOnlyCollection<Actor>;
actorsCollection ??= actors.ToList();
if (actorsCollection.Count == 0)
return;
Center(actors.Select(a => a.CenterPosition).Average());
Center(actorsCollection.Select(a => a.CenterPosition).Average());
}
public void Center(WPos pos)

View File

@@ -44,12 +44,14 @@ namespace OpenRA.Graphics
readonly List<IFinalizedRenderable> preparedAnnotationRenderables = new();
readonly List<IRenderable> renderablesBuffer = new();
readonly IRenderer[] renderers;
readonly IRenderPostProcessPass[] postProcessPasses;
internal WorldRenderer(ModData modData, World world)
{
World = world;
TileSize = World.Map.Grid.TileSize;
TileScale = World.Map.Grid.Type == MapGridType.RectangularIsometric ? 1448 : 1024;
TileScale = World.Map.Grid.TileScale;
Viewport = new Viewport(this, world.Map);
createPaletteReference = CreatePaletteReference;
@@ -66,9 +68,24 @@ namespace OpenRA.Graphics
palette.Initialize();
TerrainLighting = world.WorldActor.TraitOrDefault<ITerrainLighting>();
renderers = world.WorldActor.TraitsImplementing<IRenderer>().ToArray();
terrainRenderer = world.WorldActor.TraitOrDefault<IRenderTerrain>();
debugVis = Exts.Lazy(() => world.WorldActor.TraitOrDefault<DebugVisualizations>());
postProcessPasses = world.WorldActor.TraitsImplementing<IRenderPostProcessPass>().ToArray();
}
public void BeginFrame()
{
foreach (var r in renderers)
r.BeginFrame();
}
public void EndFrame()
{
foreach (var r in renderers)
r.EndFrame();
}
public void UpdatePalettesForPlayer(string internalName, Color color, bool replaceExisting)
@@ -87,7 +104,7 @@ namespace OpenRA.Graphics
{
// HACK: This is working around the fact that palettes are defined on traits rather than sequences
// and can be removed once this has been fixed.
return name == null ? null : palettes.GetOrAdd(name, createPaletteReference);
return string.IsNullOrEmpty(name) ? null : palettes.GetOrAdd(name, createPaletteReference);
}
public void AddPalette(string name, ImmutablePalette pal, bool allowModifiers = false, bool allowOverwrite = false)
@@ -109,8 +126,8 @@ namespace OpenRA.Graphics
palette.ReplacePalette(name, pal);
// Update cached PlayerReference if one exists
if (palettes.ContainsKey(name))
palettes[name].Palette = pal;
if (palettes.TryGetValue(name, out var paletteReference))
paletteReference.Palette = pal;
}
public void SetPaletteColorShift(string name, float hueOffset, float satOffset, float valueModifier, float minHue, float maxHue)
@@ -270,6 +287,8 @@ namespace OpenRA.Graphics
if (enableDepthBuffer)
Game.Renderer.ClearDepthBuffer();
ApplyPostProcessing(PostProcessPassType.AfterActors);
World.ApplyToActorsWithTrait<IRenderAboveWorld>((actor, trait) =>
{
if (actor.IsInWorld && !actor.Disposed)
@@ -279,6 +298,8 @@ namespace OpenRA.Graphics
if (enableDepthBuffer)
Game.Renderer.ClearDepthBuffer();
ApplyPostProcessing(PostProcessPassType.AfterWorld);
World.ApplyToActorsWithTrait<IRenderShroud>((actor, trait) => trait.RenderShroud(this));
if (enableDepthBuffer)
@@ -292,9 +313,23 @@ namespace OpenRA.Graphics
foreach (var r in g)
r.Render(this);
ApplyPostProcessing(PostProcessPassType.AfterShroud);
Game.Renderer.Flush();
}
void ApplyPostProcessing(PostProcessPassType type)
{
foreach (var pass in postProcessPasses)
{
if (pass.Type != type || !pass.Enabled)
continue;
Game.Renderer.Flush();
pass.Draw(this);
}
}
public void DrawAnnotations()
{
Game.Renderer.EnableAntialiasingFilter();

View File

@@ -10,7 +10,6 @@
#endregion
using System.Collections.Generic;
using System.Linq;
namespace OpenRA
{
@@ -31,29 +30,26 @@ namespace OpenRA
if (!string.IsNullOrEmpty(node.Value))
Default = FieldLoader.GetValue<Hotkey>("value", node.Value);
var descriptionNode = node.Nodes.FirstOrDefault(n => n.Key == "Description");
if (descriptionNode != null)
Description = descriptionNode.Value.Value;
var nodeDict = node.ToDictionary();
var typesNode = node.Nodes.FirstOrDefault(n => n.Key == "Types");
if (typesNode != null)
Types = FieldLoader.GetValue<HashSet<string>>("Types", typesNode.Value.Value);
if (nodeDict.TryGetValue("Description", out var descriptionYaml))
Description = descriptionYaml.Value;
var contextsNode = node.Nodes.FirstOrDefault(n => n.Key == "Contexts");
if (contextsNode != null)
Contexts = FieldLoader.GetValue<HashSet<string>>("Contexts", contextsNode.Value.Value);
if (nodeDict.TryGetValue("Types", out var typesYaml))
Types = FieldLoader.GetValue<HashSet<string>>("Types", typesYaml.Value);
var platformNode = node.Nodes.FirstOrDefault(n => n.Key == "Platform");
if (platformNode != null)
if (nodeDict.TryGetValue("Contexts", out var contextYaml))
Contexts = FieldLoader.GetValue<HashSet<string>>("Contexts", contextYaml.Value);
if (nodeDict.TryGetValue("Platform", out var platformYaml))
{
var platformOverride = platformNode.Value.Nodes.FirstOrDefault(n => n.Key == Platform.CurrentPlatform.ToString());
var platformOverride = platformYaml.NodeWithKeyOrDefault(Platform.CurrentPlatform.ToString());
if (platformOverride != null)
Default = FieldLoader.GetValue<Hotkey>("value", platformOverride.Value.Value);
}
var readonlyNode = node.Nodes.FirstOrDefault(n => n.Key == "Readonly");
if (readonlyNode != null)
Readonly = FieldLoader.GetValue<bool>("Readonly", readonlyNode.Value.Value);
if (nodeDict.TryGetValue("Readonly", out var readonlyYaml))
Readonly = FieldLoader.GetValue<bool>("Readonly", readonlyYaml.Value);
}
}
}

View File

@@ -35,7 +35,7 @@ namespace OpenRA
foreach (var kv in settings)
{
if (definitions.ContainsKey(kv.Key) && !definitions[kv.Key].Readonly)
if (definitions.TryGetValue(kv.Key, out var definition) && !definition.Readonly)
keys[kv.Key] = kv.Value;
}
@@ -43,6 +43,9 @@ namespace OpenRA
hd.Value.HasDuplicates = GetFirstDuplicate(hd.Value, this[hd.Value.Name].GetValue()) != null;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Performance", "CA1854:Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method",
Justification = "Func must perform a live lookup in the collection, as the lookup value can change.")]
internal Func<Hotkey> GetHotkeyReference(string name)
{
// Is this a mod-defined hotkey?

View File

@@ -13,7 +13,8 @@ using System.Collections.Generic;
namespace OpenRA
{
// List of keycodes, duplicated from SDL 2.0.1
// List of keycodes. Duplicated from SDL 2.0.1, with the addition
// of MOUSE4 and MOUSE5.
public enum Keycode
{
UNKNOWN = 0,
@@ -252,6 +253,8 @@ namespace OpenRA
KBDILLUMUP = 280 | (1 << 30),
EJECT = 281 | (1 << 30),
SLEEP = 282 | (1 << 30),
MOUSE4 = 283 | (1 << 30),
MOUSE5 = 284 | (1 << 30)
}
public static class KeycodeExts
@@ -494,6 +497,8 @@ namespace OpenRA
{ Keycode.KBDILLUMUP, "KBDIllumUp" },
{ Keycode.EJECT, "Eject" },
{ Keycode.SLEEP, "Sleep" },
{ Keycode.MOUSE4, "Mouse 4" },
{ Keycode.MOUSE5, "Mouse 5" },
};
public static string DisplayString(Keycode k)

View File

@@ -54,7 +54,7 @@ namespace OpenRA
return mods;
}
Manifest LoadMod(string id, string path)
static Manifest LoadMod(string id, string path)
{
IReadOnlyPackage package = null;
try
@@ -79,7 +79,7 @@ namespace OpenRA
return null;
}
Dictionary<string, Manifest> GetInstalledMods(IEnumerable<string> searchPaths, IEnumerable<string> explicitPaths)
static Dictionary<string, Manifest> GetInstalledMods(IEnumerable<string> searchPaths, IEnumerable<string> explicitPaths)
{
var ret = new Dictionary<string, Manifest>();
var candidates = GetCandidateMods(searchPaths)

View File

@@ -66,8 +66,10 @@ namespace OpenRA
}
catch (Exception e)
{
Console.WriteLine("Failed to load keys: {0}", e);
Log.Write("debug", "Failed to load player keypair from `{0}` with exception: {1}", filePath, e);
Console.WriteLine("Failed to load keys:");
Console.WriteLine(e);
Log.Write("debug", $"Failed to load player keypair from `{filePath}` with exception:");
Log.Write("debug", e);
}
}
@@ -82,16 +84,17 @@ namespace OpenRA
{
var client = HttpClientFactory.Create();
var httpResponseMessage = await client.GetAsync(playerDatabase.Profile + Fingerprint);
var url = playerDatabase.Profile + Fingerprint;
var httpResponseMessage = await client.GetAsync(url);
var result = await httpResponseMessage.Content.ReadAsStreamAsync();
var yaml = MiniYaml.FromStream(result).First();
var yaml = MiniYaml.FromStream(result, url).First();
if (yaml.Key == "Player")
{
innerData = FieldLoader.Load<PlayerProfile>(yaml.Value);
if (innerData.KeyRevoked)
{
Log.Write("debug", "Revoking key with fingerprint {0}", Fingerprint);
Log.Write("debug", $"Revoking key with fingerprint {Fingerprint}");
DeleteKeypair();
}
else
@@ -102,7 +105,8 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to parse player data result with exception: {0}", e);
Log.Write("debug", "Failed to parse player data result with exception:");
Log.Write("debug", e);
innerState = LinkState.ConnectionFailed;
}
finally
@@ -136,8 +140,10 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to generate keypair with exception: {1}", e);
Console.WriteLine("Key generation failed: {0}", e);
Log.Write("debug", "Failed to generate keypair with exception:");
Log.Write("debug", e);
Console.WriteLine("Key generation failed:");
Console.WriteLine(e);
innerState = LinkState.Uninitialized;
}
@@ -152,8 +158,10 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to delete keypair with exception: {1}", e);
Console.WriteLine("Key deletion failed: {0}", e);
Log.Write("debug", "Failed to delete keypair with exception:");
Log.Write("debug", e);
Console.WriteLine("Key deletion failed:");
Console.WriteLine(e);
}
innerState = LinkState.Uninitialized;

View File

@@ -1,26 +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
namespace OpenRA
{
public interface ILog
{
void Write(string channel, string format, params object[] args);
}
public class LogProxy : ILog
{
public void Write(string channel, string format, params object[] args)
{
Log.Write(channel, format, args);
}
}
}

View File

@@ -43,17 +43,6 @@ namespace OpenRA
}
}
public sealed class ModelSequenceFormat : IGlobalModData
{
public readonly string Type;
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
public ModelSequenceFormat(MiniYaml yaml)
{
Type = yaml.Value;
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
}
}
public class ModMetadata
{
public string Title;
@@ -65,7 +54,7 @@ namespace OpenRA
}
/// <summary>Describes what is to be loaded in order to run a mod.</summary>
public class Manifest : IDisposable
public sealed class Manifest : IDisposable
{
public readonly string Id;
public readonly IReadOnlyPackage Package;
@@ -105,7 +94,8 @@ namespace OpenRA
Id = modId;
Package = package;
var nodes = MiniYaml.FromStream(package.GetStream("mod.yaml"), "mod.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);
for (var i = nodes.Count - 1; i >= 0; i--)
{
if (nodes[i].Key != "Include")
@@ -118,7 +108,7 @@ namespace OpenRA
throw new YamlException($"{nodes[i].Location}: File `{filename}` not found.");
nodes.RemoveAt(i);
nodes.InsertRange(i, MiniYaml.FromStream(contents, filename));
nodes.InsertRange(i, MiniYaml.FromStream(contents, $"{package.Name}:{filename}", stringPool: stringPool));
}
// Merge inherited overrides
@@ -157,25 +147,25 @@ namespace OpenRA
// Allow inherited mods to import parent maps.
var compat = new List<string> { Id };
if (yaml.ContainsKey("SupportsMapsFrom"))
compat.AddRange(yaml["SupportsMapsFrom"].Value.Split(',').Select(c => c.Trim()));
if (yaml.TryGetValue("SupportsMapsFrom", out var entry))
compat.AddRange(entry.Value.Split(',').Select(c => c.Trim()));
MapCompatibility = compat.ToArray();
if (yaml.ContainsKey("DefaultOrderGenerator"))
DefaultOrderGenerator = yaml["DefaultOrderGenerator"].Value;
if (yaml.TryGetValue("DefaultOrderGenerator", out entry))
DefaultOrderGenerator = entry.Value;
if (yaml.ContainsKey("PackageFormats"))
PackageFormats = FieldLoader.GetValue<string[]>("PackageFormats", yaml["PackageFormats"].Value);
if (yaml.TryGetValue("PackageFormats", out entry))
PackageFormats = FieldLoader.GetValue<string[]>("PackageFormats", entry.Value);
if (yaml.ContainsKey("SoundFormats"))
SoundFormats = FieldLoader.GetValue<string[]>("SoundFormats", yaml["SoundFormats"].Value);
if (yaml.TryGetValue("SoundFormats", out entry))
SoundFormats = FieldLoader.GetValue<string[]>("SoundFormats", entry.Value);
if (yaml.ContainsKey("SpriteFormats"))
SpriteFormats = FieldLoader.GetValue<string[]>("SpriteFormats", yaml["SpriteFormats"].Value);
if (yaml.TryGetValue("SpriteFormats", out entry))
SpriteFormats = FieldLoader.GetValue<string[]>("SpriteFormats", entry.Value);
if (yaml.ContainsKey("VideoFormats"))
VideoFormats = FieldLoader.GetValue<string[]>("VideoFormats", yaml["VideoFormats"].Value);
if (yaml.TryGetValue("VideoFormats", out entry))
VideoFormats = FieldLoader.GetValue<string[]>("VideoFormats", entry.Value);
}
public void LoadCustomData(ObjectCreator oc)
@@ -211,18 +201,18 @@ namespace OpenRA
static string[] YamlList(Dictionary<string, MiniYaml> yaml, string key)
{
if (!yaml.ContainsKey(key))
if (!yaml.TryGetValue(key, out var value))
return Array.Empty<string>();
return yaml[key].ToDictionary().Keys.ToArray();
return value.Nodes.Select(n => n.Key).ToArray();
}
static IReadOnlyDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
{
if (!yaml.ContainsKey(key))
if (!yaml.TryGetValue(key, out var value))
return new Dictionary<string, string>();
return yaml[key].ToDictionary(my => my.Value);
return value.ToDictionary(my => my.Value);
}
public bool Contains<T>() where T : IGlobalModData

View File

@@ -175,8 +175,7 @@ namespace OpenRA
protected CompositeActorInit(TraitInfo info)
: base(info.InstanceName) { }
protected CompositeActorInit()
: base() { }
protected CompositeActorInit() { }
public virtual void Initialize(MiniYaml yaml)
{

View File

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

View File

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

View File

@@ -24,10 +24,10 @@ namespace OpenRA
protected readonly T[] Entries;
protected readonly Rectangle Bounds;
public CellLayerBase(Map map)
protected CellLayerBase(Map map)
: this(map.Grid.Type, new Size(map.MapSize.X, map.MapSize.Y)) { }
public CellLayerBase(MapGridType gridType, Size size)
protected CellLayerBase(MapGridType gridType, Size size)
{
Size = size;
Bounds = new Rectangle(0, 0, Size.Width, Size.Height);

View File

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

View File

@@ -11,6 +11,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -65,7 +66,7 @@ namespace OpenRA
MissionSelector = 4
}
class MapField
sealed class MapField
{
enum Type { Normal, NodeList, MiniYaml }
readonly FieldInfo field;
@@ -90,13 +91,13 @@ namespace OpenRA
throw new InvalidOperationException("Map does not have a field/property " + fieldName);
var t = field != null ? field.FieldType : property.PropertyType;
type = t == typeof(List<MiniYamlNode>) ? Type.NodeList :
type = t == typeof(IReadOnlyCollection<MiniYamlNode>) ? Type.NodeList :
t == typeof(MiniYaml) ? Type.MiniYaml : Type.Normal;
}
public void Deserialize(Map map, List<MiniYamlNode> nodes)
public void Deserialize(Map map, MiniYaml yaml)
{
var node = nodes.FirstOrDefault(n => n.Key == key);
var node = yaml.NodeWithKeyOrDefault(key);
if (node == null)
{
if (required)
@@ -130,14 +131,14 @@ namespace OpenRA
var value = field != null ? field.GetValue(map) : property.GetValue(map, null);
if (type == Type.NodeList)
{
var listValue = (List<MiniYamlNode>)value;
var listValue = (IReadOnlyCollection<MiniYamlNode>)value;
if (required || listValue.Count > 0)
nodes.Add(new MiniYamlNode(key, null, listValue));
}
else if (type == Type.MiniYaml)
{
var yamlValue = (MiniYaml)value;
if (required || (yamlValue != null && (yamlValue.Value != null || yamlValue.Nodes.Count > 0)))
if (required || (yamlValue != null && (yamlValue.Value != null || yamlValue.Nodes.Length > 0)))
nodes.Add(new MiniYamlNode(key, yamlValue));
}
else
@@ -149,7 +150,7 @@ namespace OpenRA
}
}
public class Map : IReadOnlyFileSystem, IDisposable
public sealed class Map : IReadOnlyFileSystem, IDisposable
{
public const int SupportedMapFormat = 11;
public const int CurrentMapFormat = 12;
@@ -158,26 +159,26 @@ namespace OpenRA
/// <summary>Defines the order of the fields in map.yaml.</summary>
static readonly MapField[] YamlFields =
{
new MapField("MapFormat"),
new MapField("RequiresMod"),
new MapField("Title"),
new MapField("Author"),
new MapField("Tileset"),
new MapField("MapSize"),
new MapField("Bounds"),
new MapField("Visibility"),
new MapField("Categories"),
new MapField("Translations", required: false, ignoreIfValue: ""),
new MapField("LockPreview", required: false, ignoreIfValue: "False"),
new MapField("Players", "PlayerDefinitions"),
new MapField("Actors", "ActorDefinitions"),
new MapField("Rules", "RuleDefinitions", required: false),
new MapField("Sequences", "SequenceDefinitions", required: false),
new MapField("ModelSequences", "ModelSequenceDefinitions", required: false),
new MapField("Weapons", "WeaponDefinitions", required: false),
new MapField("Voices", "VoiceDefinitions", required: false),
new MapField("Music", "MusicDefinitions", required: false),
new MapField("Notifications", "NotificationDefinitions", required: false),
new("MapFormat"),
new("RequiresMod"),
new("Title"),
new("Author"),
new("Tileset"),
new("MapSize"),
new("Bounds"),
new("Visibility"),
new("Categories"),
new("LockPreview", required: false, ignoreIfValue: "False"),
new("Players", "PlayerDefinitions"),
new("Actors", "ActorDefinitions"),
new("Rules", "RuleDefinitions", required: false),
new("Translations", "TranslationDefinitions", required: false),
new("Sequences", "SequenceDefinitions", required: false),
new("ModelSequences", "ModelSequenceDefinitions", required: false),
new("Weapons", "WeaponDefinitions", required: false),
new("Voices", "VoiceDefinitions", required: false),
new("Music", "MusicDefinitions", required: false),
new("Notifications", "NotificationDefinitions", required: false),
};
// Format versions
@@ -193,22 +194,22 @@ namespace OpenRA
public Rectangle Bounds;
public MapVisibility Visibility = MapVisibility.Lobby;
public string[] Categories = { "Conquest" };
public string[] Translations;
public int2 MapSize { get; private set; }
// Player and actor yaml. Public for access by the map importers and lint checks.
public List<MiniYamlNode> PlayerDefinitions = new();
public List<MiniYamlNode> ActorDefinitions = new();
public IReadOnlyCollection<MiniYamlNode> PlayerDefinitions = ImmutableArray<MiniYamlNode>.Empty;
public IReadOnlyCollection<MiniYamlNode> ActorDefinitions = ImmutableArray<MiniYamlNode>.Empty;
// Custom map yaml. Public for access by the map importers and lint checks
public readonly MiniYaml RuleDefinitions;
public readonly MiniYaml SequenceDefinitions;
public readonly MiniYaml ModelSequenceDefinitions;
public readonly MiniYaml WeaponDefinitions;
public readonly MiniYaml VoiceDefinitions;
public readonly MiniYaml MusicDefinitions;
public readonly MiniYaml NotificationDefinitions;
public MiniYaml RuleDefinitions;
public MiniYaml TranslationDefinitions;
public MiniYaml SequenceDefinitions;
public MiniYaml ModelSequenceDefinitions;
public MiniYaml WeaponDefinitions;
public MiniYaml VoiceDefinitions;
public MiniYaml MusicDefinitions;
public MiniYaml NotificationDefinitions;
public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = new();
@@ -256,8 +257,6 @@ namespace OpenRA
CellLayer<byte> projectedHeight;
Rectangle projectionSafeBounds;
internal Translation Translation;
public static string ComputeUID(IReadOnlyPackage package)
{
return ComputeUID(package, GetMapFormat(package));
@@ -276,7 +275,10 @@ namespace OpenRA
try
{
foreach (var filename in contents)
if (filename.EndsWith(".yaml") || filename.EndsWith(".bin") || filename.EndsWith(".lua") || (format >= 12 && filename == "map.png"))
if (filename.EndsWith(".yaml", StringComparison.Ordinal) ||
filename.EndsWith(".bin", StringComparison.Ordinal) ||
filename.EndsWith(".lua", StringComparison.Ordinal) ||
(format >= 12 && filename == "map.png"))
streams.Add(package.GetStream(filename));
// Take the SHA1
@@ -359,15 +361,15 @@ namespace OpenRA
if (!Package.Contains("map.yaml") || !Package.Contains("map.bin"))
throw new InvalidDataException($"Not a valid map\n File: {package.Name}");
var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), package.Name));
var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), $"{package.Name}:map.yaml"));
foreach (var field in YamlFields)
field.Deserialize(this, yaml.Nodes);
field.Deserialize(this, yaml);
if (MapFormat < SupportedMapFormat)
throw new InvalidDataException($"Map format {MapFormat} is not supported.\n File: {package.Name}");
PlayerDefinitions = MiniYaml.NodesOrEmpty(yaml, "Players");
ActorDefinitions = MiniYaml.NodesOrEmpty(yaml, "Actors");
PlayerDefinitions = yaml.NodeWithKeyOrDefault("Players")?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty;
ActorDefinitions = yaml.NodeWithKeyOrDefault("Actors")?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty;
Grid = modData.Manifest.Get<MapGrid>();
@@ -443,14 +445,14 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to load rules for {0} with error {1}", Title, e);
Log.Write("debug", $"Failed to load rules for {Title} with error");
Log.Write("debug", e);
InvalidCustomRules = true;
InvalidCustomRulesException = e;
Rules = Ruleset.LoadDefaultsForTileSet(modData, Tileset);
}
Sequences = new SequenceSet(this, modData, Tileset, SequenceDefinitions);
Translation = new Translation(Game.Settings.Player.Language, Translations, this);
var tl = new MPos(0, 0).ToCPos(this);
var br = new MPos(MapSize.X - 1, MapSize.Y - 1).ToCPos(this);
@@ -609,7 +611,7 @@ namespace OpenRA
// Odd-height ramps get bumped up a level to the next even height layer
if ((height & 1) == 1 && Ramp[uv] != 0)
height += 1;
height++;
var candidates = new List<PPos>();
@@ -648,21 +650,24 @@ namespace OpenRA
foreach (var file in Package.Contents)
toPackage.Update(file, Package.GetStream(file).ReadAllBytes());
if (!LockPreview)
void UpdatePackage(string filename, byte[] data)
{
var previewData = SavePreview();
if (Package != toPackage || !Enumerable.SequenceEqual(previewData, Package.GetStream("map.png").ReadAllBytes()))
toPackage.Update("map.png", previewData);
if (Package != toPackage)
toPackage.Update(filename, data);
else
{
var stream = Package.GetStream(filename);
if (stream == null || !Enumerable.SequenceEqual(data, stream.ReadAllBytes()))
toPackage.Update(filename, data);
}
}
// Update the package with the new map data
var textData = Encoding.UTF8.GetBytes(root.WriteToString());
if (Package != toPackage || !Enumerable.SequenceEqual(textData, Package.GetStream("map.yaml").ReadAllBytes()))
toPackage.Update("map.yaml", textData);
if (!LockPreview)
UpdatePackage("map.png", SavePreview());
var binaryData = SaveBinaryData();
if (Package != toPackage || !Enumerable.SequenceEqual(binaryData, Package.GetStream("map.bin").ReadAllBytes()))
toPackage.Update("map.bin", binaryData);
// Update the package with the new map data
UpdatePackage("map.yaml", Encoding.UTF8.GetBytes(root.WriteToString()));
UpdatePackage("map.bin", SaveBinaryData());
Package = toPackage;
@@ -683,16 +688,16 @@ namespace OpenRA
writer.Write((ushort)MapSize.Y);
// Data offsets
var tilesOffset = 17;
const int TilesOffset = 17;
var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.X * MapSize.Y + 17 : 0;
var resourcesOffset = (Grid.MaximumTerrainHeight > 0 ? 4 : 3) * MapSize.X * MapSize.Y + 17;
writer.Write((uint)tilesOffset);
writer.Write((uint)TilesOffset);
writer.Write((uint)heightsOffset);
writer.Write((uint)resourcesOffset);
// Tile data
if (tilesOffset != 0)
if (TilesOffset != 0)
{
for (var i = 0; i < MapSize.X; i++)
{
@@ -750,7 +755,7 @@ namespace OpenRA
{
var actorTypes = Rules.Actors.Values.Where(a => a.HasTraitInfo<IMapPreviewSignatureInfo>());
var actors = ActorDefinitions.Where(a => actorTypes.Any(ai => ai.Name == a.Value.Value));
var positions = new List<(MPos Position, Color Color)>();
var positions = new List<(MPos Uv, Color Color)>();
foreach (var actor in actors)
{
var s = new ActorReference(actor.Value.Value, actor.Value.ToDictionary());
@@ -774,19 +779,10 @@ namespace OpenRA
if (Grid.MaximumTerrainHeight > 0)
{
// The minimap is drawn in cell space, so we need to
// unproject the PPos bounds to find the MPos boundaries.
// This matches the calculation in RadarWidget that is used ingame
for (var x = Bounds.Left; x < Bounds.Right; x++)
{
var allTop = Unproject(new PPos(x, Bounds.Top));
var allBottom = Unproject(new PPos(x, Bounds.Bottom));
if (allTop.Count > 0)
top = Math.Min(top, allTop.MinBy(uv => uv.V).V);
(top, bottom) = GetCellSpaceBounds();
if (allBottom.Count > 0)
bottom = Math.Max(bottom, allBottom.MaxBy(uv => uv.V).V);
}
if (top == int.MaxValue || bottom == int.MinValue)
throw new InvalidDataException("The map has invalid boundaries");
}
else
{
@@ -803,18 +799,21 @@ namespace OpenRA
bitmapWidth = 2 * bitmapWidth - 1;
var stride = bitmapWidth * 4;
var pxStride = 4;
const int PxStride = 4;
var minimapData = new byte[stride * height];
(Color Left, Color Right) terrainColor = default;
var colorsByPosition = positions
.GroupBy(p => p.Uv)
.ToDictionary(g => g.Key, g => g.First().Color);
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
var uv = new MPos(x + Bounds.Left, y + top);
// FirstOrDefault will return a (MPos.Zero, Color.Transparent) if positions is empty
var actorColor = positions.FirstOrDefault(ap => ap.Position == uv).Color;
// TryGetValue will return Color.Transparent if not found
colorsByPosition.TryGetValue(uv, out var actorColor);
if (actorColor.A == 0)
terrainColor = GetTerrainColorPair(uv);
@@ -822,10 +821,10 @@ namespace OpenRA
{
// Odd rows are shifted right by 1px
var dx = uv.V & 1;
var xOffset = pxStride * (2 * x + dx);
var xOffset = PxStride * (2 * x + dx);
if (x + dx > 0)
{
var z = y * stride + xOffset - pxStride;
var z = y * stride + xOffset - PxStride;
var c = actorColor.A == 0 ? terrainColor.Left : actorColor;
minimapData[z++] = c.R;
minimapData[z++] = c.G;
@@ -845,7 +844,7 @@ namespace OpenRA
}
else
{
var z = y * stride + pxStride * x;
var z = y * stride + PxStride * x;
var c = actorColor.A == 0 ? terrainColor.Left : actorColor;
minimapData[z++] = c.R;
minimapData[z++] = c.G;
@@ -859,6 +858,28 @@ namespace OpenRA
return png.Save();
}
public (int Top, int Bottom) GetCellSpaceBounds()
{
var top = int.MaxValue;
var bottom = int.MinValue;
// The minimap is drawn in cell space, so we need to
// unproject the PPos bounds to find the MPos boundaries.
// This matches the calculation in RadarWidget that is used ingame
for (var x = Bounds.Left; x < Bounds.Right; x++)
{
var allTop = Unproject(new PPos(x, Bounds.Top));
var allBottom = Unproject(new PPos(x, Bounds.Bottom));
if (allTop.Count > 0)
top = Math.Min(top, allTop.MinBy(uv => uv.V).V);
if (allBottom.Count > 0)
bottom = Math.Max(bottom, allBottom.MaxBy(uv => uv.V).V);
}
return (top, bottom);
}
public bool Contains(CPos cell)
{
if (Grid.Type == MapGridType.RectangularIsometric)
@@ -1179,7 +1200,7 @@ namespace OpenRA
// Project this guessed cell and take the first available cell
// If it is projected outside the layer, then make another guess.
var allProjected = ProjectedCellsCovering(uv);
var projected = allProjected.Length > 0 ? allProjected.First()
var projected = allProjected.Length > 0 ? allProjected[0]
: new PPos(uv.U, uv.V.Clamp(Bounds.Top, Bounds.Bottom));
// Clamp the projected cell to the map area
@@ -1207,7 +1228,7 @@ namespace OpenRA
// This shouldn't happen. But if it does, return the original value and hope the caller doesn't explode.
if (unProjected.Count == 0)
{
Log.Write("debug", "Failed to clamp map cell {0} to map bounds", uv);
Log.Write("debug", $"Failed to clamp map cell {uv} to map bounds");
return uv;
}
}
@@ -1248,7 +1269,7 @@ namespace OpenRA
PPos edge;
if (allProjected.Length > 0)
{
var puv = allProjected.First();
var puv = allProjected[0];
var horizontalBound = (puv.U - Bounds.Left < Bounds.Width / 2) ? Bounds.Left : Bounds.Right;
var verticalBound = (puv.V - Bounds.Top < Bounds.Height / 2) ? Bounds.Top : Bounds.Bottom;
@@ -1279,7 +1300,7 @@ namespace OpenRA
// This shouldn't happen. But if it does, return the original value and hope the caller doesn't explode.
if (unProjected.Count == 0)
{
Log.Write("debug", "Failed to find closest edge for map cell {0}", uv);
Log.Write("debug", $"Failed to find closest edge for map cell {uv}");
return uv;
}
}
@@ -1348,13 +1369,18 @@ namespace OpenRA
throw new ArgumentOutOfRangeException(nameof(maxRange),
$"The requested range ({maxRange}) cannot exceed the value of MaximumTileSearchRange ({Grid.MaximumTileSearchRange})");
for (var i = minRange; i <= maxRange; i++)
return FindTilesInAnnulus();
IEnumerable<CPos> FindTilesInAnnulus()
{
foreach (var offset in Grid.TilesByDistance[i])
for (var i = minRange; i <= maxRange; i++)
{
var t = offset + center;
if (allowOutsideBounds ? Tiles.Contains(t) : Contains(t))
yield return t;
foreach (var offset in Grid.TilesByDistance[i])
{
var t = offset + center;
if (allowOutsideBounds ? Tiles.Contains(t) : Contains(t))
yield return t;
}
}
}
}
@@ -1410,14 +1436,6 @@ namespace OpenRA
return false;
}
public string Translate(string key, IDictionary<string, object> args = null)
{
if (Translation.TryGetString(key, out var message, args))
return message;
return modData.Translation.GetString(key, args);
}
public void Dispose()
{
Sequences.Dispose();

View File

@@ -38,7 +38,7 @@ namespace OpenRA
readonly object syncRoot = new();
readonly Queue<MapPreview> generateMinimap = new();
public Dictionary<string, string> StringPool { get; } = new Dictionary<string, string>();
public HashSet<string> StringPool { get; } = new();
readonly List<MapDirectoryTracker> mapDirectoryTrackers = new();
@@ -97,7 +97,7 @@ namespace OpenRA
? MapClassification.Unknown : Enum<MapClassification>.Parse(kv.Value);
IReadOnlyPackage package;
var optional = name.StartsWith("~", StringComparison.Ordinal);
var optional = name.StartsWith('~');
if (optional)
name = name[1..];
@@ -106,7 +106,7 @@ namespace OpenRA
// HACK: If the path is inside the support directory then we may need to create it
// Assume that the path is a directory if there is not an existing file with the same name
var resolved = Platform.ResolvePath(name);
if (resolved.StartsWith(Platform.SupportDir) && !File.Exists(resolved))
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && !File.Exists(resolved))
Directory.CreateDirectory(resolved);
package = modData.ModFiles.OpenPackage(name);
@@ -123,10 +123,15 @@ namespace OpenRA
mapDirectoryTrackers.Add(new MapDirectoryTracker(mapGrid, package, classification));
}
// PERF: Load the mod YAML once outside the loop, and reuse it when resolving each maps custom YAML.
var modDataRules = modData.GetRulesYaml();
foreach (var kv in MapLocations)
{
foreach (var map in kv.Key.Contents)
LoadMap(map, kv.Key, kv.Value, mapGrid, null);
{
LoadMapInternal(map, kv.Key, kv.Value, mapGrid, null, modDataRules);
GC.Collect();
}
}
// We only want to track maps in runtime, not at loadtime
@@ -134,6 +139,11 @@ namespace OpenRA
}
public void LoadMap(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap)
{
LoadMapInternal(map, package, classification, mapGrid, oldMap, null);
}
void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap, IEnumerable<List<MiniYamlNode>> modDataRules)
{
IReadOnlyPackage mapPackage = null;
try
@@ -141,10 +151,15 @@ namespace OpenRA
using (new PerfTimer(map))
{
mapPackage = package.OpenPackage(map, modData.ModFiles);
Log.Write("debug", $"Loading Map: {map} FROM {mapPackage.Name}");
Console.WriteLine($"Loading Map: {map} FROM {mapPackage.Name}");
if (mapPackage != null)
{
var uid = Map.ComputeUID(mapPackage);
previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type);
previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type, modDataRules);
// Freeing the package to save memory if there is a lot of Maps
previews[uid].PackageDispose();
if (oldMap != uid)
{
@@ -158,10 +173,16 @@ namespace OpenRA
catch (Exception e)
{
mapPackage?.Dispose();
Console.WriteLine("Failed to load map: {0}", map);
Console.WriteLine("Details: {0}", e);
Log.Write("debug", "Failed to load map: {0}", map);
Log.Write("debug", "Details: {0}", e);
Console.WriteLine($"Failed to load map: {map}");
Console.WriteLine("Details:");
Console.WriteLine(e);
Console.WriteLine("StackTrace:");
Console.WriteLine(System.Environment.StackTrace);
Log.Write("debug", $"Failed to load map: {map}");
Log.Write("debug", "Details:");
Log.Write("debug", e);
Log.Write("debug", "StackTrace:");
Log.Write("debug", System.Environment.StackTrace);
}
}
@@ -181,13 +202,13 @@ namespace OpenRA
continue;
var name = kv.Key;
var optional = name.StartsWith("~", StringComparison.Ordinal);
var optional = name.StartsWith('~');
if (optional)
name = name[1..];
// Don't try to open the map directory in the support directory if it doesn't exist
var resolved = Platform.ResolvePath(name);
if (resolved.StartsWith(Platform.SupportDir) && (!Directory.Exists(resolved) || !File.Exists(resolved)))
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && (!Directory.Exists(resolved) || !File.Exists(resolved)))
continue;
using (var package = (IReadWritePackage)modData.ModFiles.OpenPackage(name))
@@ -224,11 +245,12 @@ namespace OpenRA
.ToList();
foreach (var uid in queryUids)
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null);
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null, null);
Task.Run(async () =>
{
var client = HttpClientFactory.Create();
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
// Limit each query to 50 maps at a time to avoid request size limits
for (var i = 0; i < queryUids.Count; i += 50)
@@ -240,26 +262,27 @@ namespace OpenRA
var httpResponseMessage = await client.GetAsync(url);
var result = await httpResponseMessage.Content.ReadAsStreamAsync();
var yaml = MiniYaml.FromStream(result);
var yaml = MiniYaml.FromStream(result, url, stringPool: stringPool);
foreach (var kv in yaml)
previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, mapDetailsReceived);
previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, modData.Manifest.MapCompatibility, mapDetailsReceived);
foreach (var uid in batchUids)
{
var p = previews[uid];
if (p.Status != MapStatus.DownloadAvailable)
p.UpdateRemoteSearch(MapStatus.Unavailable, null);
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null);
}
}
catch (Exception e)
{
Log.Write("debug", "Remote map query failed with error: {0}", e);
Log.Write("debug", "URL was: {0}", url);
Log.Write("debug", "Remote map query failed with error:");
Log.Write("debug", e);
Log.Write("debug", $"URL was: {url}");
foreach (var uid in batchUids)
{
var p = previews[uid];
p.UpdateRemoteSearch(MapStatus.Unavailable, null);
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null);
mapQueryFailed?.Invoke(p);
}
}
@@ -272,11 +295,11 @@ namespace OpenRA
Log.Write("debug", "MapCache.LoadAsyncInternal started");
// Milliseconds to wait on one loop when nothing to do
var emptyDelay = 50;
const int EmptyDelay = 50;
// Keep the thread alive for at least 5 seconds after the last minimap generation
var maxKeepAlive = 5000 / emptyDelay;
var keepAlive = maxKeepAlive;
const int MaxKeepAlive = 5000 / EmptyDelay;
var keepAlive = MaxKeepAlive;
while (true)
{
@@ -296,11 +319,11 @@ namespace OpenRA
if (todo.Count == 0)
{
Thread.Sleep(emptyDelay);
Thread.Sleep(EmptyDelay);
continue;
}
else
keepAlive = maxKeepAlive;
keepAlive = MaxKeepAlive;
// Render the minimap into the shared sheet
foreach (var p in todo)
@@ -315,7 +338,8 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to load minimap with exception: {0}", e);
Log.Write("debug", "Failed to load minimap with exception:");
Log.Write("debug", e);
}
});
}
@@ -337,8 +361,8 @@ namespace OpenRA
while (this[uid].Status != MapStatus.Available)
{
if (mapUpdates.ContainsKey(uid))
uid = mapUpdates[uid];
if (mapUpdates.TryGetValue(uid, out var newUid))
uid = newUid;
else
return null;
}
@@ -361,7 +385,6 @@ namespace OpenRA
{
// Wait for any existing thread to exit before starting a new one.
previewLoaderThread?.Join();
previewLoaderThread = new Thread(LoadAsyncInternal)
{
Name = "Map Preview Loader",

View File

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

View File

@@ -116,22 +116,26 @@ namespace OpenRA
public readonly WVec[] SubCellOffsets =
{
new WVec(0, 0, 0), // full cell - index 0
new WVec(-299, -256, 0), // top left - index 1
new WVec(256, -256, 0), // top right - index 2
new WVec(0, 0, 0), // center - index 3
new WVec(-299, 256, 0), // bottom left - index 4
new WVec(256, 256, 0), // bottom right - index 5
new(0, 0, 0), // full cell - index 0
new(-299, -256, 0), // top left - index 1
new(256, -256, 0), // top right - index 2
new(0, 0, 0), // center - index 3
new(-299, 256, 0), // bottom left - index 4
new(256, 256, 0), // bottom right - index 5
};
public CellRamp[] Ramps { get; }
internal readonly CVec[][] TilesByDistance;
public int TileScale { get; }
public MapGrid(MiniYaml yaml)
{
FieldLoader.Load(this, yaml);
TileScale = Type == MapGridType.RectangularIsometric ? 1448 : 1024;
// The default subcell index defaults to the middle entry
var defaultSubCellIndex = (byte)DefaultSubCell;
if (defaultSubCellIndex == byte.MaxValue)

View File

@@ -59,12 +59,13 @@ namespace OpenRA
public readonly string rules;
public readonly string players_block;
public readonly int mapformat;
public readonly string game_mod;
}
public class MapPreview : IDisposable, IReadOnlyFileSystem
public sealed class MapPreview : IDisposable, IReadOnlyFileSystem
{
/// <summary>Wrapper that enables map data to be replaced in an atomic fashion.</summary>
class InnerData
sealed class InnerData
{
public int MapFormat;
public string Title;
@@ -90,6 +91,7 @@ namespace OpenRA
public MiniYaml SequenceDefinitions;
public MiniYaml ModelSequenceDefinitions;
public Translation Translation { get; private set; }
public ActorInfo WorldActorInfo { get; private set; }
public ActorInfo PlayerActorInfo { get; private set; }
@@ -110,7 +112,7 @@ namespace OpenRA
return key == "world" || key == "player";
}
public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary<string, MiniYaml> yaml)
public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary<string, MiniYaml> yaml, IEnumerable<List<MiniYamlNode>> modDataRules)
{
RuleDefinitions = LoadRuleSection(yaml, "Rules");
WeaponDefinitions = LoadRuleSection(yaml, "Weapons");
@@ -120,21 +122,29 @@ namespace OpenRA
SequenceDefinitions = LoadRuleSection(yaml, "Sequences");
ModelSequenceDefinitions = LoadRuleSection(yaml, "ModelSequences");
Translation = yaml.TryGetValue("Translations", out var node) && node != null
? new Translation(Game.Settings.Player.Language, FieldLoader.GetValue<string[]>("value", node.Value), fileSystem)
: null;
try
{
// PERF: Implement a minimal custom loader for custom world and player actors to minimize loading time
// This assumes/enforces that these actor types can only inherit abstract definitions (starting with ^)
if (RuleDefinitions != null)
{
var files = modData.Manifest.Rules.AsEnumerable();
modDataRules ??= modData.GetRulesYaml();
var files = Enumerable.Empty<string>();
if (RuleDefinitions.Value != null)
{
var mapFiles = FieldLoader.GetValue<string[]>("value", RuleDefinitions.Value);
files = files.Append(mapFiles);
}
var sources = files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s).Where(IsLoadableRuleDefinition).ToList());
if (RuleDefinitions.Nodes.Count > 0)
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var sources =
modDataRules.Select(x => x.Where(IsLoadableRuleDefinition).ToList())
.Concat(files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool).Where(IsLoadableRuleDefinition).ToList()));
if (RuleDefinitions.Nodes.Length > 0)
sources = sources.Append(RuleDefinitions.Nodes.Where(IsLoadableRuleDefinition).ToList());
var yamlNodes = MiniYaml.Merge(sources);
@@ -145,7 +155,8 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed to load rules for `{0}` with error: {1}", Title, e.Message);
Log.Write("debug", $"Failed to load rules for `{Title}` with error:");
Log.Write("debug", e);
}
WorldActorInfo = modData.DefaultRules.Actors[SystemActors.World];
@@ -163,7 +174,23 @@ namespace OpenRA
readonly ModData modData;
public readonly string Uid;
public IReadOnlyPackage Package { get; private set; }
public string PackageName { get; private set; }
IReadOnlyPackage pPackage;
public IReadOnlyPackage Package
{
get
{
if (pPackage == null)
{
pPackage = parentPackage.OpenPackage(PackageName, modData.ModFiles);
}
return pPackage;
}
private set => pPackage = value;
}
IReadOnlyPackage parentPackage;
volatile InnerData innerData;
@@ -194,6 +221,19 @@ namespace OpenRA
public long DownloadBytes { get; private set; }
public int DownloadPercentage { get; private set; }
/// <summary>
/// Functionality mirrors <see cref="TranslationProvider.GetString"/>, except instead of using
/// loaded <see cref="Map"/>'s translations as backup, we use this <see cref="MapPreview"/>'s.
/// </summary>
public string GetLocalisedString(string key, IDictionary<string, object> args = null)
{
// PERF: instead of loading mod level Translation per each MapPreview, reuse the already loaded one in TranslationProvider.
if (TranslationProvider.TryGetModString(key, out var message, args))
return message;
return innerData.Translation?.GetString(key, args) ?? key;
}
Sprite minimap;
bool generatingMinimap;
public Sprite GetMinimap()
@@ -234,6 +274,7 @@ namespace OpenRA
{
this.cache = cache;
this.modData = modData;
this.pPackage = null;
Uid = uid;
innerData = new InnerData
@@ -260,9 +301,10 @@ namespace OpenRA
{
this.modData = modData;
cache = modData.MapCache;
this.pPackage = null;
Uid = map.Uid;
Package = map.Package;
PackageName = map.Package.Name;
var mapPlayers = new MapPlayers(map.PlayerDefinitions);
var spawns = new List<CPos>();
@@ -293,16 +335,17 @@ namespace OpenRA
innerData.SetCustomRules(modData, this, new Dictionary<string, MiniYaml>()
{
{ "Rules", map.RuleDefinitions },
{ "Translations", map.TranslationDefinitions },
{ "Weapons", map.WeaponDefinitions },
{ "Voices", map.VoiceDefinitions },
{ "Music", map.MusicDefinitions },
{ "Notifications", map.NotificationDefinitions },
{ "Sequences", map.SequenceDefinitions },
{ "ModelSequences", map.ModelSequenceDefinitions }
});
}, null);
}
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType)
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType, IEnumerable<List<MiniYamlNode>> modDataRules)
{
Dictionary<string, MiniYaml> yaml;
using (var yamlStream = p.GetStream("map.yaml"))
@@ -310,10 +353,10 @@ namespace OpenRA
if (yamlStream == null)
throw new FileNotFoundException("Required file map.yaml not present in this map");
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, "map.yaml", stringPool: cache.StringPool)).ToDictionary();
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, $"{p.Name}:map.yaml", stringPool: cache.StringPool)).ToDictionary();
}
Package = p;
PackageName = p.Name;
parentPackage = parent;
var newData = innerData.Clone();
@@ -392,7 +435,7 @@ namespace OpenRA
newData.Status = MapStatus.Unavailable;
}
newData.SetCustomRules(modData, this, yaml);
newData.SetCustomRules(modData, this, yaml, modDataRules);
if (cache.LoadPreviewImages && p.Contains("map.png"))
using (var dataStream = p.GetStream("map.png"))
@@ -404,7 +447,7 @@ namespace OpenRA
innerData = newData;
}
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action<MapPreview> parseMetadata = null)
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, string[] mapCompatibility, Action<MapPreview> parseMetadata = null)
{
var newData = innerData.Clone();
newData.Status = status;
@@ -444,21 +487,31 @@ namespace OpenRA
}
catch (Exception e)
{
Log.Write("debug", "Failed parsing mapserver minimap response: {0}", e);
Log.Write("debug", "Failed parsing mapserver minimap response:");
Log.Write("debug", e);
newData.Preview = null;
}
}
var playersString = Encoding.UTF8.GetString(Convert.FromBase64String(r.players_block));
newData.Players = new MapPlayers(MiniYaml.FromString(playersString));
newData.Players = new MapPlayers(MiniYaml.FromString(playersString,
$"{yaml.NodeWithKey(nameof(r.players_block)).Location.Name}:{nameof(r.players_block)}"));
var rulesString = Encoding.UTF8.GetString(Convert.FromBase64String(r.rules));
var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString)).ToDictionary();
newData.SetCustomRules(modData, this, rulesYaml);
var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString,
$"{yaml.NodeWithKey(nameof(r.rules)).Location.Name}:{nameof(r.rules)}")).ToDictionary();
newData.SetCustomRules(modData, this, rulesYaml, null);
// Map is for a different mod: update its information so it can be displayed
// in the cross-mod server browser UI, but mark it as unavailable so it can't
// be selected in a server for the current mod.
if (!mapCompatibility.Contains(r.game_mod))
newData.Status = MapStatus.Unavailable;
}
catch (Exception e)
{
Log.Write("debug", "Failed parsing mapserver response: {0}", e);
Log.Write("debug", "Failed parsing mapserver response:");
Log.Write("debug", e);
}
// Commit updated data before running the callbacks
@@ -525,20 +578,21 @@ namespace OpenRA
await response.ReadAsStreamWithProgress(fileStream, OnDownloadProgress, CancellationToken.None);
mapInstallPackage.Update(mapFilename, fileStream.ToArray());
Log.Write("debug", "Downloaded map to '{0}'", mapFilename);
Log.Write("debug", $"Downloaded map to '{mapFilename}'");
var package = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles);
if (package == null)
innerData.Status = MapStatus.DownloadError;
else
{
UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType);
UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType, null);
Game.RunAfterTick(onSuccess);
}
}
catch (Exception e)
{
Log.Write("debug", "Map installation failed with error: {0}", e);
Log.Write("debug", "Map installation failed with error:");
Log.Write("debug", e);
innerData.Status = MapStatus.DownloadError;
}
});
@@ -551,10 +605,15 @@ namespace OpenRA
public void Dispose()
{
if (Package != null)
PackageDispose();
}
public void PackageDispose()
{
if (pPackage != null)
{
Package.Dispose();
Package = null;
pPackage.Dispose();
pPackage = null;
}
}

View File

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

View File

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

View File

@@ -69,7 +69,7 @@ namespace OpenRA
// HACK: Temporary placeholder to avoid having to change all the traits that reference this constant.
// This can be removed after the palette references have been moved from traits to sequences.
public class TileSet
public static class TileSet
{
public const string TerrainPaletteInternalName = "terrain";
}

View File

@@ -11,6 +11,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using OpenRA.FileSystem;
@@ -19,18 +20,36 @@ namespace OpenRA
{
public static class MiniYamlExts
{
public static void WriteToFile(this List<MiniYamlNode> y, string filename)
public static void WriteToFile(this IEnumerable<MiniYamlNode> y, string filename)
{
File.WriteAllLines(filename, y.ToLines().Select(x => x.TrimEnd()).ToArray());
}
public static string WriteToString(this List<MiniYamlNode> y)
public static string WriteToString(this IEnumerable<MiniYamlNode> y)
{
// Remove all trailing newlines and restore the final EOF newline
return y.ToLines().JoinWith("\n").TrimEnd('\n') + "\n";
}
public static IEnumerable<string> ToLines(this List<MiniYamlNode> y)
public static IEnumerable<string> ToLines(this IEnumerable<MiniYamlNode> y)
{
foreach (var kv in y)
foreach (var line in kv.Value.ToLines(kv.Key, kv.Comment))
yield return line;
}
public static void WriteToFile(this IEnumerable<MiniYamlNodeBuilder> y, string filename)
{
File.WriteAllLines(filename, y.ToLines().Select(x => x.TrimEnd()).ToArray());
}
public static string WriteToString(this IEnumerable<MiniYamlNodeBuilder> y)
{
// Remove all trailing newlines and restore the final EOF newline
return y.ToLines().JoinWith("\n").TrimEnd('\n') + "\n";
}
public static IEnumerable<string> ToLines(this IEnumerable<MiniYamlNodeBuilder> y)
{
foreach (var kv in y)
foreach (var line in kv.Value.ToLines(kv.Key, kv.Comment))
@@ -38,18 +57,33 @@ namespace OpenRA
}
}
public class MiniYamlNode
public sealed class MiniYamlNode
{
public struct SourceLocation
public readonly struct SourceLocation
{
public string Filename; public int Line;
public override string ToString() { return $"{Filename}:{Line}"; }
public readonly string Name;
public readonly int Line;
public SourceLocation(string name, int line)
{
Name = name;
Line = line;
}
public override string ToString() { return $"{Name}:{Line}"; }
}
public SourceLocation Location;
public string Key;
public MiniYaml Value;
public string Comment;
public readonly SourceLocation Location;
public readonly string Key;
public readonly MiniYaml Value;
public readonly string Comment;
public MiniYamlNode WithValue(MiniYaml value)
{
if (Value == value)
return this;
return new MiniYamlNode(Key, value, Comment, Location);
}
public MiniYamlNode(string k, MiniYaml v, string c = null)
{
@@ -65,42 +99,74 @@ namespace OpenRA
}
public MiniYamlNode(string k, string v, string c = null)
: this(k, v, c, null) { }
: this(k, new MiniYaml(v, Enumerable.Empty<MiniYamlNode>()), c) { }
public MiniYamlNode(string k, string v, List<MiniYamlNode> n)
public MiniYamlNode(string k, string v, IEnumerable<MiniYamlNode> n)
: this(k, new MiniYaml(v, n), null) { }
public MiniYamlNode(string k, string v, string c, List<MiniYamlNode> n)
: this(k, new MiniYaml(v, n), c) { }
public MiniYamlNode(string k, string v, string c, List<MiniYamlNode> n, SourceLocation loc)
: this(k, new MiniYaml(v, n), c, loc) { }
public override string ToString()
{
return $"{{YamlNode: {Key} @ {Location}}}";
}
public MiniYamlNode Clone()
{
return new MiniYamlNode(Key, Value.Clone(), Comment, Location);
}
}
public class MiniYaml
public sealed class MiniYaml
{
const int SpacesPerLevel = 4;
static readonly Func<string, string> StringIdentity = s => s;
static readonly Func<MiniYaml, MiniYaml> MiniYamlIdentity = my => my;
public string Value;
public List<MiniYamlNode> Nodes;
public MiniYaml Clone()
public readonly string Value;
public readonly ImmutableArray<MiniYamlNode> Nodes;
public MiniYaml WithValue(string value)
{
var clonedNodes = new List<MiniYamlNode>(Nodes.Count);
if (Value == value)
return this;
return new MiniYaml(value, Nodes);
}
public MiniYaml WithNodes(IEnumerable<MiniYamlNode> nodes)
{
if (nodes is ImmutableArray<MiniYamlNode> n && Nodes == n)
return this;
return new MiniYaml(Value, nodes);
}
public MiniYaml WithNodesAppended(IEnumerable<MiniYamlNode> nodes)
{
var newNodes = Nodes.AddRange(nodes);
if (Nodes == newNodes)
return this;
return new MiniYaml(Value, newNodes);
}
public MiniYamlNode NodeWithKey(string key)
{
var result = NodeWithKeyOrDefault(key);
if (result == null)
throw new InvalidDataException($"No node with key '{key}'");
return result;
}
public MiniYamlNode NodeWithKeyOrDefault(string key)
{
// PERF: Avoid LINQ.
var first = true;
MiniYamlNode result = null;
foreach (var node in Nodes)
clonedNodes.Add(node.Clone());
return new MiniYaml(Value, clonedNodes);
{
if (node.Key != key)
continue;
if (!first)
throw new InvalidDataException($"Duplicate key '{node.Key}' in {node.Location}");
first = false;
result = node;
}
return result;
}
public Dictionary<string, MiniYaml> ToDictionary()
@@ -116,47 +182,40 @@ namespace OpenRA
public Dictionary<TKey, TElement> ToDictionary<TKey, TElement>(
Func<string, TKey> keySelector, Func<MiniYaml, TElement> elementSelector)
{
var ret = new Dictionary<TKey, TElement>(Nodes.Count);
var ret = new Dictionary<TKey, TElement>(Nodes.Length);
foreach (var y in Nodes)
{
var key = keySelector(y.Key);
var element = elementSelector(y.Value);
try
{
ret.Add(key, element);
}
catch (ArgumentException ex)
{
throw new InvalidDataException($"Duplicate key '{y.Key}' in {y.Location}", ex);
}
if (!ret.TryAdd(key, element))
throw new InvalidDataException($"Duplicate key '{y.Key}' in {y.Location}");
}
return ret;
}
public MiniYaml(string value)
: this(value, null) { }
: this(value, Enumerable.Empty<MiniYamlNode>()) { }
public MiniYaml(string value, List<MiniYamlNode> nodes)
public MiniYaml(string value, IEnumerable<MiniYamlNode> nodes)
{
Value = value;
Nodes = nodes ?? new List<MiniYamlNode>();
Nodes = ImmutableArray.CreateRange(nodes);
}
public static List<MiniYamlNode> NodesOrEmpty(MiniYaml y, string s)
static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string name, bool discardCommentsAndWhitespace, HashSet<string> stringPool)
{
var nd = y.ToDictionary();
return nd.ContainsKey(s) ? nd[s].Nodes : new List<MiniYamlNode>();
}
// YAML config often contains repeated strings for key, values, comments.
// Pool these strings so we only need one copy of each unique string.
// This saves on long-term memory usage as parsed values can often live a long time.
// A caller can also provide a pool as input, allowing de-duplication across multiple parses.
stringPool ??= new HashSet<string>();
static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string filename, bool discardCommentsAndWhitespace, Dictionary<string, string> stringPool)
{
stringPool ??= new Dictionary<string, string>();
var levels = new List<List<MiniYamlNode>>
var result = new List<List<MiniYamlNode>>
{
new List<MiniYamlNode>()
new()
};
var parsedLines = new List<(int Level, string Key, string Value, string Comment, MiniYamlNode.SourceLocation Location)>();
var lineNo = 0;
foreach (var ll in lines)
@@ -172,7 +231,7 @@ namespace OpenRA
ReadOnlySpan<char> key = default;
ReadOnlySpan<char> value = default;
ReadOnlySpan<char> comment = default;
var location = new MiniYamlNode.SourceLocation { Filename = filename, Line = lineNo };
var location = new MiniYamlNode.SourceLocation(name, lineNo);
if (line.Length > 0)
{
@@ -203,15 +262,6 @@ namespace OpenRA
}
}
if (levels.Count <= level)
throw new YamlException($"Bad indent in miniyaml at {location}");
while (levels.Count > level + 1)
{
levels[^1].TrimExcess();
levels.RemoveAt(levels.Count - 1);
}
// Extract key, value, comment from line as `<key>: <value>#<comment>`
// The # character is allowed in the value if escaped (\#).
// Leading and trailing whitespace is always trimmed from keys.
@@ -233,7 +283,7 @@ namespace OpenRA
if (commentStart < 0 && line[i] == '#' && (i == 0 || line[i - 1] != '\\'))
{
commentStart = i + 1;
if (commentStart <= keyLength)
if (i <= keyStart + keyLength)
keyLength = i - keyStart;
else
valueLength = i - valueStart;
@@ -271,6 +321,12 @@ namespace OpenRA
if (!key.IsEmpty || !discardCommentsAndWhitespace)
{
if (parsedLines.Count > 0 && parsedLines[^1].Level < level - 1)
throw new YamlException($"Bad indent in miniyaml at {location}");
while (parsedLines.Count > 0 && parsedLines[^1].Level > level)
BuildCompletedSubNode(level);
var keyString = key.IsEmpty ? null : key.ToString();
var valueString = value.IsEmpty ? null : value.ToString();
@@ -278,44 +334,75 @@ namespace OpenRA
// (i.e. a lone # at the end of a line) can be correctly re-serialized
var commentString = comment == default ? null : comment.ToString();
keyString = keyString == null ? null : stringPool.GetOrAdd(keyString, keyString);
valueString = valueString == null ? null : stringPool.GetOrAdd(valueString, valueString);
commentString = commentString == null ? null : stringPool.GetOrAdd(commentString, commentString);
keyString = keyString == null ? null : stringPool.GetOrAdd(keyString);
valueString = valueString == null ? null : stringPool.GetOrAdd(valueString);
commentString = commentString == null ? null : stringPool.GetOrAdd(commentString);
var nodes = new List<MiniYamlNode>();
levels[level].Add(new MiniYamlNode(keyString, valueString, commentString, nodes, location));
levels.Add(nodes);
parsedLines.Add((level, keyString, valueString, commentString, location));
}
}
foreach (var nodes in levels)
nodes.TrimExcess();
if (parsedLines.Count > 0)
BuildCompletedSubNode(0);
return levels[0];
return result[0];
void BuildCompletedSubNode(int level)
{
var lastLevel = parsedLines[^1].Level;
while (lastLevel >= result.Count)
result.Add(new List<MiniYamlNode>());
while (parsedLines.Count > 0 && parsedLines[^1].Level >= level)
{
var parent = parsedLines[^1];
var startOfRange = parsedLines.Count - 1;
while (startOfRange > 0 && parsedLines[startOfRange - 1].Level == parent.Level)
startOfRange--;
for (var i = startOfRange; i < parsedLines.Count - 1; i++)
{
var sibling = parsedLines[i];
result[parent.Level].Add(
new MiniYamlNode(sibling.Key, new MiniYaml(sibling.Value), sibling.Comment, sibling.Location));
}
var childNodes = parent.Level + 1 < result.Count ? result[parent.Level + 1] : null;
result[parent.Level].Add(new MiniYamlNode(
parent.Key,
new MiniYaml(parent.Value, childNodes ?? Enumerable.Empty<MiniYamlNode>()),
parent.Comment,
parent.Location));
childNodes?.Clear();
parsedLines.RemoveRange(startOfRange, parsedLines.Count - startOfRange);
}
}
}
public static List<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
public static List<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
{
return FromStream(File.OpenRead(path), path, discardCommentsAndWhitespace, stringPool);
}
public static List<MiniYamlNode> FromStream(Stream s, string fileName = "<no filename available>", bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
public static List<MiniYamlNode> FromStream(Stream s, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
{
return FromLines(s.ReadAllLinesAsMemory(), fileName, discardCommentsAndWhitespace, stringPool);
return FromLines(s.ReadAllLinesAsMemory(), name, discardCommentsAndWhitespace, stringPool);
}
public static List<MiniYamlNode> FromString(string text, string fileName = "<no filename available>", bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
public static List<MiniYamlNode> FromString(string text, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
{
return FromLines(text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(s => s.AsMemory()), fileName, 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<List<MiniYamlNode>> sources)
public static List<MiniYamlNode> Merge(IEnumerable<IReadOnlyCollection<MiniYamlNode>> sources)
{
if (!sources.Any())
var sourcesList = sources.ToList();
if (sourcesList.Count == 0)
return new List<MiniYamlNode>();
var tree = sources.Where(s => s != null)
var tree = sourcesList
.Where(s => s != null)
.Select(MergeSelfPartial)
.Aggregate(MergePartial)
.Where(n => n.Key != null)
@@ -324,39 +411,39 @@ namespace OpenRA
var resolved = new Dictionary<string, MiniYaml>(tree.Count);
foreach (var kv in tree)
{
var inherited = new Dictionary<string, MiniYamlNode.SourceLocation>
{
{ kv.Key, default }
};
// Inheritance is tracked from parent->child, but not from child->parentsiblings.
var inherited = ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty.Add(kv.Key, default);
var children = ResolveInherits(kv.Value, tree, inherited);
resolved.Add(kv.Key, new MiniYaml(kv.Value.Value, children));
}
// Resolve any top-level removals (e.g. removing whole actor blocks)
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)).ToList());
return ResolveInherits(nodes, tree, new Dictionary<string, MiniYamlNode.SourceLocation>());
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)));
return ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty);
}
static void MergeIntoResolved(MiniYamlNode overrideNode, List<MiniYamlNode> existingNodes,
Dictionary<string, MiniYaml> tree, Dictionary<string, MiniYamlNode.SourceLocation> inherited)
static void MergeIntoResolved(MiniYamlNode overrideNode, List<MiniYamlNode> existingNodes, HashSet<string> existingNodeKeys,
Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
{
var existingNode = existingNodes.Find(n => n.Key == overrideNode.Key);
if (existingNode != null)
if (existingNodeKeys.Add(overrideNode.Key))
{
existingNode.Value = MergePartial(existingNode.Value, overrideNode.Value);
existingNode.Value.Nodes = ResolveInherits(existingNode.Value, tree, inherited);
existingNodes.Add(overrideNode);
return;
}
else
existingNodes.Add(overrideNode.Clone());
var existingNodeIndex = IndexOfKey(existingNodes, overrideNode.Key);
var existingNode = existingNodes[existingNodeIndex];
var value = MergePartial(existingNode.Value, overrideNode.Value);
var nodes = ResolveInherits(value, tree, inherited);
if (!value.Nodes.SequenceEqual(nodes))
value = value.WithNodes(nodes);
existingNodes[existingNodeIndex] = existingNode.WithValue(value);
}
static List<MiniYamlNode> ResolveInherits(MiniYaml node, Dictionary<string, MiniYaml> tree, Dictionary<string, MiniYamlNode.SourceLocation> inherited)
static List<MiniYamlNode> ResolveInherits(MiniYaml node, Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
{
var resolved = new List<MiniYamlNode>(node.Nodes.Count);
// Inheritance is tracked from parent->child, but not from child->parentsiblings.
inherited = new Dictionary<string, MiniYamlNode.SourceLocation>(inherited);
var resolved = new List<MiniYamlNode>(node.Nodes.Length);
var resolvedKeys = new HashSet<string>(node.Nodes.Length);
foreach (var n in node.Nodes)
{
@@ -366,24 +453,29 @@ namespace OpenRA
throw new YamlException(
$"{n.Location}: Parent type `{n.Value.Value}` not found");
if (inherited.ContainsKey(n.Value.Value))
try
{
inherited = inherited.Add(n.Value.Value, n.Location);
}
catch (ArgumentException)
{
throw new YamlException($"{n.Location}: Parent type `{n.Value.Value}` was already inherited by this yaml tree at {inherited[n.Value.Value]} (note: may be from a derived tree)");
}
inherited.Add(n.Value.Value, n.Location);
foreach (var r in ResolveInherits(parent, tree, inherited))
MergeIntoResolved(r, resolved, tree, inherited);
MergeIntoResolved(r, resolved, resolvedKeys, tree, inherited);
}
else if (n.Key.StartsWith("-", StringComparison.Ordinal))
else if (n.Key.StartsWith('-'))
{
var removed = n.Key[1..];
if (resolved.RemoveAll(r => r.Key == removed) == 0)
throw new YamlException($"{n.Location}: There are no elements with key `{removed}` to remove");
resolvedKeys.Remove(removed);
}
else
MergeIntoResolved(n, resolved, tree, inherited);
MergeIntoResolved(n, resolved, resolvedKeys, tree, inherited);
}
resolved.TrimExcess();
return resolved;
}
@@ -391,7 +483,7 @@ namespace OpenRA
/// Merges any duplicate keys that are defined within the same set of nodes.
/// Does not resolve inheritance or node removals.
/// </summary>
static List<MiniYamlNode> MergeSelfPartial(List<MiniYamlNode> existingNodes)
static IReadOnlyCollection<MiniYamlNode> MergeSelfPartial(IReadOnlyCollection<MiniYamlNode> existingNodes)
{
var keys = new HashSet<string>(existingNodes.Count);
var ret = new List<MiniYamlNode>(existingNodes.Count);
@@ -402,17 +494,20 @@ namespace OpenRA
else
{
// Node with the same key has already been added: merge new node over the existing one
var original = ret.First(r => r.Key == n.Key);
original.Value = MergePartial(original.Value, n.Value);
var originalIndex = IndexOfKey(ret, n.Key);
var original = ret[originalIndex];
ret[originalIndex] = original.WithValue(MergePartial(original.Value, n.Value));
}
}
ret.TrimExcess();
return ret;
}
static MiniYaml MergePartial(MiniYaml existingNodes, MiniYaml overrideNodes)
{
existingNodes?.Nodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
overrideNodes?.Nodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
if (existingNodes == null)
return overrideNodes;
@@ -422,7 +517,7 @@ namespace OpenRA
return new MiniYaml(overrideNodes.Value ?? existingNodes.Value, MergePartial(existingNodes.Nodes, overrideNodes.Nodes));
}
static List<MiniYamlNode> MergePartial(List<MiniYamlNode> existingNodes, List<MiniYamlNode> overrideNodes)
static IReadOnlyCollection<MiniYamlNode> MergePartial(IReadOnlyCollection<MiniYamlNode> existingNodes, IReadOnlyCollection<MiniYamlNode> overrideNodes)
{
if (existingNodes.Count == 0)
return overrideNodes;
@@ -431,27 +526,67 @@ namespace OpenRA
return existingNodes;
var ret = new List<MiniYamlNode>(existingNodes.Count + overrideNodes.Count);
var plainKeys = new HashSet<string>(existingNodes.Count + overrideNodes.Count);
var existingDict = existingNodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
var overrideDict = overrideNodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
var allKeys = existingDict.Keys.Union(overrideDict.Keys);
foreach (var node in existingNodes)
MergeNode(node);
foreach (var node in overrideNodes)
MergeNode(node);
foreach (var key in allKeys)
void MergeNode(MiniYamlNode node)
{
existingDict.TryGetValue(key, out var existingNode);
overrideDict.TryGetValue(key, out var overrideNode);
// Append Removal nodes to the result.
// Therefore: we know the remainder of the method deals with a plain node.
if (node.Key.StartsWith('-'))
{
ret.Add(node);
return;
}
var loc = overrideNode?.Location ?? default;
var comment = (overrideNode ?? existingNode).Comment;
var merged = (existingNode == null || overrideNode == null) ? overrideNode ?? existingNode :
new MiniYamlNode(key, MergePartial(existingNode.Value, overrideNode.Value), comment, loc);
ret.Add(merged);
// If no previous node with this key is present, it is new and can just be appended.
if (plainKeys.Add(node.Key))
{
ret.Add(node);
return;
}
// A Removal node is closer than the previous node.
// We should not merge the new node, as the data being merged will jump before the Removal.
// Instead, append it so the previous node is applied, then removed, then the new node is applied.
var previousNodeIndex = LastIndexOfKey(ret, node.Key);
var previousRemovalNodeIndex = LastIndexOfKey(ret, $"-{node.Key}");
if (previousRemovalNodeIndex != -1 && previousRemovalNodeIndex > previousNodeIndex)
{
ret.Add(node);
return;
}
// A previous node is present with no intervening Removal.
// We should merge the new one into it, in place.
ret[previousNodeIndex] = node.WithValue(MergePartial(ret[previousNodeIndex].Value, node.Value));
}
ret.TrimExcess();
return ret;
}
static int IndexOfKey(List<MiniYamlNode> nodes, string key)
{
// PERF: Avoid LINQ.
for (var i = 0; i < nodes.Count; i++)
if (nodes[i].Key == key)
return i;
return -1;
}
static int LastIndexOfKey(List<MiniYamlNode> nodes, string key)
{
// PERF: Avoid LINQ.
for (var i = nodes.Count - 1; i >= 0; i--)
if (nodes[i].Key == key)
return i;
return -1;
}
public IEnumerable<string> ToLines(string key, string comment = null)
{
var hasKey = !string.IsNullOrEmpty(key);
@@ -474,14 +609,100 @@ namespace OpenRA
files = files.Append(mapFiles);
}
var yaml = files.Select(s => FromStream(fileSystem.Open(s), s));
if (mapRules != null && mapRules.Nodes.Count > 0)
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
IEnumerable<IReadOnlyCollection<MiniYamlNode>> yaml = files.Select(s => FromStream(fileSystem.Open(s), s, stringPool: stringPool));
if (mapRules != null && mapRules.Nodes.Length > 0)
yaml = yaml.Append(mapRules.Nodes);
return Merge(yaml);
}
}
public sealed class MiniYamlNodeBuilder
{
public MiniYamlNode.SourceLocation Location;
public string Key;
public MiniYamlBuilder Value;
public string Comment;
public MiniYamlNodeBuilder(MiniYamlNode node)
{
Location = node.Location;
Key = node.Key;
Value = new MiniYamlBuilder(node.Value);
Comment = node.Comment;
}
public MiniYamlNodeBuilder(string k, MiniYamlBuilder v, string c = null)
{
Key = k;
Value = v;
Comment = c;
}
public MiniYamlNodeBuilder(string k, MiniYamlBuilder v, string c, MiniYamlNode.SourceLocation loc)
: this(k, v, c)
{
Location = loc;
}
public MiniYamlNodeBuilder(string k, string v, string c = null)
: this(k, new MiniYamlBuilder(v, null), c) { }
public MiniYamlNodeBuilder(string k, string v, List<MiniYamlNode> n)
: this(k, new MiniYamlBuilder(v, n), null) { }
public MiniYamlNode Build()
{
return new MiniYamlNode(Key, Value.Build(), Comment, Location);
}
}
public sealed class MiniYamlBuilder
{
public string Value;
public List<MiniYamlNodeBuilder> Nodes;
public MiniYamlBuilder(MiniYaml yaml)
{
Value = yaml.Value;
Nodes = yaml.Nodes.Select(n => new MiniYamlNodeBuilder(n)).ToList();
}
public MiniYamlBuilder(string value)
: this(value, null) { }
public MiniYamlBuilder(string value, List<MiniYamlNode> nodes)
{
Value = value;
Nodes = nodes == null ? new List<MiniYamlNodeBuilder>() : nodes.ConvertAll(x => new MiniYamlNodeBuilder(x));
}
public MiniYaml Build()
{
return new MiniYaml(Value, Nodes.Select(n => n.Build()));
}
public IEnumerable<string> ToLines(string key, string comment = null)
{
var hasKey = !string.IsNullOrEmpty(key);
var hasValue = !string.IsNullOrEmpty(Value);
var hasComment = comment != null;
yield return (hasKey ? key + ":" : "")
+ (hasValue ? " " + Value.Replace("#", "\\#") : "")
+ (hasComment ? (hasKey || hasValue ? " " : "") + "#" + comment : "");
if (Nodes != null)
foreach (var line in Nodes.ToLines())
yield return "\t" + line;
}
public MiniYamlNodeBuilder NodeWithKeyOrDefault(string key)
{
return Nodes.SingleOrDefault(n => n.Key == key);
}
}
[Serializable]
public class YamlException : Exception
{

View File

@@ -33,10 +33,8 @@ namespace OpenRA
public readonly ISpriteLoader[] SpriteLoaders;
public readonly ITerrainLoader TerrainLoader;
public readonly ISpriteSequenceLoader SpriteSequenceLoader;
public readonly IModelSequenceLoader ModelSequenceLoader;
public readonly IVideoLoader[] VideoLoaders;
public readonly HotkeyManager Hotkeys;
public readonly Translation Translation;
public ILoadScreen LoadScreen { get; }
public CursorProvider CursorProvider { get; private set; }
public FS ModFiles;
@@ -91,19 +89,8 @@ namespace OpenRA
SpriteSequenceLoader = (ISpriteSequenceLoader)sequenceCtor.Invoke(new[] { this });
var modelFormat = Manifest.Get<ModelSequenceFormat>();
var modelLoader = ObjectCreator.FindType(modelFormat.Type + "Loader");
var modelCtor = modelLoader?.GetConstructor(new[] { typeof(ModData) });
if (modelLoader == null || !modelLoader.GetInterfaces().Contains(typeof(IModelSequenceLoader)) || modelCtor == null)
throw new InvalidOperationException($"Unable to find a model loader for type '{modelFormat.Type}'.");
ModelSequenceLoader = (IModelSequenceLoader)modelCtor.Invoke(new[] { this });
ModelSequenceLoader.OnMissingModelError = s => Log.Write("debug", s);
Hotkeys = new HotkeyManager(ModFiles, Game.Settings.Keys, Manifest);
Translation = new Translation(Game.Settings.Player.Language, Manifest.Translations, DefaultFileSystem);
defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this));
defaultTerrainInfo = Exts.Lazy(() =>
{
@@ -137,6 +124,7 @@ namespace OpenRA
// horribly when you use ModData in unexpected ways.
ChromeMetrics.Initialize(this);
ChromeProvider.Initialize(this);
TranslationProvider.Initialize(this, fileSystem);
Game.Sound.Initialize(SoundLoaders, fileSystem);
@@ -168,6 +156,12 @@ namespace OpenRA
return map;
}
public List<MiniYamlNode>[] GetRulesYaml()
{
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s, stringPool: stringPool)).ToArray();
}
public void Dispose()
{
LoadScreen?.Dispose();

View File

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

View File

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

View File

@@ -140,7 +140,7 @@ namespace OpenRA.Network
static object LoadClients(MiniYaml yaml)
{
var clients = new List<GameClient>();
var clientsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Clients");
var clientsNode = yaml.NodeWithKeyOrDefault("Clients");
if (clientsNode != null)
{
var regex = new Regex(@"Client@\d+");
@@ -159,7 +159,7 @@ namespace OpenRA.Network
// Games advertised using the old API used a single Mods field
if (Mod == null || Version == null)
{
var modsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Mods");
var modsNode = yaml.NodeWithKeyOrDefault("Mods");
if (modsNode != null)
{
var modVersion = modsNode.Value.Value.Split('@');
@@ -169,9 +169,8 @@ namespace OpenRA.Network
}
// Games advertised using the old API calculated the play time locally
if (State == 2 && PlayTime < 0)
if (DateTime.TryParse(Started, out var startTime))
PlayTime = (int)(DateTime.UtcNow - startTime).TotalSeconds;
if (State == 2 && PlayTime < 0 && DateTime.TryParse(Started, out var startTime))
PlayTime = (int)(DateTime.UtcNow - startTime).TotalSeconds;
var externalKey = ExternalMod.MakeKey(Mod, Version);
if (Game.ExternalMods.TryGetValue(externalKey, out var external) && external.Version == Version)
@@ -217,7 +216,7 @@ namespace OpenRA.Network
Name = server.Settings.Name;
// IP address will be replaced with a real value by the master server / receiving LAN client
Address = "0.0.0.0:" + server.Settings.ListenPort.ToString();
Address = "0.0.0.0:" + server.Settings.ListenPort.ToStringInvariant();
State = (int)server.State;
MaxPlayers = server.LobbyInfo.Slots.Count(s => !s.Value.Closed) - server.LobbyInfo.Clients.Count(c1 => c1.Bot != null);
Map = server.Map.Uid;
@@ -234,7 +233,7 @@ namespace OpenRA.Network
public string ToPOSTData(bool lanGame)
{
var root = new List<MiniYamlNode>() { new MiniYamlNode("Protocol", ProtocolVersion.ToString()) };
var root = new List<MiniYamlNode>() { new("Protocol", ProtocolVersion.ToStringInvariant()) };
foreach (var field in SerializeFields)
root.Add(FieldSaver.SaveField(this, field));
@@ -243,18 +242,16 @@ namespace OpenRA.Network
// Add fields that are normally generated by the master server
// LAN games overload the Id with a GUID string (rather than an ID) to allow deduplication
root.Add(new MiniYamlNode("Id", Platform.SessionGUID.ToString()));
root.Add(new MiniYamlNode("Players", Clients.Count(c => !c.IsBot && !c.IsSpectator).ToString()));
root.Add(new MiniYamlNode("Spectators", Clients.Count(c => c.IsSpectator).ToString()));
root.Add(new MiniYamlNode("Bots", Clients.Count(c => c.IsBot).ToString()));
root.Add(new MiniYamlNode("Players", Clients.Count(c => !c.IsBot && !c.IsSpectator).ToStringInvariant()));
root.Add(new MiniYamlNode("Spectators", Clients.Count(c => c.IsSpectator).ToStringInvariant()));
root.Add(new MiniYamlNode("Bots", Clients.Count(c => c.IsBot).ToStringInvariant()));
// Included for backwards compatibility with older clients that don't support separated Mod/Version.
root.Add(new MiniYamlNode("Mods", Mod + "@" + Version));
}
var clientsNode = new MiniYaml("");
var i = 0;
foreach (var c in Clients)
clientsNode.Nodes.Add(new MiniYamlNode("Client@" + i++.ToString(), FieldSaver.Save(c)));
var clientsNode = new MiniYaml("", Clients.Select((c, i) =>
new MiniYamlNode("Client@" + i, FieldSaver.Save(c))));
root.Add(new MiniYamlNode("Clients", clientsNode));
return new MiniYaml("", root)

View File

@@ -19,9 +19,9 @@ using ICSharpCode.SharpZipLib.Zip;
namespace OpenRA.Network
{
public class GeoIP
public static class GeoIP
{
class IP2LocationReader
sealed class IP2LocationReader
{
public readonly DateTime Date;
readonly Stream stream;
@@ -128,7 +128,8 @@ namespace OpenRA.Network
}
catch (Exception e)
{
Log.Write("geoip", "DatabaseReader failed: {0}", e);
Log.Write("geoip", "DatabaseReader failed:");
Log.Write("geoip", e);
}
}
@@ -142,7 +143,8 @@ namespace OpenRA.Network
}
catch (Exception e)
{
Log.Write("geoip", "LookupCountry failed: {0}", e);
Log.Write("geoip", "LookupCountry failed:");
Log.Write("geoip", e);
}
}

View File

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

View File

@@ -9,10 +9,8 @@
*/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Linguini.Shared.Types.Bundle;
namespace OpenRA.Network
@@ -57,61 +55,51 @@ namespace OpenRA.Network
public readonly string Key = string.Empty;
[FieldLoader.LoadUsing(nameof(LoadArguments))]
public readonly FluentArgument[] Arguments = Array.Empty<FluentArgument>();
public string TranslatedText { get; }
public readonly Dictionary<string, object> Arguments;
static object LoadArguments(MiniYaml yaml)
{
var arguments = new List<FluentArgument>();
var argumentsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Arguments");
var arguments = new Dictionary<string, object>();
var argumentsNode = yaml.NodeWithKeyOrDefault("Arguments");
if (argumentsNode != null)
{
var regex = new Regex(@"Argument@\d+");
foreach (var argument in argumentsNode.Value.Nodes)
if (regex.IsMatch(argument.Key))
arguments.Add(FieldLoader.Load<FluentArgument>(argument.Value));
foreach (var argumentNode in argumentsNode.Value.Nodes)
{
var argument = FieldLoader.Load<FluentArgument>(argumentNode.Value);
if (argument.Type == FluentArgument.FluentArgumentType.Number)
{
if (!double.TryParse(argument.Value, out var number))
Log.Write("debug", $"Failed to parse {argument.Value}");
arguments.Add(argument.Key, number);
}
else
arguments.Add(argument.Key, argument.Value);
}
}
return arguments.ToArray();
return arguments;
}
public LocalizedMessage(ModData modData, MiniYaml yaml)
public LocalizedMessage(MiniYaml yaml)
{
// Let the FieldLoader do the dirty work of loading the public fields.
FieldLoader.Load(this, yaml);
var argumentDictionary = new Dictionary<string, object>();
foreach (var argument in Arguments)
{
if (argument.Type == FluentArgument.FluentArgumentType.Number)
{
if (!double.TryParse(argument.Value, out var number))
Log.Write("debug", $"Failed to parse {argument.Value}");
argumentDictionary.Add(argument.Key, number);
}
else
argumentDictionary.Add(argument.Key, argument.Value);
}
TranslatedText = modData.Translation.GetString(Key, argumentDictionary);
}
public static string Serialize(string key, Dictionary<string, object> arguments = null)
{
var root = new List<MiniYamlNode>
{
new MiniYamlNode("Protocol", ProtocolVersion.ToString()),
new MiniYamlNode("Key", key)
new("Protocol", ProtocolVersion.ToStringInvariant()),
new("Key", key)
};
if (arguments != null)
{
var argumentsNode = new MiniYaml("");
var i = 0;
foreach (var argument in arguments.Select(a => new FluentArgument(a.Key, a.Value)))
argumentsNode.Nodes.Add(new MiniYamlNode("Argument@" + i++, FieldSaver.Save(argument)));
var argumentsNode = new MiniYaml("", arguments
.Select(a => new FluentArgument(a.Key, a.Value))
.Select((argument, i) => new MiniYamlNode("Argument@" + i, FieldSaver.Save(argument))));
root.Add(new MiniYamlNode("Arguments", argumentsNode));
}

View File

@@ -17,7 +17,7 @@ namespace OpenRA.Network
{
public enum NatStatus { Enabled, Disabled, NotSupported }
public class Nat
public static class Nat
{
public static NatStatus Status => NatUtility.IsSearching ? natDevice != null ? NatStatus.Enabled : NatStatus.NotSupported : NatStatus.Disabled;
@@ -49,8 +49,8 @@ namespace OpenRA.Network
// Only interact with one at a time. Some support both UPnP and NAT-PMP.
natDevice = args.Device;
Log.Write("nat", "Device found: {0}", natDevice.DeviceEndpoint);
Log.Write("nat", "Type: {0}", natDevice.NatProtocol);
Log.Write("nat", $"Device found: {natDevice.DeviceEndpoint}");
Log.Write("nat", $"Type: {natDevice.NatProtocol}");
}
finally
{

View File

@@ -156,7 +156,18 @@ namespace OpenRA
else
{
var pos = new WPos(r.ReadInt32(), r.ReadInt32(), r.ReadInt32());
target = Target.FromPos(pos);
var numberOfTerrainPositions = r.ReadInt16();
if (numberOfTerrainPositions == -1)
target = Target.FromPos(pos);
else
{
var terrainPositions = new WPos[numberOfTerrainPositions];
for (var i = 0; i < numberOfTerrainPositions; i++)
terrainPositions[i] = new WPos(r.ReadInt32(), r.ReadInt32(), r.ReadInt32());
target = Target.FromSerializedTerrainPosition(pos, terrainPositions);
}
}
break;
@@ -209,7 +220,7 @@ namespace OpenRA
default:
{
Log.Write("debug", "Received unknown order with type {0}", type);
Log.Write("debug", $"Received unknown order with type {type}");
return null;
}
}
@@ -217,7 +228,7 @@ namespace OpenRA
catch (Exception e)
{
Log.Write("debug", "Caught exception while processing order");
Log.Write("debug", e.ToString());
Log.Write("debug", e);
// HACK: this can hopefully go away in the future
TextNotificationsManager.Debug("Ignoring malformed order that would have crashed the game");
@@ -388,6 +399,21 @@ namespace OpenRA
w.Write(targetState.Pos.X);
w.Write(targetState.Pos.Y);
w.Write(targetState.Pos.Z);
// Don't send extra data over the network that will be restored by the Target ctor
var terrainPositions = targetState.TerrainPositions.Length;
if (terrainPositions == 1 && targetState.TerrainPositions[0] == targetState.Pos)
w.Write((short)-1);
else
{
w.Write((short)terrainPositions);
foreach (var position in targetState.TerrainPositions)
{
w.Write(position.X);
w.Write(position.Y);
w.Write(position.Z);
}
}
}
break;

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ namespace OpenRA.Network
{
public sealed class ReplayConnection : IConnection
{
class Chunk
sealed class Chunk
{
public int Frame;
public (int ClientId, byte[] Packet)[] Packets;
@@ -69,7 +69,7 @@ namespace OpenRA.Network
if (o.OrderString == "StartGame")
IsValid = true;
else if (o.OrderString == "SyncInfo" && !IsValid)
LobbyInfo = Session.Deserialize(o.TargetString);
LobbyInfo = Session.Deserialize(o.TargetString, o.OrderString);
}
}
}

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ using OpenRA.Primitives;
namespace OpenRA.Network
{
class SyncReport
sealed class SyncReport
{
const int NumSyncReports = 7;
static readonly Cache<Type, TypeInfo> TypeInfoCache = new(t => new TypeInfo(t));
@@ -119,12 +119,12 @@ namespace OpenRA.Network
{
desyncFrameFound = true;
var mod = Game.ModData.Manifest.Metadata;
Log.Write("sync", "Player: {0} ({1} {2} {3})", Game.Settings.Player.Name, Platform.CurrentPlatform, Environment.OSVersion, Platform.RuntimeVersion);
Log.Write("sync", $"Player: {Game.Settings.Player.Name} ({Platform.CurrentPlatform} {Environment.OSVersion} {Platform.RuntimeVersion})");
if (Game.IsHost)
Log.Write("sync", "Player is host.");
Log.Write("sync", "Game ID: {0} (Mod: {1} at Version {2})", orderManager.LobbyInfo.GlobalSettings.GameUid, mod.Title, mod.Version);
Log.Write("sync", "Sync for net frame {0} -------------", r.Frame);
Log.Write("sync", "SharedRandom: {0} (#{1})", r.SyncedRandom, r.TotalCount);
Log.Write("sync", $"Game ID: {orderManager.LobbyInfo.GlobalSettings.GameUid} (Mod: {mod.Title} at Version {mod.Version})");
Log.Write("sync", $"Sync for net frame {r.Frame} -------------");
Log.Write("sync", $"SharedRandom: {r.SyncedRandom} (#{r.TotalCount})");
Log.Write("sync", "Synced Traits:");
foreach (var a in r.Traits)
{
@@ -139,7 +139,7 @@ namespace OpenRA.Network
Log.Write("sync", "Synced Effects:");
foreach (var e in r.Effects)
{
Log.Write("sync", "\t {0} ({1})", e.Name, e.Hash);
Log.Write("sync", $"\t {e.Name} ({e.Hash})");
var nvp = e.NamesValues;
for (var i = 0; i < nvp.Names.Length; i++)
@@ -149,7 +149,7 @@ namespace OpenRA.Network
Log.Write("sync", "Orders Issued:");
foreach (var o in r.Orders)
Log.Write("sync", "\t {0}", o.ToString());
Log.Write("sync", $"\t {o}");
}
}
@@ -161,7 +161,7 @@ namespace OpenRA.Network
Log.Write("sync", $"Recorded frames do not contain the frame {frame}. No sync report available!");
}
class Report
sealed class Report
{
public int Frame;
public int SyncedRandom;
@@ -201,8 +201,12 @@ namespace OpenRA.Network
public TypeInfo(Type type)
{
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
var fields = type.GetFields(Flags).Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>());
var properties = type.GetProperties(Flags).Where(pi => pi.HasAttribute<SyncAttribute>());
var fields = type.GetFields(Flags)
.Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>())
.ToList();
var properties = type.GetProperties(Flags)
.Where(pi => pi.HasAttribute<SyncAttribute>())
.ToList();
foreach (var prop in properties)
if (!prop.CanRead || prop.GetIndexParameters().Length > 0)
@@ -300,7 +304,7 @@ namespace OpenRA.Network
public object this[int index]
{
get
readonly get
{
if (item2OrSentinel == Sentinel)
return ((object[])item1OrArray)[index];

View File

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

View File

@@ -49,13 +49,15 @@ namespace OpenRA
assemblies = assemblyList.SelectMany(asm => asm.GetNamespaces().Select(ns => (asm, ns))).ToArray();
}
void LoadAssembly(List<Assembly> assemblyList, string resolvedPath)
static void LoadAssembly(List<Assembly> assemblyList, string resolvedPath)
{
// .NET doesn't provide any way of querying the metadata of an assembly without either:
// (a) loading duplicate data into the application domain, breaking the world.
// (b) crashing if the assembly has already been loaded.
// We can't check the internal name of the assembly, so we'll work off the data instead
var hash = CryptoUtil.SHA1Hash(File.ReadAllBytes(resolvedPath));
string hash;
using (var stream = File.OpenRead(resolvedPath))
hash = CryptoUtil.SHA1Hash(stream);
if (!ResolvedAssemblies.TryGetValue(hash, out var assembly))
{
@@ -131,9 +133,9 @@ namespace OpenRA
public ConstructorInfo GetCtor(Type type)
{
var flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
var ctors = type.GetConstructors(flags).Where(x => x.HasAttribute<UseCtorAttribute>());
if (ctors.Count() > 1)
const BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
var ctors = type.GetConstructors(Flags).Where(x => x.HasAttribute<UseCtorAttribute>()).ToList();
if (ctors.Count > 1)
throw new InvalidOperationException("ObjectCreator: UseCtor on multiple constructors; invalid.");
return ctors.FirstOrDefault();
}
@@ -150,8 +152,8 @@ namespace OpenRA
for (var i = 0; i < p.Length; i++)
{
var key = p[i].Name;
if (!args.ContainsKey(key)) throw new InvalidOperationException($"ObjectCreator: key `{key}' not found");
a[i] = args[key];
if (!args.TryGetValue(key, out var arg)) throw new InvalidOperationException($"ObjectCreator: key `{key}' not found");
a[i] = arg;
}
return ctor.Invoke(a);

View File

@@ -6,12 +6,14 @@
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="6.0.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.4.0" />
<PackageReference Include="OpenRA-Eluant" Version="1.0.20" />
<PackageReference Include="Linguini.Bundle" Version="0.6.0" />
<PackageReference Include="OpenRA-Eluant" Version="1.0.22" />
<PackageReference Include="Mono.NAT" Version="3.0.4" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Threading.Channels" Version="6.0.0" />
</ItemGroup>
</Project>

View File

@@ -79,6 +79,72 @@ namespace OpenRA
}
}
public static string OperatingSystem
{
get
{
if (CurrentPlatform == PlatformType.Linux)
{
var desktopType = Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP");
var sessionType = Environment.GetEnvironmentVariable("XDG_SESSION_TYPE");
string suffix;
if (!string.IsNullOrEmpty(desktopType) && !string.IsNullOrEmpty(sessionType))
suffix = $" ({desktopType};{sessionType})";
else if (!string.IsNullOrEmpty(desktopType))
suffix = $" ({desktopType})";
else if (!string.IsNullOrEmpty(sessionType))
suffix = $" ({sessionType})";
else
suffix = "";
try
{
var psi = new ProcessStartInfo("hostnamectl", "status")
{
UseShellExecute = false,
RedirectStandardOutput = true
};
var p = Process.Start(psi);
string line;
while ((line = p.StandardOutput.ReadLine()) != null)
if (line.StartsWith("Operating System: ", StringComparison.Ordinal))
return line[18..] + suffix;
}
catch { }
if (File.Exists("/etc/os-release"))
foreach (var line in File.ReadLines("/etc/os-release"))
if (line.StartsWith("PRETTY_NAME=", StringComparison.Ordinal))
return line[13..^1] + suffix;
}
else if (CurrentPlatform == PlatformType.OSX)
{
try
{
var psi = new ProcessStartInfo("system_profiler", "SPSoftwareDataType")
{
UseShellExecute = false,
RedirectStandardOutput = true
};
var p = Process.Start(psi);
string line;
while ((line = p.StandardOutput.ReadLine()) != null)
{
line = line.Trim();
if (line.StartsWith("System Version: ", StringComparison.Ordinal))
return line[16..];
}
}
catch { }
}
return Environment.OSVersion.ToString();
}
}
/// <summary>
/// Directory containing user-specific support files (settings, maps, replays, game data, etc).
/// </summary>
@@ -208,7 +274,7 @@ namespace OpenRA
throw new DirectoryNotFoundException(path);
if (!path.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) &&
!path.EndsWith(Path.AltDirectorySeparatorChar.ToString(), StringComparison.Ordinal))
!path.EndsWith(Path.AltDirectorySeparatorChar.ToString(), StringComparison.Ordinal))
path += Path.DirectorySeparatorChar;
engineDirAccessed = true;

View File

@@ -133,7 +133,7 @@ namespace OpenRA
static FactionInfo ResolveDisplayFaction(World world, string factionName)
{
var factions = world.Map.Rules.Actors[SystemActors.World].TraitInfos<FactionInfo>().ToArray();
var factions = world.Map.Rules.Actors[SystemActors.World].TraitInfos<FactionInfo>();
return factions.FirstOrDefault(f => f.InternalName == factionName) ?? factions.First();
}
@@ -216,7 +216,7 @@ namespace OpenRA
{
var logic = PlayerActor.TraitsImplementing<IBot>().FirstOrDefault(b => b.Info.Type == BotType);
if (logic == null)
Log.Write("debug", "Invalid bot type: {0}", BotType);
Log.Write("debug", $"Invalid bot type: {BotType}");
else
logic.Activate(this);
}

View File

@@ -9,7 +9,6 @@
*/
#endregion
using System.Linq;
using System.Threading.Tasks;
using OpenRA.FileFormats;
using OpenRA.Graphics;
@@ -94,10 +93,10 @@ namespace OpenRA
});
}
var labelNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Label");
var icon24Node = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon24");
var icon48Node = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon48");
var icon72Node = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon72");
var labelNode = yaml.NodeWithKeyOrDefault("Label");
var icon24Node = yaml.NodeWithKeyOrDefault("Icon24");
var icon48Node = yaml.NodeWithKeyOrDefault("Icon48");
var icon72Node = yaml.NodeWithKeyOrDefault("Icon72");
if (labelNode == null)
return null;

View File

@@ -10,7 +10,6 @@
#endregion
using System.Collections.Generic;
using System.Linq;
namespace OpenRA
{
@@ -31,7 +30,7 @@ namespace OpenRA
{
var badges = new List<PlayerBadge>();
var badgesNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Badges");
var badgesNode = yaml.NodeWithKeyOrDefault("Badges");
if (badgesNode != null)
{
var playerDatabase = Game.ModData.Manifest.Get<PlayerDatabase>();

View File

@@ -56,17 +56,27 @@ namespace OpenRA.Primitives
int Index(DelayedAction action)
{
// Returns the index of the next action with a strictly greater time.
var index = actions.BinarySearch(action);
var index = actions.BinarySearch(action, DelayedAction.TimeComparer);
if (index < 0)
return ~index;
while (index < actions.Count && action.CompareTo(actions[index]) >= 0)
while (index < actions.Count && DelayedAction.TimeComparer.Compare(action, actions[index]) >= 0)
index++;
return index;
}
}
readonly struct DelayedAction : IComparable<DelayedAction>
readonly struct DelayedAction
{
sealed class DelayedActionTimeComparer : IComparer<DelayedAction>
{
public int Compare(DelayedAction x, DelayedAction y)
{
return x.Time.CompareTo(y.Time);
}
}
public static IComparer<DelayedAction> TimeComparer = new DelayedActionTimeComparer();
public readonly long Time;
public readonly Action Action;
@@ -76,11 +86,6 @@ namespace OpenRA.Primitives
Time = time;
}
public int CompareTo(DelayedAction other)
{
return Time.CompareTo(other.Time);
}
public override string ToString()
{
return "Time: " + Time + " Action: " + Action;

View File

@@ -86,7 +86,7 @@ namespace OpenRA.Primitives
public static BitSet<T> FromStringsNoAlloc(string[] values)
{
return new BitSet<T>(BitSetAllocator<T>.GetBitsNoAlloc(values)) { };
return new BitSet<T>(BitSetAllocator<T>.GetBitsNoAlloc(values));
}
public override string ToString()

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