Compare commits

..

431 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
1633 changed files with 33839 additions and 17295 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -15,10 +15,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'
@@ -39,7 +39,7 @@ jobs:
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Check Code
run: |
@@ -57,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'

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

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

@@ -33,7 +33,7 @@
<Optimize>false</Optimize>
<!-- Enable only for Debug builds to improve compile-time performance for Release builds -->
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<!-- Enabling GenerateDocumentationFile is required for IDE0005 (Remove unnecessary import)
<!-- Enabling GenerateDocumentationFile is required for IDE0005 (Remove unnecessary import)
rule to run in command line builds. https://github.com/dotnet/roslyn/issues/41640
Enable only for Debug builds to improve compile-time performance for Release builds -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -51,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

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

@@ -71,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);
@@ -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)
@@ -249,19 +252,44 @@ namespace OpenRA
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,7 +77,7 @@ 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)
@@ -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))
@@ -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);

View File

@@ -22,15 +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 T WithDefault<T>(T def, Func<T> f)
public static string FormatCurrent(this string format, params object[] args)
{
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); }
@@ -131,11 +130,35 @@ namespace OpenRA
return ret;
}
public static T GetOrAdd<T>(this HashSet<T> set, T value)
{
if (!set.TryGetValue(value, out var ret))
set.Add(ret = value);
return ret;
}
public static T GetOrAdd<T>(this HashSet<T> set, T value, Func<T, T> createFn)
{
if (!set.TryGetValue(value, out var ret))
set.Add(ret = createFn(value));
return ret;
}
public static int IndexOf<T>(this T[] array, T value)
{
return Array.IndexOf(array, value);
}
public static T FirstOrDefault<T>(this T[] array, Predicate<T> match)
{
return Array.Find(array, match);
}
public static T FirstOrDefault<T>(this List<T> list, Predicate<T> match)
{
return list.Find(match);
}
public static T Random<T>(this IEnumerable<T> ts, MersenneTwister r)
{
return Random(ts, r, true);
@@ -148,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)
{
@@ -303,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;
}
@@ -344,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;
}
@@ -356,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);
@@ -493,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);
}
@@ -513,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;
@@ -562,11 +615,6 @@ namespace OpenRA
{
return new LineSplitEnumerator(str.AsSpan(), separator);
}
public static bool TryParseInt32Invariant(string s, out int i)
{
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
}
public ref struct LineSplitEnumerator
@@ -581,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 },
@@ -118,7 +119,7 @@ namespace OpenRA
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];
@@ -195,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);
@@ -219,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);
}
@@ -235,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);
@@ -249,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);
}
@@ -259,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);
@@ -278,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);
@@ -292,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);
@@ -385,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;
}
@@ -398,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);
@@ -412,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);
@@ -460,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);
@@ -500,7 +520,7 @@ namespace OpenRA
if (yaml == null)
return Activator.CreateInstance(fieldType);
var dict = Activator.CreateInstance(fieldType, yaml.Nodes.Count);
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];
@@ -531,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 });
}

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,7 +68,7 @@ namespace OpenRA.FileFormats
Width = IPAddress.NetworkToHostOrder(ms.ReadInt32());
Height = IPAddress.NetworkToHostOrder(ms.ReadInt32());
var bitDepth = ms.ReadUInt8();
bitDepth = ms.ReadUInt8();
var colorType = (PngColorType)ms.ReadUInt8();
if (IsPaletted(bitDepth, colorType))
Type = SpriteFrameType.Indexed8;
@@ -76,7 +78,7 @@ namespace OpenRA.FileFormats
Data = new byte[Width * Height * PixelStride];
var compression = ms.ReadUInt8();
/*var filter = */ms.ReadUInt8();
ms.ReadUInt8(); // filter
var interlace = ms.ReadUInt8();
if (compression != 0)
@@ -92,8 +94,8 @@ 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.ReadUInt8(); var g = ms.ReadUInt8(); var b = ms.ReadUInt8();
Palette[i] = Color.FromArgb(r, g, b);
@@ -136,14 +138,34 @@ namespace OpenRA.FileFormats
{
var pxStride = PixelStride;
var rowStride = Width * pxStride;
var pixelsPerByte = 8 / bitDepth;
var sourceRowStride = Exts.IntegerDivisionRoundingAwayFromZero(rowStride, pixelsPerByte);
Span<byte> prevLine = new byte[rowStride];
for (var y = 0; y < Height; y++)
{
var filter = (PngFilter)ds.ReadUInt8();
ds.ReadBytes(Data, y * rowStride, rowStride);
ds.ReadBytes(Data, y * rowStride, sourceRowStride);
var line = Data.AsSpan(y * rowStride, rowStride);
// If the source has a bit depth of 1, 2 or 4 it packs multiple pixels per byte.
// Unpack to bit depth of 8, yielding 1 pixel per byte.
// This makes life easier for consumers of palleted data.
if (bitDepth < 8)
{
var mask = 0xFF >> (8 - bitDepth);
for (var i = sourceRowStride - 1; i >= 0; i--)
{
var packed = line[i];
for (var j = 0; j < pixelsPerByte; j++)
{
var dest = i * pixelsPerByte + j;
if (dest < line.Length) // Guard against last byte being only partially packed
line[dest] = (byte)(packed >> (8 - (j + 1) * bitDepth) & mask);
}
}
}
switch (filter)
{
case PngFilter.None:
@@ -269,7 +291,7 @@ namespace OpenRA.FileFormats
static bool IsPaletted(byte bitDepth, PngColorType colorType)
{
if (bitDepth == 8 && colorType == (PngColorType.Indexed | PngColorType.Color))
if (bitDepth <= 8 && colorType == (PngColorType.Indexed | PngColorType.Color))
return true;
if (bitDepth == 8 && colorType == (PngColorType.Color | PngColorType.Alpha))
@@ -287,10 +309,10 @@ namespace OpenRA.FileFormats
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);
@@ -302,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));
@@ -350,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);
}
@@ -371,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

@@ -84,7 +84,7 @@ namespace OpenRA.FileSystem
// in FileSystem.OpenPackage. Their internal name therefore contains the
// full parent path too. We need to be careful to not add a second path
// prefix to these hacked packages.
var filePath = filename.StartsWith(Name) ? 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))
@@ -98,7 +98,7 @@ namespace OpenRA.FileSystem
// in FileSystem.OpenPackage. Their internal name therefore contains the
// full parent path too. We need to be careful to not add a second path
// prefix to these hacked packages.
var filePath = filename.StartsWith(Name) ? 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

@@ -21,7 +21,7 @@ namespace OpenRA.FileSystem
{
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;

View File

@@ -180,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.
@@ -223,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()
@@ -416,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
@@ -524,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)
@@ -601,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);
@@ -610,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())
{
@@ -664,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)
@@ -674,7 +689,7 @@ namespace OpenRA
}
Ui.PrepareRenderables();
Renderer.WorldModelRenderer.EndFrame();
worldRenderer?.EndFrame();
}
// worldRenderer is null during the initial install/download screen
@@ -778,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
@@ -913,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()
{
@@ -935,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,6 +139,7 @@ namespace OpenRA
readyToResolve.Clear();
readyToResolve.AddRange(more);
}
#pragma warning restore CA1851
if (unresolved.Count != 0)
{
@@ -160,7 +162,7 @@ namespace OpenRA
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

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

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

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

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; }
sealed class PlaceholderModelCache : IModelCache
{
public IVertexBuffer<Vertex> VertexBuffer => throw new NotImplementedException();
public void Dispose() { }
public IModel GetModel(string model)
{
throw new NotImplementedException();
}
public IModel GetModelSequence(string model, string sequence)
{
throw new NotImplementedException();
}
public bool HasModelSequence(string model, string sequence)
{
throw new NotImplementedException();
}
}
public PlaceholderModelSequenceLoader(ModData modData) { }
public IModelCache CacheModels(IReadOnlyFileSystem fileSystem, ModData modData, IReadOnlyDictionary<string, MiniYamlNode> modelDefinitions)
{
return new PlaceholderModelCache();
}
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)
@@ -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>
@@ -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

@@ -24,11 +24,9 @@ namespace OpenRA.Graphics
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

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

@@ -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,6 +44,8 @@ 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)
{
@@ -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)
@@ -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

@@ -84,10 +84,11 @@ 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);

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

@@ -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,10 +98,10 @@ 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<object> GetEnumerator() { return initDict.Value.GetEnumerator(); }
@@ -139,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>();
}
@@ -152,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

@@ -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;
@@ -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
@@ -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("LockPreview", required: false, ignoreIfValue: "False"),
new MapField("Players", "PlayerDefinitions"),
new MapField("Actors", "ActorDefinitions"),
new MapField("Rules", "RuleDefinitions", required: false),
new MapField("Translations", "TranslationDefinitions", required: false),
new MapField("Sequences", "SequenceDefinitions", required: false),
new MapField("ModelSequences", "ModelSequenceDefinitions", required: false),
new MapField("Weapons", "WeaponDefinitions", required: false),
new MapField("Voices", "VoiceDefinitions", required: false),
new MapField("Music", "MusicDefinitions", required: false),
new MapField("Notifications", "NotificationDefinitions", required: false),
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
@@ -197,18 +198,18 @@ namespace OpenRA
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 TranslationDefinitions;
public readonly MiniYaml SequenceDefinitions;
public readonly MiniYaml ModelSequenceDefinitions;
public readonly MiniYaml WeaponDefinitions;
public readonly MiniYaml VoiceDefinitions;
public readonly MiniYaml MusicDefinitions;
public readonly MiniYaml NotificationDefinitions;
public 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();
@@ -274,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
@@ -357,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>();
@@ -607,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>();
@@ -646,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;
@@ -681,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++)
{
@@ -772,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
{
@@ -801,7 +799,7 @@ 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;
@@ -823,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;
@@ -846,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;
@@ -860,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)
@@ -1180,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
@@ -1249,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;
@@ -1349,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;
}
}
}
}

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);
@@ -128,7 +128,10 @@ namespace OpenRA
foreach (var kv in MapLocations)
{
foreach (var map in kv.Key.Contents)
{
LoadMapInternal(map, kv.Key, kv.Value, mapGrid, null, modDataRules);
GC.Collect();
}
}
// We only want to track maps in runtime, not at loadtime
@@ -148,11 +151,16 @@ 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, modDataRules);
// Freeing the package to save memory if there is a lot of Maps
previews[uid].PackageDispose();
if (oldMap != uid)
{
LastModifiedMap = uid;
@@ -168,9 +176,13 @@ namespace OpenRA
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);
}
}
@@ -190,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))
@@ -233,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)
@@ -249,15 +262,15 @@ 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)
@@ -269,7 +282,7 @@ namespace OpenRA
foreach (var uid in batchUids)
{
var p = previews[uid];
p.UpdateRemoteSearch(MapStatus.Unavailable, null);
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null);
mapQueryFailed?.Invoke(p);
}
}
@@ -282,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)
{
@@ -306,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)
@@ -348,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;
}
@@ -372,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,12 +116,12 @@ 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; }

View File

@@ -59,6 +59,7 @@ namespace OpenRA
public readonly string rules;
public readonly string players_block;
public readonly int mapformat;
public readonly string game_mod;
}
public sealed class MapPreview : IDisposable, IReadOnlyFileSystem
@@ -139,10 +140,11 @@ namespace OpenRA
files = files.Append(mapFiles);
}
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var sources =
modDataRules.Select(x => x.Where(IsLoadableRuleDefinition).ToList())
.Concat(files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s).Where(IsLoadableRuleDefinition).ToList()));
if (RuleDefinitions.Nodes.Count > 0)
.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);
@@ -172,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;
@@ -256,6 +274,7 @@ namespace OpenRA
{
this.cache = cache;
this.modData = modData;
this.pPackage = null;
Uid = uid;
innerData = new InnerData
@@ -282,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>();
@@ -333,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();
@@ -427,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;
@@ -474,11 +494,19 @@ namespace OpenRA
}
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();
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)
{
@@ -577,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

@@ -20,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))
@@ -43,22 +61,29 @@ namespace OpenRA
{
public readonly struct SourceLocation
{
public readonly string Filename;
public readonly string Name;
public readonly int Line;
public SourceLocation(string filename, int line)
public SourceLocation(string name, int line)
{
Filename = filename;
Name = name;
Line = line;
}
public override string ToString() { return $"{Filename}:{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)
{
@@ -74,26 +99,15 @@ 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 sealed class MiniYaml
@@ -101,15 +115,58 @@ namespace OpenRA
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()
@@ -125,7 +182,7 @@ namespace OpenRA
public Dictionary<TKey, TElement> ToDictionary<TKey, TElement>(
Func<string, TKey> keySelector, Func<MiniYaml, TElement> elementSelector)
{
var ret = new Dictionary<TKey, TElement>(Nodes.Count);
var ret = new Dictionary<TKey, TElement>(Nodes.Length);
foreach (var y in Nodes)
{
var key = keySelector(y.Key);
@@ -138,28 +195,27 @@ namespace OpenRA
}
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.TryGetValue(s, out var v) ? v.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)
@@ -175,7 +231,7 @@ namespace OpenRA
ReadOnlySpan<char> key = default;
ReadOnlySpan<char> value = default;
ReadOnlySpan<char> comment = default;
var location = new MiniYamlNode.SourceLocation(filename, lineNo);
var location = new MiniYamlNode.SourceLocation(name, lineNo);
if (line.Length > 0)
{
@@ -206,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.
@@ -236,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;
@@ -274,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();
@@ -281,39 +334,68 @@ 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)
{
var sourcesList = sources.ToList();
if (sourcesList.Count == 0)
@@ -336,7 +418,7 @@ namespace OpenRA
}
// Resolve any top-level removals (e.g. removing whole actor blocks)
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)).ToList());
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)));
return ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty);
}
@@ -345,19 +427,23 @@ namespace OpenRA
{
if (existingNodeKeys.Add(overrideNode.Key))
{
existingNodes.Add(overrideNode.Clone());
existingNodes.Add(overrideNode);
return;
}
var existingNode = existingNodes.Find(n => n.Key == overrideNode.Key);
existingNode.Value = MergePartial(existingNode.Value, overrideNode.Value);
existingNode.Value.Nodes = ResolveInherits(existingNode.Value, tree, inherited);
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, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
{
var resolved = new List<MiniYamlNode>(node.Nodes.Count);
var resolvedKeys = new HashSet<string>(node.Nodes.Count);
var resolved = new List<MiniYamlNode>(node.Nodes.Length);
var resolvedKeys = new HashSet<string>(node.Nodes.Length);
foreach (var n in node.Nodes)
{
@@ -379,7 +465,7 @@ namespace OpenRA
foreach (var r in ResolveInherits(parent, 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)
@@ -390,7 +476,6 @@ namespace OpenRA
MergeIntoResolved(n, resolved, resolvedKeys, tree, inherited);
}
resolved.TrimExcess();
return resolved;
}
@@ -398,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);
@@ -409,12 +494,12 @@ 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;
}
@@ -432,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;
@@ -452,7 +537,7 @@ namespace OpenRA
{
// Append Removal nodes to the result.
// Therefore: we know the remainder of the method deals with a plain node.
if (node.Key.StartsWith("-", StringComparison.Ordinal))
if (node.Key.StartsWith('-'))
{
ret.Add(node);
return;
@@ -468,9 +553,8 @@ namespace OpenRA
// A Removal node is closer than the previous node.
// We should not merge the new node, as the data being merged will jump before the Removal.
// Instead, append it so the previous node is applied, then removed, then the new node is applied.
var removalKey = $"-{node.Key}";
var previousNodeIndex = ret.FindLastIndex(n => n.Key == node.Key);
var previousRemovalNodeIndex = ret.FindLastIndex(n => n.Key == removalKey);
var previousNodeIndex = LastIndexOfKey(ret, node.Key);
var previousRemovalNodeIndex = LastIndexOfKey(ret, $"-{node.Key}");
if (previousRemovalNodeIndex != -1 && previousRemovalNodeIndex > previousNodeIndex)
{
ret.Add(node);
@@ -479,13 +563,30 @@ namespace OpenRA
// A previous node is present with no intervening Removal.
// We should merge the new one into it, in place.
ret[previousNodeIndex] = new MiniYamlNode(node.Key, MergePartial(ret[previousNodeIndex].Value, node.Value), node.Comment, node.Location);
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);
@@ -508,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,7 +33,6 @@ 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 ILoadScreen LoadScreen { get; }
@@ -90,15 +89,6 @@ namespace OpenRA
SpriteSequenceLoader = (ISpriteSequenceLoader)sequenceCtor.Invoke(new[] { this });
var modelFormat = Manifest.Get<ModelSequenceFormat>();
var modelLoader = ObjectCreator.FindType(modelFormat.Type + "Loader");
var modelCtor = modelLoader?.GetConstructor(new[] { typeof(ModData) });
if (modelLoader == null || !modelLoader.GetInterfaces().Contains(typeof(IModelSequenceLoader)) || modelCtor == null)
throw new InvalidOperationException($"Unable to find a model loader for type '{modelFormat.Type}'.");
ModelSequenceLoader = (IModelSequenceLoader)modelCtor.Invoke(new[] { this });
ModelSequenceLoader.OnMissingModelError = s => Log.Write("debug", s);
Hotkeys = new HotkeyManager(ModFiles, Game.Settings.Keys, Manifest);
defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this));
@@ -168,7 +158,8 @@ namespace OpenRA
public List<MiniYamlNode>[] GetRulesYaml()
{
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s)).ToArray();
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()

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());
}
@@ -288,35 +288,35 @@ namespace OpenRA.Network
{
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(MetadataMarker);
file.Write(LastOrdersFrame);
file.Write(LastSyncFrame);
file.Write(lastSyncPacket, 0, Order.SyncHashOrderLength);
var globalSettingsNodes = new List<MiniYamlNode>() { GlobalSettings.Serialize() };
file.WriteString(Encoding.UTF8, globalSettingsNodes.WriteToString());
file.WriteLengthPrefixedString(Encoding.UTF8, globalSettingsNodes.WriteToString());
var slotNodes = Slots
.Select(s => s.Value.Serialize())
.ToList();
file.WriteString(Encoding.UTF8, slotNodes.WriteToString());
file.WriteLengthPrefixedString(Encoding.UTF8, slotNodes.WriteToString());
var slotClientNodes = SlotClients
.Select(s => s.Value.Serialize(s.Key))
.ToList();
file.WriteString(Encoding.UTF8, slotClientNodes.WriteToString());
file.WriteLengthPrefixedString(Encoding.UTF8, slotClientNodes.WriteToString());
var traitDataOffset = file.Length;
file.Write(BitConverter.GetBytes(TraitDataMarker), 0, 4);
file.Write(TraitDataMarker);
var traitDataNodes = TraitData
.Select(kv => new MiniYamlNode(kv.Key.ToString(), kv.Value))
.Select(kv => new MiniYamlNode(kv.Key.ToStringInvariant(), kv.Value))
.ToList();
file.WriteString(Encoding.UTF8, traitDataNodes.WriteToString());
file.WriteLengthPrefixedString(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

@@ -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(MiniYaml yaml)
{
// Let the FieldLoader do the dirty work of loading the public fields.
FieldLoader.Load(this, yaml);
var argumentDictionary = new Dictionary<string, object>();
foreach (var argument in Arguments)
{
if (argument.Type == FluentArgument.FluentArgumentType.Number)
{
if (!double.TryParse(argument.Value, out var number))
Log.Write("debug", $"Failed to parse {argument.Value}");
argumentDictionary.Add(argument.Key, number);
}
else
argumentDictionary.Add(argument.Key, argument.Value);
}
TranslatedText = TranslationProvider.GetString(Key, argumentDictionary);
}
public static string Serialize(string key, 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

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

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

@@ -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,6 +20,24 @@ 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)
@@ -40,342 +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(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":
{
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;
{
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;
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
}
KickVoteTarget = (int)order.ExtraData;
break;
}
case "EndKickVote":
{
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;
if (KickVoteTarget == (int)order.ExtraData)
KickVoteTarget = null;
{
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;
}
}
}

View File

@@ -133,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();
}
@@ -152,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

@@ -10,11 +10,10 @@
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Linguini.Bundle" Version="0.5.0" />
<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

@@ -109,14 +109,14 @@ namespace OpenRA
var p = Process.Start(psi);
string line;
while ((line = p.StandardOutput.ReadLine()) != null)
if (line.StartsWith("Operating System: "))
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="))
if (line.StartsWith("PRETTY_NAME=", StringComparison.Ordinal))
return line[13..^1] + suffix;
}
else if (CurrentPlatform == PlatformType.OSX)
@@ -134,7 +134,7 @@ namespace OpenRA
while ((line = p.StandardOutput.ReadLine()) != null)
{
line = line.Trim();
if (line.StartsWith("System Version: "))
if (line.StartsWith("System Version: ", StringComparison.Ordinal))
return line[16..];
}
}
@@ -274,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();
}

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

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

View File

@@ -17,7 +17,7 @@ namespace OpenRA.Primitives
{
public readonly struct Color : IEquatable<Color>, IScriptBindable
{
readonly long argb;
readonly uint argb;
public static Color FromArgb(int red, int green, int blue)
{
@@ -26,7 +26,7 @@ namespace OpenRA.Primitives
public static Color FromArgb(int alpha, int red, int green, int blue)
{
return new Color(((byte)alpha << 24) + ((byte)red << 16) + ((byte)green << 8) + (byte)blue);
return new Color((uint)(((byte)alpha << 24) + ((byte)red << 16) + ((byte)green << 8) + (byte)blue));
}
public static Color FromAhsl(int alpha, float h, float s, float l)
@@ -55,14 +55,14 @@ namespace OpenRA.Primitives
return (A, h, s, v);
}
Color(long argb)
Color(uint argb)
{
this.argb = argb;
}
public int ToArgb()
public uint ToArgb()
{
return (int)argb;
return argb;
}
public static Color FromArgb(int alpha, Color baseColor)
@@ -70,14 +70,9 @@ namespace OpenRA.Primitives
return FromArgb(alpha, baseColor.R, baseColor.G, baseColor.B);
}
public static Color FromArgb(int argb)
{
return FromArgb((byte)(argb >> 24), (byte)(argb >> 16), (byte)(argb >> 8), (byte)argb);
}
public static Color FromArgb(uint argb)
{
return FromArgb((byte)(argb >> 24), (byte)(argb >> 16), (byte)(argb >> 8), (byte)argb);
return new Color(argb);
}
static float SrgbToLinear(float c)
@@ -154,7 +149,7 @@ namespace OpenRA.Primitives
// Wrap negative values into [0-1)
if (h < 0)
h += 1;
h++;
var s = delta / rgbMax;
return (h, s, v);
@@ -169,12 +164,12 @@ namespace OpenRA.Primitives
byte alpha = 255;
if (!byte.TryParse(value.AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var red)
|| !byte.TryParse(value.AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var green)
|| !byte.TryParse(value.AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var blue))
|| !byte.TryParse(value.AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var green)
|| !byte.TryParse(value.AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var blue))
return false;
if (value.Length == 8
&& !byte.TryParse(value.AsSpan(6, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out alpha))
&& !byte.TryParse(value.AsSpan(6, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out alpha))
return false;
color = FromArgb(alpha, red, green, blue);
@@ -224,9 +219,9 @@ namespace OpenRA.Primitives
public override string ToString()
{
if (A == 255)
return R.ToString("X2") + G.ToString("X2") + B.ToString("X2");
return CryptoUtil.ToHex(stackalloc byte[3] { R, G, B });
return R.ToString("X2") + G.ToString("X2") + B.ToString("X2") + A.ToString("X2");
return CryptoUtil.ToHex(stackalloc byte[4] { R, G, B, A });
}
public static Color Transparent => FromArgb(0x00FFFFFF);

View File

@@ -99,7 +99,7 @@ namespace OpenRA.Primitives
public static LongBitSet<T> FromStringsNoAlloc(string[] values)
{
return new LongBitSet<T>(LongBitSetAllocator<T>.GetBitsNoAlloc(values)) { };
return new LongBitSet<T>(LongBitSetAllocator<T>.GetBitsNoAlloc(values));
}
public static void Reset()

View File

@@ -13,13 +13,12 @@ using System;
namespace OpenRA.Primitives
{
public struct Rectangle : IEquatable<Rectangle>
public readonly struct Rectangle : IEquatable<Rectangle>
{
// TODO: Make these readonly: this will require a lot of changes to the UI logic
public int X;
public int Y;
public int Width;
public int Height;
public readonly int X;
public readonly int Y;
public readonly int Width;
public readonly int Height;
public static readonly Rectangle Empty;
public static Rectangle FromLTRB(int left, int top, int right, int bottom)
@@ -58,35 +57,35 @@ namespace OpenRA.Primitives
Height = size.Height;
}
public int Left => X;
public int Right => X + Width;
public int Top => Y;
public int Bottom => Y + Height;
public bool IsEmpty => X == 0 && Y == 0 && Width == 0 && Height == 0;
public int2 Location => new(X, Y);
public Size Size => new(Width, Height);
public readonly int Left => X;
public readonly int Right => X + Width;
public readonly int Top => Y;
public readonly int Bottom => Y + Height;
public readonly bool IsEmpty => X == 0 && Y == 0 && Width == 0 && Height == 0;
public readonly int2 Location => new(X, Y);
public readonly Size Size => new(Width, Height);
public int2 TopLeft => Location;
public int2 TopRight => new(X + Width, Y);
public int2 BottomLeft => new(X, Y + Height);
public int2 BottomRight => new(X + Width, Y + Height);
public readonly int2 TopLeft => Location;
public readonly int2 TopRight => new(X + Width, Y);
public readonly int2 BottomLeft => new(X, Y + Height);
public readonly int2 BottomRight => new(X + Width, Y + Height);
public bool Contains(int x, int y)
public readonly bool Contains(int x, int y)
{
return x >= Left && x < Right && y >= Top && y < Bottom;
}
public bool Contains(int2 pt)
public readonly bool Contains(int2 pt)
{
return Contains(pt.X, pt.Y);
}
public bool Equals(Rectangle other)
public readonly bool Equals(Rectangle other)
{
return this == other;
}
public override bool Equals(object obj)
public override readonly bool Equals(object obj)
{
if (obj is not Rectangle)
return false;
@@ -94,17 +93,17 @@ namespace OpenRA.Primitives
return this == (Rectangle)obj;
}
public override int GetHashCode()
public override readonly int GetHashCode()
{
return Height + Width ^ X + Y;
}
public bool IntersectsWith(Rectangle rect)
public readonly bool IntersectsWith(Rectangle rect)
{
return Left < rect.Right && Right > rect.Left && Top < rect.Bottom && Bottom > rect.Top;
}
bool IntersectsWithInclusive(Rectangle r)
readonly bool IntersectsWithInclusive(Rectangle r)
{
return Left <= r.Right && Right >= r.Left && Top <= r.Bottom && Bottom >= r.Top;
}
@@ -117,14 +116,14 @@ namespace OpenRA.Primitives
return FromLTRB(Math.Max(a.Left, b.Left), Math.Max(a.Top, b.Top), Math.Min(a.Right, b.Right), Math.Min(a.Bottom, b.Bottom));
}
public bool Contains(Rectangle rect)
public readonly bool Contains(Rectangle rect)
{
return rect == Intersect(this, rect);
}
public static Rectangle operator *(int a, Rectangle b) { return new Rectangle(a * b.X, a * b.Y, a * b.Width, a * b.Height); }
public override string ToString()
public override readonly string ToString()
{
return $"{X},{Y},{Width},{Height}";
}

View File

@@ -0,0 +1,146 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
namespace OpenRA.Primitives
{
/// <summary>Fixed size rorating buffer backed by an array.</summary>
public class RingBuffer<T> : ICollection<T>, IEnumerable<T>
{
readonly IComparer<T> comparer;
readonly T[] values;
int start;
public int Capacity => values.Length;
public int Count { get; private set; }
public bool IsReadOnly => false;
public RingBuffer(int capacity, IComparer<T> comparer)
{
this.comparer = comparer;
values = new T[capacity];
start = 0;
Count = 0;
}
public RingBuffer(int capacity)
: this(capacity, Comparer<T>.Default) { }
public void Add(T value)
{
values[(start + Count) % values.Length] = value;
if (Count < values.Length)
Count++;
else
start = (start + 1) % values.Length;
}
public void Clear()
{
Array.Clear(values, 0, values.Length);
start = 0;
Count = 0;
}
public bool Contains(T value)
{
var capacity = values.Length;
var end = start + Count;
for (var i = start; i < end; ++i)
if (comparer.Compare(values[i % capacity], value) == 0)
return true;
return false;
}
public void CopyTo(T[] array, int arrayIndex)
{
if (array == null)
throw new ArgumentNullException(nameof(array));
if (arrayIndex < 0)
throw new ArgumentNullException(nameof(arrayIndex));
if (arrayIndex + Count >= array.Length)
throw new ArgumentException("Invalid array capacity");
var destinationIndex = arrayIndex;
var end = start + Count;
var capacity = values.Length;
for (var i = start; i < end; ++i)
array[destinationIndex++] = values[i % capacity];
}
public bool Remove(T value)
{
var capacity = values.Length;
var end = start + Count;
for (var i = start; i < end; ++i)
{
if (comparer.Compare(values[i % capacity], value) == 0)
{
end--;
for (var j = i; j < end; ++j)
values[j % capacity] = values[(j + 1) % capacity];
Count--;
return true;
}
}
return false;
}
public T this[int pos]
{
get => values[(start + pos) % values.Length];
set
{
if (pos >= Count)
throw new ArgumentException($"Index out of bounds: {pos}");
values[(start + pos) % values.Length] = value;
}
}
public T First()
{
if (Count == 0)
throw new ArgumentException("Empty buffer");
return values[start];
}
public T Last()
{
if (Count == 0)
throw new ArgumentException("Empty buffer");
return values[(start + Count - 1) % values.Length];
}
public IEnumerator<T> GetEnumerator()
{
var initState = start + Count;
for (var i = 0; i < Count; i++)
{
if (start + Count != initState)
throw new InvalidOperationException("Collection was modified; enumeration operation may not execute");
yield return values[(start + i) % values.Length];
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); }
}
}

View File

@@ -12,14 +12,20 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace OpenRA.Primitives
{
public class TypeDictionary : IEnumerable<object>
{
static readonly Func<Type, List<object>> CreateList = type => new List<object>();
readonly Dictionary<Type, List<object>> data = new();
static readonly Func<Type, ITypeContainer> CreateTypeContainer = t =>
(ITypeContainer)typeof(TypeContainer<>).MakeGenericType(t).GetConstructor(Type.EmptyTypes).Invoke(null);
readonly Dictionary<Type, ITypeContainer> data = new();
ITypeContainer InnerGet(Type t)
{
return data.GetOrAdd(t, CreateTypeContainer);
}
public void Add(object val)
{
@@ -33,7 +39,7 @@ namespace OpenRA.Primitives
void InnerAdd(Type t, object val)
{
data.GetOrAdd(t, CreateList).Add(val);
InnerGet(t).Add(val);
}
public bool Contains<T>()
@@ -48,35 +54,33 @@ namespace OpenRA.Primitives
public T Get<T>()
{
return (T)Get(typeof(T), true);
return Get<T>(true);
}
public T GetOrDefault<T>()
{
var result = Get(typeof(T), false);
if (result == null)
return default;
return (T)result;
return Get<T>(false);
}
object Get(Type t, bool throwsIfMissing)
T Get<T>(bool throwsIfMissing)
{
if (!data.TryGetValue(t, out var ret))
if (!data.TryGetValue(typeof(T), out var container))
{
if (throwsIfMissing)
throw new InvalidOperationException($"TypeDictionary does not contain instance of type `{t}`");
return null;
throw new InvalidOperationException($"TypeDictionary does not contain instance of type `{typeof(T)}`");
return default;
}
if (ret.Count > 1)
throw new InvalidOperationException($"TypeDictionary contains multiple instances of type `{t}`");
return ret[0];
var list = ((TypeContainer<T>)container).Objects;
if (list.Count > 1)
throw new InvalidOperationException($"TypeDictionary contains multiple instances of type `{typeof(T)}`");
return list[0];
}
public IEnumerable<T> WithInterface<T>()
public IReadOnlyCollection<T> WithInterface<T>()
{
if (data.TryGetValue(typeof(T), out var objs))
return objs.Cast<T>();
if (data.TryGetValue(typeof(T), out var container))
return ((TypeContainer<T>)container).Objects;
return Array.Empty<T>();
}
@@ -92,18 +96,19 @@ namespace OpenRA.Primitives
void InnerRemove(Type t, object val)
{
if (!data.TryGetValue(t, out var objs))
if (!data.TryGetValue(t, out var container))
return;
objs.Remove(val);
if (objs.Count == 0)
container.Remove(val);
if (container.Count == 0)
data.Remove(t);
}
public void TrimExcess()
{
data.TrimExcess();
foreach (var objs in data.Values)
objs.TrimExcess();
foreach (var t in data.Keys)
InnerGet(t).TrimExcess();
}
public IEnumerator<object> GetEnumerator()
@@ -115,6 +120,36 @@ namespace OpenRA.Primitives
{
return GetEnumerator();
}
interface ITypeContainer
{
int Count { get; }
void Add(object value);
void Remove(object value);
void TrimExcess();
}
sealed class TypeContainer<T> : ITypeContainer
{
public List<T> Objects { get; } = new List<T>();
public int Count => Objects.Count;
public void Add(object value)
{
Objects.Add((T)value);
}
public void Remove(object value)
{
Objects.Remove((T)value);
}
public void TrimExcess()
{
Objects.TrimExcess();
}
}
}
public static class TypeExts

View File

@@ -27,7 +27,7 @@ namespace OpenRA
public SpriteRenderer WorldSpriteRenderer { get; }
public RgbaSpriteRenderer WorldRgbaSpriteRenderer { get; }
public RgbaColorRenderer WorldRgbaColorRenderer { get; }
public ModelRenderer WorldModelRenderer { get; }
public IRenderer[] WorldRenderers = Array.Empty<IRenderer>();
public RgbaColorRenderer RgbaColorRenderer { get; }
public SpriteRenderer SpriteRenderer { get; }
public RgbaSpriteRenderer RgbaSpriteRenderer { get; }
@@ -41,10 +41,13 @@ namespace OpenRA
internal IGraphicsContext Context { get; }
internal int SheetSize { get; }
internal int TempBufferSize { get; }
internal int TempVertexBufferSize { get; }
internal int TempIndexBufferSize { get; }
readonly IVertexBuffer<Vertex> tempBuffer;
readonly IVertexBuffer<Vertex> tempVertexBuffer;
readonly IIndexBuffer quadIndexBuffer;
readonly Stack<Rectangle> scissorState = new();
readonly ITexture worldBufferSnapshot;
IFrameBuffer screenBuffer;
Sprite screenSprite;
@@ -58,6 +61,15 @@ namespace OpenRA
public Size WorldFrameBufferSize => worldSheet.Size;
public int WorldDownscaleFactor { get; private set; } = 1;
/// <summary>
/// Copies and returns the currently rendered world state as a temporary texture.
/// </summary>
public ITexture WorldBufferSnapshot()
{
worldBufferSnapshot.SetDataFromReadBuffer(new Rectangle(int2.Zero, worldSheet.Size));
return worldBufferSnapshot;
}
SheetBuilder fontSheetBuilder;
readonly IPlatform platform;
@@ -75,24 +87,28 @@ namespace OpenRA
this.platform = platform;
var resolution = GetResolution(graphicSettings);
TempVertexBufferSize = graphicSettings.BatchSize - graphicSettings.BatchSize % 4;
TempIndexBufferSize = TempVertexBufferSize / 4 * 6;
Window = platform.CreateWindow(new Size(resolution.Width, resolution.Height),
graphicSettings.Mode, graphicSettings.UIScale, graphicSettings.BatchSize,
graphicSettings.VideoDisplay, graphicSettings.GLProfile, !graphicSettings.DisableLegacyGL);
graphicSettings.Mode, graphicSettings.UIScale, TempVertexBufferSize, TempIndexBufferSize,
graphicSettings.VideoDisplay, graphicSettings.GLProfile);
Context = Window.Context;
TempBufferSize = graphicSettings.BatchSize;
SheetSize = graphicSettings.SheetSize;
WorldSpriteRenderer = new SpriteRenderer(this, Context.CreateShader("combined"));
var combinedBindings = new CombinedShaderBindings();
WorldSpriteRenderer = new SpriteRenderer(this, Context.CreateShader(combinedBindings));
WorldRgbaSpriteRenderer = new RgbaSpriteRenderer(WorldSpriteRenderer);
WorldRgbaColorRenderer = new RgbaColorRenderer(WorldSpriteRenderer);
WorldModelRenderer = new ModelRenderer(this, Context.CreateShader("model"));
SpriteRenderer = new SpriteRenderer(this, Context.CreateShader("combined"));
SpriteRenderer = new SpriteRenderer(this, Context.CreateShader(combinedBindings));
RgbaSpriteRenderer = new RgbaSpriteRenderer(SpriteRenderer);
RgbaColorRenderer = new RgbaColorRenderer(SpriteRenderer);
tempBuffer = Context.CreateVertexBuffer(TempBufferSize);
tempVertexBuffer = Context.CreateVertexBuffer<Vertex>(TempVertexBufferSize);
quadIndexBuffer = Context.CreateIndexBuffer(Util.CreateQuadIndices(TempIndexBufferSize / 6));
worldBufferSnapshot = Context.CreateTexture();
}
static Size GetResolution(GraphicSettings graphicsSettings)
@@ -253,8 +269,6 @@ namespace OpenRA
if (lastWorldViewport != worldViewport)
{
WorldSpriteRenderer.SetViewportParams(worldSheet.Size, WorldDownscaleFactor, depthMargin, worldViewport.Location);
WorldModelRenderer.SetViewportParams();
lastWorldViewport = worldViewport;
}
@@ -300,9 +314,11 @@ namespace OpenRA
Flush();
currentPaletteTexture = palette.Texture;
SpriteRenderer.SetPalette(currentPaletteTexture, palette.ColorShifts);
WorldSpriteRenderer.SetPalette(currentPaletteTexture, palette.ColorShifts);
WorldModelRenderer.SetPalette(currentPaletteTexture);
SpriteRenderer.SetPalette(palette);
WorldSpriteRenderer.SetPalette(palette);
foreach (var r in WorldRenderers)
r.SetPalette(palette);
}
public void EndFrame(IInputHandler inputHandler)
@@ -326,27 +342,32 @@ namespace OpenRA
renderType = RenderType.None;
}
public void DrawBatch(Vertex[] vertices, int numVertices, PrimitiveType type)
{
tempBuffer.SetData(vertices, numVertices);
DrawBatch(tempBuffer, 0, numVertices, type);
}
public void DrawBatch(ref Vertex[] vertices, int numVertices, PrimitiveType type)
{
tempBuffer.SetData(ref vertices, numVertices);
DrawBatch(tempBuffer, 0, numVertices, type);
}
public void DrawBatch<T>(IVertexBuffer<T> vertices,
public void DrawBatch<T>(IVertexBuffer<T> vertices, IShader shader,
int firstVertex, int numVertices, PrimitiveType type)
where T : struct
{
vertices.Bind();
shader.Bind();
Context.DrawPrimitives(type, firstVertex, numVertices);
PerfHistory.Increment("batches", 1);
}
public void DrawQuadBatch(ref Vertex[] vertices, IShader shader, int numVertices)
{
tempVertexBuffer.SetData(ref vertices, numVertices);
DrawQuadBatch(tempVertexBuffer, quadIndexBuffer, shader, numVertices / 4 * 6, 0);
}
public void DrawQuadBatch<T>(IVertexBuffer<T> vertices, IIndexBuffer indices, IShader shader, int numIndices, int start)
where T : struct
{
vertices.Bind();
indices.Bind();
shader.Bind();
Context.DrawElements(numIndices, start);
PerfHistory.Increment("batches", 1);
}
public void Flush()
{
CurrentBatchRenderer = null;
@@ -374,9 +395,19 @@ namespace OpenRA
}
}
public IVertexBuffer<Vertex> CreateVertexBuffer(int length)
public IFrameBuffer CreateFrameBuffer(Size s)
{
return Context.CreateVertexBuffer(length);
return Context.CreateFrameBuffer(s);
}
public IShader CreateShader(IShaderBindings bindings)
{
return Context.CreateShader(bindings);
}
public IVertexBuffer<T> CreateVertexBuffer<T>(int length) where T : struct
{
return Context.CreateVertexBuffer<T>(length);
}
public void EnableScissor(Rectangle rect)
@@ -503,8 +534,11 @@ namespace OpenRA
public void Dispose()
{
WorldModelRenderer.Dispose();
tempBuffer.Dispose();
worldBuffer?.Dispose();
screenBuffer.Dispose();
worldBufferSnapshot.Dispose();
tempVertexBuffer.Dispose();
quadIndexBuffer.Dispose();
fontSheetBuilder?.Dispose();
if (Fonts != null)
foreach (var font in Fonts.Values)

View File

@@ -74,14 +74,17 @@ namespace OpenRA.Scripting
/// Provides global bindings in Lua code.
/// </summary>
/// <remarks>
/// <para>
/// Instance methods and properties declared in derived classes will be made available in Lua. Use
/// <see cref="ScriptGlobalAttribute"/> on your derived class to specify the name exposed in Lua. It is recommended
/// to apply <see cref="DescAttribute"/> against each method or property to provide a description of what it does.
///
/// </para>
/// <para>
/// Any parameters to your method that are <see cref="LuaValue"/>s will be disposed automatically when your method
/// completes. If you need to return any of these values, or need them to live longer than your method, you must
/// use <see cref="LuaValue.CopyReference"/> to get your own copy of the value. Any copied values you return will
/// be disposed automatically, but you assume responsibility for disposing any other copies.
/// </para>
/// </remarks>
public abstract class ScriptGlobal : ScriptObjectWrapper
{
@@ -99,7 +102,7 @@ namespace OpenRA.Scripting
if (names.Length != 1)
throw new InvalidOperationException($"[ScriptGlobal] attribute not found for global table '{type}'");
Name = names.First().Name;
Name = names[0].Name;
Bind(new[] { this });
}
@@ -111,7 +114,7 @@ namespace OpenRA.Scripting
{
using (var luaObject = a.ToLuaValue(Context))
using (var filterResult = filter.Call(luaObject))
using (var result = filterResult.First())
using (var result = filterResult[0])
return result.ToBoolean();
});
}
@@ -211,7 +214,7 @@ namespace OpenRA.Scripting
var ctor = b.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c =>
{
var p = c.GetParameters();
return p.Length == 1 && p.First().ParameterType == typeof(ScriptContext);
return p.Length == 1 && p[0].ParameterType == typeof(ScriptContext);
});
if (ctor == null)

View File

@@ -125,8 +125,8 @@ namespace OpenRA.Scripting
public static IEnumerable<MemberInfo> WrappableMembers(Type t)
{
// Only expose defined public non-static methods that were explicitly declared by the author
var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
foreach (var mi in t.GetMembers(flags))
const BindingFlags Flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
foreach (var mi in t.GetMembers(Flags))
{
// Properties are always wrappable
if (mi is PropertyInfo)
@@ -150,7 +150,7 @@ namespace OpenRA.Scripting
// Remove the namespace and the trailing "Info"
return types.SelectMany(i => i.GetGenericArguments())
.Select(g => g.Name.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault())
.Select(s => s.EndsWith("Info") ? s.Remove(s.Length - 4, 4) : s)
.Select(s => s.EndsWith("Info", StringComparison.Ordinal) ? s.Remove(s.Length - 4, 4) : s)
.ToArray();
}
}

View File

@@ -40,13 +40,10 @@ namespace OpenRA.Scripting
t = nullable;
// Value wraps a CLR object
if (value.TryGetClrObject(out var temp))
if (value.TryGetClrObject(out var temp) && temp.GetType() == t)
{
if (temp.GetType() == t)
{
clrObject = temp;
return true;
}
clrObject = temp;
return true;
}
if (value is LuaNil && !t.IsValueType)

View File

@@ -62,10 +62,7 @@ namespace OpenRA.Traits
public static Actor WithHighestSelectionPriority(this IEnumerable<ActorBoundsPair> actors, int2 selectionPixel, Modifiers modifiers)
{
if (!actors.Any())
return null;
return actors.MaxBy(a => CalculateActorSelectionPriority(a.Actor.Info, a.Bounds, selectionPixel, modifiers)).Actor;
return actors.MaxByOrDefault(a => CalculateActorSelectionPriority(a.Actor.Info, a.Bounds, selectionPixel, modifiers)).Actor;
}
public static FrozenActor WithHighestSelectionPriority(this IEnumerable<FrozenActor> actors, int2 selectionPixel, Modifiers modifiers)

View File

@@ -60,11 +60,11 @@ namespace OpenRA.Server
static byte[] CreatePingFrame()
{
var ms = new MemoryStream(21);
ms.WriteArray(BitConverter.GetBytes(13));
ms.WriteArray(BitConverter.GetBytes(0));
ms.WriteArray(BitConverter.GetBytes(0));
ms.Write(13);
ms.Write(0);
ms.Write(0);
ms.WriteByte((byte)OrderType.Ping);
ms.WriteArray(BitConverter.GetBytes(Game.RunTime));
ms.Write(Game.RunTime);
return ms.GetBuffer();
}
@@ -115,7 +115,7 @@ namespace OpenRA.Server
frame = BitConverter.ToInt32(bytes, 4);
state = ReceiveState.Data;
if (expectLength < 0 || (server.Type != ServerType.Local && expectLength > MaxOrderLength))
if (expectLength < 0 || (server.IsMultiplayer && expectLength > MaxOrderLength))
{
Log.Write("server", $"Closing socket connection to {EndPoint} because of excessive order length: {expectLength}");
return;
@@ -153,9 +153,8 @@ namespace OpenRA.Server
return;
// Regularly check player ping
if (lastPingSent.ElapsedMilliseconds > 1000)
if (TrySendData(CreatePingFrame()))
lastPingSent.Restart();
if (lastPingSent.ElapsedMilliseconds > 1000 && TrySendData(CreatePingFrame()))
lastPingSent.Restart();
// Send all data immediately, we will block again on read
while (sendQueue.TryTake(out var data, 0))

View File

@@ -68,7 +68,7 @@ namespace OpenRA.Server
ticksPerInterval = Interval / timestep;
this.players = players.ToList();
baselinePlayer = this.players.First();
baselinePlayer = this.players[0];
foreach (var player in this.players)
{
@@ -128,7 +128,7 @@ namespace OpenRA.Server
players.Remove(player);
if (player == baselinePlayer && players.Count > 0)
{
var newBaseline = players.First();
var newBaseline = players[0];
Interlocked.Exchange(ref baselinePlayer, newBaseline);
}

View File

@@ -77,6 +77,6 @@ namespace OpenRA.Server
// The protocol for server and world orders
// This applies after the handshake has completed, and is provided to support
// alternative server implementations that wish to support multiple versions in parallel
public const int Orders = 20;
public const int Orders = 21;
}
}

View File

@@ -40,8 +40,9 @@ namespace OpenRA.Server
public enum ServerType
{
Local = 0,
Multiplayer = 1,
Dedicated = 2
Skirmish = 1,
Multiplayer = 2,
Dedicated = 3
}
public sealed class Server
@@ -117,6 +118,7 @@ namespace OpenRA.Server
public readonly MersenneTwister Random = new();
public readonly ServerType Type;
public bool IsMultiplayer => Type == ServerType.Dedicated || Type == ServerType.Multiplayer;
public readonly List<Connection> Conns = new();
@@ -128,7 +130,8 @@ namespace OpenRA.Server
// Managed by LobbyCommands
public MapPreview Map;
public readonly MapStatusCache MapStatusCache;
public GameSave GameSave = null;
public GameSave GameSave;
public HashSet<string> MapPool;
// Default to the next frame for ServerType.Local - MP servers take the value from the selected GameSpeed.
public int OrderLatency = 1;
@@ -247,12 +250,9 @@ namespace OpenRA.Server
{
listener.Server.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 1);
}
catch (Exception ex)
catch (Exception ex) when (ex is SocketException || ex is ArgumentException)
{
if (ex is SocketException || ex is ArgumentException)
Log.Write("server", $"Failed to set socket option on {endpoint}: {ex.Message}");
else
throw;
Log.Write("server", $"Failed to set socket option on {endpoint}: {ex.Message}");
}
listener.Start();
@@ -282,7 +282,8 @@ namespace OpenRA.Server
}
}
}
}) { Name = $"Connection listener ({listener.LocalEndpoint})", IsBackground = true }.Start();
})
{ Name = $"Connection listener ({listener.LocalEndpoint})", IsBackground = true }.Start();
}
catch (SocketException ex)
{
@@ -305,10 +306,10 @@ namespace OpenRA.Server
randomSeed = (int)DateTime.Now.ToBinary();
if (type != ServerType.Local && settings.EnableGeoIP)
if (IsMultiplayer && settings.EnableGeoIP)
GeoIP.Initialize();
if (type != ServerType.Local)
if (IsMultiplayer)
Nat.TryForwardPort(Settings.ListenPort, Settings.ListenPort);
foreach (var trait in modData.Manifest.ServerTraits)
@@ -316,7 +317,6 @@ namespace OpenRA.Server
serverTraits.TrimExcess();
Map = ModData.MapCache[settings.Map];
MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks);
playerMessageTracker = new PlayerMessageTracker(this, DispatchOrdersToClient, SendLocalizedMessageTo);
@@ -327,8 +327,6 @@ namespace OpenRA.Server
GlobalSettings =
{
RandomSeed = randomSeed,
Map = Map.Uid,
MapStatus = Session.MapStatus.Unknown,
ServerName = settings.Name,
EnableSingleplayer = settings.EnableSingleplayer || Type != ServerType.Dedicated,
EnableSyncReports = settings.EnableSyncReports,
@@ -348,8 +346,7 @@ namespace OpenRA.Server
new Thread(_ =>
{
// Initial status is set off the main thread to avoid triggering a load screen when joining a skirmish game
LobbyInfo.GlobalSettings.MapStatus = MapStatusCache[Map];
// Note: at least one of these is required to set the initial LobbyInfo.Map and MapStatus
foreach (var t in serverTraits.WithInterface<INotifyServerStart>())
t.ServerStarted(this);
@@ -386,7 +383,7 @@ namespace OpenRA.Server
if (State == ServerState.ShuttingDown)
{
EndGame();
if (type != ServerType.Local)
if (IsMultiplayer)
Nat.TryRemovePortForward();
break;
}
@@ -439,8 +436,8 @@ namespace OpenRA.Server
{
// Send handshake and client index.
var ms = new MemoryStream(8);
ms.WriteArray(BitConverter.GetBytes(ProtocolVersion.Handshake));
ms.WriteArray(BitConverter.GetBytes(newConn.PlayerIndex));
ms.Write(ProtocolVersion.Handshake);
ms.Write(newConn.PlayerIndex);
newConn.TrySendData(ms.ToArray());
// Dispatch a handshake order
@@ -466,7 +463,7 @@ namespace OpenRA.Server
Conns.Add(newConn);
}
void ValidateClient(Connection newConn, string data)
void ValidateClient(Connection newConn, string data, string name)
{
try
{
@@ -479,7 +476,7 @@ namespace OpenRA.Server
return;
}
var handshake = HandshakeResponse.Deserialize(data);
var handshake = HandshakeResponse.Deserialize(data, name);
if (!string.IsNullOrEmpty(Settings.Password) && handshake.Password != Settings.Password)
{
@@ -494,7 +491,7 @@ namespace OpenRA.Server
{
Name = OpenRA.Settings.SanitizedPlayerName(handshake.Client.Name),
IPAddress = ipAddress.ToString(),
AnonymizedIPAddress = Type != ServerType.Local && Settings.ShareAnonymizedIPs ? Session.AnonymizeIP(ipAddress) : null,
AnonymizedIPAddress = IsMultiplayer && Settings.ShareAnonymizedIPs ? Session.AnonymizeIP(ipAddress) : null,
Location = GeoIP.LookupCountry(ipAddress),
Index = newConn.PlayerIndex,
PreferredColor = handshake.Client.PreferredColor,
@@ -583,8 +580,7 @@ namespace OpenRA.Server
Log.Write("server", $"{client.Name} ({newConn.EndPoint}) has joined the game.");
if (Type != ServerType.Local)
SendLocalizedMessage(Joined, Translation.Arguments("player", client.Name));
SendLocalizedMessage(Joined, Translation.Arguments("player", client.Name));
if (Type == ServerType.Dedicated)
{
@@ -607,7 +603,7 @@ namespace OpenRA.Server
}
}
if (Type == ServerType.Local)
if (!IsMultiplayer)
{
// Local servers can only be joined by the local client, so we can trust their identity without validation
client.Fingerprint = handshake.Fingerprint;
@@ -622,10 +618,11 @@ namespace OpenRA.Server
try
{
var httpClient = HttpClientFactory.Create();
var httpResponseMessage = await httpClient.GetAsync(playerDatabase.Profile + handshake.Fingerprint);
var url = playerDatabase.Profile + handshake.Fingerprint;
var httpResponseMessage = await httpClient.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")
{
profile = FieldLoader.Load<PlayerProfile>(yaml.Value);
@@ -708,19 +705,19 @@ namespace OpenRA.Server
static byte[] CreateFrame(int client, int frame, byte[] data)
{
var ms = new MemoryStream(data.Length + 12);
ms.WriteArray(BitConverter.GetBytes(data.Length + 4));
ms.WriteArray(BitConverter.GetBytes(client));
ms.WriteArray(BitConverter.GetBytes(frame));
ms.WriteArray(data);
ms.Write(data.Length + 4);
ms.Write(client);
ms.Write(frame);
ms.Write(data);
return ms.GetBuffer();
}
static byte[] CreateAckFrame(int frame, byte count)
{
var ms = new MemoryStream(14);
ms.WriteArray(BitConverter.GetBytes(6));
ms.WriteArray(BitConverter.GetBytes(0));
ms.WriteArray(BitConverter.GetBytes(frame));
ms.Write(6);
ms.Write(0);
ms.Write(frame);
ms.WriteByte((byte)OrderType.Ack);
ms.WriteByte(count);
return ms.GetBuffer();
@@ -729,9 +726,9 @@ namespace OpenRA.Server
static byte[] CreateTickScaleFrame(float scale)
{
var ms = new MemoryStream(17);
ms.WriteArray(BitConverter.GetBytes(9));
ms.WriteArray(BitConverter.GetBytes(0));
ms.WriteArray(BitConverter.GetBytes(0));
ms.Write(9);
ms.Write(0);
ms.Write(0);
ms.WriteByte((byte)OrderType.TickScale);
ms.Write(scale);
return ms.GetBuffer();
@@ -880,13 +877,13 @@ namespace OpenRA.Server
public void DispatchServerOrdersToClients(byte[] data, int frame = 0)
{
var from = 0;
var frameData = CreateFrame(from, frame, data);
const int From = 0;
var frameData = CreateFrame(From, frame, data);
foreach (var c in Conns.ToList())
if (c.Validated)
DispatchFrameToClient(c, from, frameData);
DispatchFrameToClient(c, From, frameData);
RecordOrder(frame, data, from);
RecordOrder(frame, data, From);
}
public void ReceiveOrders(Connection conn, int frame, byte[] data)
@@ -972,7 +969,7 @@ namespace OpenRA.Server
void WriteLineWithTimeStamp(string line)
{
Console.WriteLine($"[{DateTime.Now.ToString(Settings.TimestampFormat)}] {line}");
Console.WriteLine($"[{DateTime.Now.ToString(Settings.TimestampFormat, CultureInfo.CurrentCulture)}] {line}");
}
void InterpretServerOrder(Connection conn, Order o)
@@ -984,7 +981,7 @@ namespace OpenRA.Server
if (!conn.Validated)
{
if (o.OrderString == "HandshakeResponse")
ValidateClient(conn, o.TargetString);
ValidateClient(conn, o.TargetString, o.OrderString);
else
{
Log.Write("server", $"Rejected connection from {conn.EndPoint}; Order `{o.OrderString}` is not a `HandshakeResponse`.");
@@ -997,70 +994,39 @@ namespace OpenRA.Server
switch (o.OrderString)
{
case "Command":
{
if (!InterpretCommand(o.TargetString, conn))
{
var handledBy = serverTraits.WithInterface<IInterpretCommand>()
.FirstOrDefault(t => t.InterpretCommand(this, conn, GetClient(conn), o.TargetString));
if (handledBy == null)
{
Log.Write("server", $"Unknown server command: {o.TargetString}");
SendLocalizedMessageTo(conn, UnknownServerCommand, Translation.Arguments("command", o.TargetString));
}
break;
Log.Write("server", $"Unknown server command: {o.TargetString}");
SendLocalizedMessageTo(conn, UnknownServerCommand, Translation.Arguments("command", o.TargetString));
}
break;
}
case "Chat":
{
if (Type == ServerType.Local || !playerMessageTracker.IsPlayerAtFloodLimit(conn))
DispatchOrdersToClients(conn, 0, o.Serialize());
{
if (!IsMultiplayer || !playerMessageTracker.IsPlayerAtFloodLimit(conn))
DispatchOrdersToClients(conn, 0, o.Serialize());
break;
}
break;
}
case "GameSaveTraitData":
{
if (GameSave != null)
{
if (GameSave != null)
{
var data = MiniYaml.FromString(o.TargetString)[0];
GameSave.AddTraitData(int.Parse(data.Key), data.Value);
}
break;
var data = MiniYaml.FromString(o.TargetString, o.OrderString)[0];
GameSave.AddTraitData(OpenRA.Exts.ParseInt32Invariant(data.Key), data.Value);
}
break;
}
case "CreateGameSave":
{
if (GameSave != null)
{
if (GameSave != null)
{
// Sanitize potentially malicious input
var filename = o.TargetString;
var invalidIndex = -1;
var invalidChars = Path.GetInvalidFileNameChars();
while ((invalidIndex = filename.IndexOfAny(invalidChars)) != -1)
filename = filename.Remove(invalidIndex, 1);
var baseSavePath = Path.Combine(
Platform.SupportDir,
"Saves",
ModData.Manifest.Id,
ModData.Manifest.Metadata.Version);
if (!Directory.Exists(baseSavePath))
Directory.CreateDirectory(baseSavePath);
GameSave.Save(Path.Combine(baseSavePath, filename));
DispatchServerOrdersToClients(Order.FromTargetString("GameSaved", filename, true));
}
break;
}
case "LoadGameSave":
{
if (Type == ServerType.Dedicated || State >= ServerState.GameStarted)
break;
// Sanitize potentially malicious input
var filename = o.TargetString;
var invalidIndex = -1;
@@ -1068,62 +1034,90 @@ namespace OpenRA.Server
while ((invalidIndex = filename.IndexOfAny(invalidChars)) != -1)
filename = filename.Remove(invalidIndex, 1);
var savePath = Path.Combine(
var baseSavePath = Path.Combine(
Platform.SupportDir,
"Saves",
ModData.Manifest.Id,
ModData.Manifest.Metadata.Version,
filename);
ModData.Manifest.Metadata.Version);
GameSave = new GameSave(savePath);
LobbyInfo.GlobalSettings = GameSave.GlobalSettings;
LobbyInfo.Slots = GameSave.Slots;
if (!Directory.Exists(baseSavePath))
Directory.CreateDirectory(baseSavePath);
// Reassign clients to slots
// - Bot ordering is preserved
// - Humans are assigned on a first-come-first-serve basis
// - Leftover humans become spectators
// Start by removing all bots and assigning all players as spectators
foreach (var c in LobbyInfo.Clients)
{
if (c.Bot != null)
LobbyInfo.Clients.Remove(c);
else
c.Slot = null;
}
// Rebuild/remap the saved client state
// TODO: Multiplayer saves should leave all humans as spectators so they can manually pick slots
var adminClientIndex = LobbyInfo.Clients.First(c => c.IsAdmin).Index;
foreach (var kv in GameSave.SlotClients)
{
if (kv.Value.Bot != null)
{
var bot = new Session.Client()
{
Index = ChooseFreePlayerIndex(),
State = Session.ClientState.NotReady,
BotControllerClientIndex = adminClientIndex
};
kv.Value.ApplyTo(bot);
LobbyInfo.Clients.Add(bot);
}
else
{
// This will throw if the server doesn't have enough human clients to fill all player slots
// See TODO above - this isn't a problem in practice because MP saves won't use this
var client = LobbyInfo.Clients.First(c => c.Slot == null);
kv.Value.ApplyTo(client);
}
}
SyncLobbyInfo();
SyncLobbyClients();
break;
GameSave.Save(Path.Combine(baseSavePath, filename));
DispatchServerOrdersToClients(Order.FromTargetString("GameSaved", filename, true));
}
break;
}
case "LoadGameSave":
{
if (Type == ServerType.Dedicated || State >= ServerState.GameStarted)
break;
// Sanitize potentially malicious input
var filename = o.TargetString;
var invalidIndex = -1;
var invalidChars = Path.GetInvalidFileNameChars();
while ((invalidIndex = filename.IndexOfAny(invalidChars)) != -1)
filename = filename.Remove(invalidIndex, 1);
var savePath = Path.Combine(
Platform.SupportDir,
"Saves",
ModData.Manifest.Id,
ModData.Manifest.Metadata.Version,
filename);
GameSave = new GameSave(savePath);
LobbyInfo.GlobalSettings = GameSave.GlobalSettings;
LobbyInfo.Slots = GameSave.Slots;
// Reassign clients to slots
// - Bot ordering is preserved
// - Humans are assigned on a first-come-first-serve basis
// - Leftover humans become spectators
// Start by removing all bots and assigning all players as spectators
foreach (var c in LobbyInfo.Clients)
{
if (c.Bot != null)
LobbyInfo.Clients.Remove(c);
else
c.Slot = null;
}
// Rebuild/remap the saved client state
// TODO: Multiplayer saves should leave all humans as spectators so they can manually pick slots
var adminClientIndex = LobbyInfo.Clients.First(c => c.IsAdmin).Index;
foreach (var kv in GameSave.SlotClients)
{
if (kv.Value.Bot != null)
{
var bot = new Session.Client()
{
Index = ChooseFreePlayerIndex(),
State = Session.ClientState.NotReady,
BotControllerClientIndex = adminClientIndex
};
kv.Value.ApplyTo(bot);
LobbyInfo.Clients.Add(bot);
}
else
{
// This will throw if the server doesn't have enough human clients to fill all player slots
// See TODO above - this isn't a problem in practice because MP saves won't use this
var client = LobbyInfo.Clients.First(c => c.Slot == null);
kv.Value.ApplyTo(client);
}
}
SyncLobbyInfo();
SyncLobbyClients();
break;
}
}
}
}
@@ -1258,7 +1252,7 @@ namespace OpenRA.Server
lock (LobbyInfo)
{
// TODO: Only need to sync the specific client that has changed to avoid conflicts!
var clientData = LobbyInfo.Clients.Select(client => client.Serialize()).ToList();
var clientData = LobbyInfo.Clients.ConvertAll(client => client.Serialize());
DispatchServerOrdersToClients(Order.FromTargetString("SyncLobbyClients", clientData.WriteToString(), true));
@@ -1358,7 +1352,7 @@ namespace OpenRA.Server
State = ServerState.GameStarted;
if (Type != ServerType.Local)
if (IsMultiplayer)
OrderLatency = gameSpeed.OrderLatency;
if (GameSave == null && LobbyInfo.GlobalSettings.GameSavesEnabled)
@@ -1372,8 +1366,8 @@ namespace OpenRA.Server
{
startGameData = new List<MiniYamlNode>()
{
new MiniYamlNode("SaveLastOrdersFrame", GameSave.LastOrdersFrame.ToString()),
new MiniYamlNode("SaveSyncFrame", GameSave.LastSyncFrame.ToString())
new("SaveLastOrdersFrame", GameSave.LastOrdersFrame.ToStringInvariant()),
new("SaveSyncFrame", GameSave.LastSyncFrame.ToStringInvariant())
}.WriteToString();
}
}
@@ -1418,6 +1412,15 @@ namespace OpenRA.Server
}
}
public bool InterpretCommand(string command, Connection conn)
{
foreach (var t in serverTraits.WithInterface<IInterpretCommand>())
if (t.InterpretCommand(this, conn, GetClient(conn), command))
return true;
return false;
}
public ConnectionTarget GetEndpointForLocalConnection()
{
var endpoints = new List<DnsEndPoint>();
@@ -1435,6 +1438,27 @@ namespace OpenRA.Server
return new ConnectionTarget(endpoints);
}
public bool MapIsUnknown(string uid)
{
if (string.IsNullOrEmpty(uid))
return true;
var status = ModData.MapCache[uid].Status;
return status != MapStatus.Available && status != MapStatus.DownloadAvailable;
}
public bool MapIsKnown(string uid)
{
if (string.IsNullOrEmpty(uid))
return false;
if (MapPool != null && !MapPool.Contains(uid))
return false;
var status = ModData.MapCache[uid].Status;
return status == MapStatus.Available || status == MapStatus.DownloadAvailable;
}
interface IServerEvent { void Invoke(Server server); }
sealed class ConnectionConnectEvent : IServerEvent
@@ -1490,9 +1514,9 @@ namespace OpenRA.Server
readonly int[] pingHistory;
// TODO: future net code changes
#pragma warning disable IDE0052
#pragma warning disable IDE0052
readonly byte queueLength;
#pragma warning restore IDE0052
#pragma warning restore IDE0052
public ConnectionPingEvent(Connection connection, int[] pingHistory, byte queueLength)
{

View File

@@ -102,6 +102,9 @@ namespace OpenRA
[Desc("For dedicated servers only, treat maps that fail the lint checks as invalid.")]
public bool EnableLintChecks = true;
[Desc("For dedicated servers only, a comma separated list of map uids that are allowed to be used.")]
public string[] MapPool = Array.Empty<string>();
[Desc("Delay in milliseconds before newly joined players can send chat messages.")]
public int FloodLimitJoinCooldown = 5000;
@@ -210,9 +213,6 @@ namespace OpenRA
[Desc("Disable operating-system provided cursor rendering.")]
public bool DisableHardwareCursors = false;
[Desc("Disable legacy OpenGL 2.1 support.")]
public bool DisableLegacyGL = true;
[Desc("Display index to use in a multi-monitor fullscreen setup.")]
public int VideoDisplay = 0;
@@ -370,13 +370,14 @@ namespace OpenRA
public void Save()
{
var yamlCacheBuilder = yamlCache.ConvertAll(n => new MiniYamlNodeBuilder(n));
foreach (var kv in Sections)
{
var sectionYaml = yamlCache.FirstOrDefault(x => x.Key == kv.Key);
var sectionYaml = yamlCacheBuilder.FirstOrDefault(x => x.Key == kv.Key);
if (sectionYaml == null)
{
sectionYaml = new MiniYamlNode(kv.Key, new MiniYaml(""));
yamlCache.Add(sectionYaml);
sectionYaml = new MiniYamlNodeBuilder(kv.Key, new MiniYamlBuilder(""));
yamlCacheBuilder.Add(sectionYaml);
}
var defaultValues = Activator.CreateInstance(kv.Value.GetType());
@@ -393,27 +394,29 @@ namespace OpenRA
else
{
// Update or add the custom value
var fieldYaml = sectionYaml.Value.Nodes.FirstOrDefault(n => n.Key == fli.YamlName);
var fieldYaml = sectionYaml.Value.NodeWithKeyOrDefault(fli.YamlName);
if (fieldYaml != null)
fieldYaml.Value.Value = serialized;
else
sectionYaml.Value.Nodes.Add(new MiniYamlNode(fli.YamlName, new MiniYaml(serialized)));
sectionYaml.Value.Nodes.Add(new MiniYamlNodeBuilder(fli.YamlName, new MiniYamlBuilder(serialized)));
}
}
}
var keysYaml = yamlCache.FirstOrDefault(x => x.Key == "Keys");
var keysYaml = yamlCacheBuilder.FirstOrDefault(x => x.Key == "Keys");
if (keysYaml == null)
{
keysYaml = new MiniYamlNode("Keys", new MiniYaml(""));
yamlCache.Add(keysYaml);
keysYaml = new MiniYamlNodeBuilder("Keys", new MiniYamlBuilder(""));
yamlCacheBuilder.Add(keysYaml);
}
keysYaml.Value.Nodes.Clear();
foreach (var kv in Keys)
keysYaml.Value.Nodes.Add(new MiniYamlNode(kv.Key, FieldSaver.FormatValue(kv.Value)));
keysYaml.Value.Nodes.Add(new MiniYamlNodeBuilder(kv.Key, FieldSaver.FormatValue(kv.Value)));
yamlCache.WriteToFile(settingsFile);
yamlCacheBuilder.WriteToFile(settingsFile);
yamlCache.Clear();
yamlCache.AddRange(yamlCacheBuilder.Select(n => n.Build()));
}
static string SanitizedName(string dirty)
@@ -424,7 +427,7 @@ namespace OpenRA
var clean = dirty;
// reserved characters for MiniYAML and JSON
var disallowedChars = new char[] { '#', '@', ':', '\n', '\t', '[', ']', '{', '}', '"', '`' };
var disallowedChars = new char[] { '#', '@', ':', '\n', '\t', '[', ']', '{', '}', '<', '>', '"', '`' };
foreach (var disallowedChar in disallowedChars)
clean = clean.Replace(disallowedChar.ToString(), string.Empty);

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