544 Commits

Author SHA1 Message Date
hacker
d1c5b19697 Updated .NET to 8
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled
2026-07-20 20:41:41 +01:00
hacker
7e801c6fd7 Updated version
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled
2026-07-20 20:27:09 +01:00
hacker
93619f2766 Added Dockerfile 2026-07-20 20:22:44 +01:00
hacker
7c44f94755 Log chat messages on the server 2026-07-20 20:22:41 +01:00
Gustas
5c51f458f2 Silence autosave audio notification 2026-02-22 17:17:59 +00:00
Gustas
413aa00ce6 Don't play chat sounds when loading games 2026-02-22 17:17:59 +00:00
Gustas
0a9b493c14 Moved game save notification to StartGameNotification 2026-02-22 17:17:59 +00:00
N.N
3a86c035b3 Rework D2k campaign scripts:
- Add InitialProductionDelay
- Different attacksDelays for early game and late game
- Adjust T3 unit production for selected missions
- Implement InitialAttackDelay, TimeBetweenAttacks
- Implement routines for idle units
- Emergency mode
- Airstrike rework
2026-02-22 17:04:04 +02:00
Vapre
0a933ba09c CellLayer, use span. 2026-02-17 18:16:49 +02:00
Matthias Mailänder
6b04dbc210 Point to the log for long stack traces. 2026-02-17 18:11:26 +02:00
Matthias Mailänder
c651f733a7 Write to error output stream. 2026-02-17 18:11:26 +02:00
xan2622
4a04f8df83 Add command history to single-player and multiplayer
The last 50 valid commands starting with / are saved to memory.
Regular chat messages are not saved to prevent spam.
The command history persists across single-player and multiplayer games.
Press arrow up/down to show the previously typed commands within the current OpenRA session.
2026-02-15 22:32:45 +02:00
atlimit8
096ad0c413 add handing ±∞ to PerfGraphWidget.SixCharacterFormatFloat() 2026-02-15 22:20:04 +02:00
atlimit8
74c4bcc072 add unit tests for PerfGraphWidget.SixCharacterFormatFloat() 2026-02-15 22:20:04 +02:00
atlimit8
2516c5a798 Collect and show perf sample totals in widget
Both the rolling and cumulative active (not paused) means are shown.
2026-02-15 22:20:04 +02:00
Paul Chote
ef5541cce4 Add registered player tooltip to lobby chat names. 2026-02-07 18:21:18 +02:00
xan2622
efe7bce08a Shift Color Picker panel left if it would extend beyond the right edge of the screen 2026-02-07 18:02:01 +02:00
xan2622
a6f5002a84 Removed the "(Esc)" hotkey from the Options button in Spectator mode.
Made the button slightly smaller (130 pixels) but enough for most translations.
2026-02-07 17:51:51 +02:00
michaeldgg2
05250933e7 Fix typo (UtiliyHelpers) 2026-02-07 17:10:38 +02:00
Matthias Mailänder
f827421b37 Deduplicate ingame player strings. 2026-01-26 20:54:37 +02:00
Matthias Mailänder
ac162936ca These strings are already shared. 2026-01-26 20:54:37 +02:00
Gustas
2164700a7a Fix field saver tests not ignoring culture 2026-01-26 19:02:11 +01:00
Matthias Mailänder
d6af55bc04 Display the complete SDL version. 2026-01-26 18:50:21 +02:00
Matthias Mailänder
f8c20a0f8d Update to SDL 2.32.10 2026-01-26 18:50:21 +02:00
Matthias Mailänder
d6e2257158 Use oldest supported macOS. 2026-01-25 18:49:57 +00:00
Gustas
a0a0d832f9 Rerendered UI 2026-01-25 13:08:24 +01:00
Matthias Mailänder
c063e16c51 Display the line where a mismatch occurred. 2026-01-22 16:02:10 +02:00
Paul Chote
dd14d72ed3 Update TD map African Gambit. 2026-01-22 15:59:24 +02:00
Paul Chote
6743904372 Fix custom map loading from relative Engine.SupportDir paths. 2026-01-19 17:38:56 +02:00
michaeldgg2
a20ab8a33a PerfDebugLogic: show world tick in addition to local tick 2026-01-19 16:31:15 +02:00
Paul Chote
85e16032c2 Add Server.EnableMapGeneration setting. 2026-01-19 15:56:08 +02:00
Paul Chote
924c79b0ad Persist generated map across skirmish settings. 2026-01-19 15:56:08 +02:00
Paul Chote
ed1ebd536b Remove requirement for server to explicitly generate maps. 2026-01-19 15:56:08 +02:00
xan2622
156122f9e3 Improves observer camera focus fallback when the player's (or bot's) Construction Yard is destroyed
Currently, when an observer clicks on a player's name in the stats panel, the camera centers on that player's Construction Yard. However, if the Construction Yard has been destroyed, the click does nothing.

This commit searches for the Construction Yard, then a MCV, then any remaining building.
2026-01-19 15:47:23 +02:00
Matthias Mailänder
a6c58c0f68 Check if the minelayer is destroyed. 2026-01-19 15:36:10 +02:00
Gustas
ea07cb2cb1 Rewrite PNG parsing for minimal allocations 2026-01-19 15:26:21 +02:00
Gustas
bea698b2f1 Don't parse UTF 2026-01-19 15:26:21 +02:00
Gustas
9528f19845 Move to faster ZLibStream 2026-01-19 15:26:21 +02:00
Gustas
d0ca1af555 Use OpenRA CRC32 2026-01-19 15:26:21 +02:00
xan2622
78873b8e00 Replace labels on Map Editor (copy, paste, undo, redo) buttons by glyphs
Icons are from https://github.com/twbs/icons
2026-01-19 15:14:42 +02:00
RoosterDragon
e48a77b67a Fix a desync in HierarchicalPathFinder.
In a multiplayer game with bots running, only the host client runs bot logic. This logic must not alter the world state, or the host will desync from the other clients which do not run the bot logic.

The bot logic can interact with the pathfinder. This can eventually flow into HierarchicalPathFinder.BuildGrid which calls AbstractCellForLocalCells to determine the abstract cells within the grid.
This method chooses an abstract cell closest to the middle, but there can be multiple cells tied for this. The input comes from a local path search, whose ordering depends on current cell costs.
Previously, this ordering could would influence which of the equally close cells we would choose. In this case we'd choose the first in the ordering.

On non-host clients which don't run the bot logic, they may not build the grid until a later tick. If the cell costs have changed the the local path search would still explore the same set of cells, but the ordering may be different.
With the previous behaviour, they could choose a different abstract cell from the equally close cells. This results in a diverged state where their abstract graph differs subtly from the host abstract graph.
Eventually, a unit will path search in a manner where this state divergence causes it to take a different path, which will be detected as a desync.

To fix the issue, we ensure we pick the same abstract cell no matter the ordering of the list, by inserting an arbitrary tiebreaker rule.
This means the differing ordering of the explored cells from the local path search is still possible, but the ordering no longer influences the abstract cell we choose as a result.
This keeps the state of the abstract graph in sync across the clients, and thus prevents units from making different pathfinding decisions and desyncing.
2026-01-19 15:01:15 +02:00
Matthias Mailänder
95e698f44e Deduplicate observer stats translation strings. 2026-01-19 14:47:35 +02:00
Matthias Mailänder
a542767720 Deduplicate replay translation strings. 2026-01-19 14:47:35 +02:00
Paul Chote
c3f902ebcf Fix sequence lint checks for map-defined actors. 2026-01-19 14:32:30 +02:00
Paul Chote
34e1e05dd8 Revise several TD encyclopedia images. 2026-01-13 11:56:16 +02:00
Ashley Newson
df20709158 Rename "Experimental" map generator to "Classic"
Renames the "Experimental" map generator to "Classic" in internal
code, configuration, and user-facing text.

Additionally, replace the user-facing names for the Experimental and
D2K map generators (as seen in the map editor tools dropdown) with
"Map Generator".
2026-01-12 22:23:50 +02:00
Ashley Newson
2cf2d7eed7 Place map generator spawns more intuitively
The map generators previously placed spawns in an order that was
somewhat arbitrary and unintuitive to users.

This change provides and makes use of a Terraformer utility to reorder
spawns, based on their location, so that they typically make more
sense to users selecting spawns from a lobby.

Added to ExperimentalMapGenerator (RA, CnC), and D2kMapGenerator.

Note that as this changes the output of the map generator for a given
selection of settings. (Replays using generated maps from older
versions will lose compatibility and no longer load.)
2026-01-12 16:22:36 +02:00
Gustas
1300ecc18f Fix TS copy paste 2026-01-04 12:58:59 +00:00
Gustas
dcd4c41d81 Transition blits to CellCoordsRegion 2026-01-04 12:58:59 +00:00
Dominic Renaud
032f20b622 Fix selection cursor behaviour
Fix erroneous minimap selection cursor when not in classic mode

Fix selection cursor not showing up with units selected when not in classic mouse style
2026-01-04 12:35:35 +00:00
Dominic Renaud
4c2f52a465 Fix playing sounds when not adding new units to selection 2026-01-04 12:35:35 +00:00
Dominic Renaud
f8eefe310c Add input style to match other, non-C&C, RTS games
Uses left-click for targeted orders (attack move, guard, selecting movement types from the command bar, etc.) and right-click for contextual ones. Works like basically all non-C&C games

Removed GameSettings.UseClassicMouseStyle, as it is replaced by MouseControlStyle


Changed Minelayer and Chronotank teleport to UnitOrderGenerator

Fixes bug with Modern controls issuing a move order cancelling a mine field order

Better organizes all unit orders into a single order generator base class, rather than having their logic spread across multiple base classes, which required some hacks to get the control logic to match
2026-01-04 12:35:35 +00:00
RoosterDragon
c47ebfbb52 Fix tileset templates to appear in the map editor in their order in YAML.
Fixes regression from 649e7e8c28. When the Templates was changed from a Dictionary to a FrozenDictionary, this caused the items to be reordered. Some code was relying on the previous behaviour where the Dictionary ordering would happen to remain the same as the YAML input.

Fix this by creating a TemplatesInDefinitionOrder which can be used to iterate in the same ordering as given in YAML. Code that depends on the ordering can use this, and other code that does lookups can use the FrozenDictionary.
2026-01-04 12:13:34 +00:00
Matthias Mailänder
2c0a4f5873 Extract briefings for localisation. 2026-01-03 11:31:48 +02:00
Matthias Mailänder
81147bc3f3 Externalize briefing strings. 2026-01-03 11:31:48 +02:00
dnqbob
efa673d999 BaseBuilderBotModule: add cash constrain on expansion 2026-01-03 11:22:18 +02:00
Paul Chote
9e1f7cad55 Fix duplicated text notifications when sound device is not available. 2026-01-01 18:05:56 +02:00
Gustas
9fccc35046 Add cancel and exit keybinds to selection 2026-01-01 12:47:51 +00:00
Gustas
afc6532aa8 Add more robust input handling 2026-01-01 12:47:51 +00:00
Gustas
8869533d07 Fix error label being mispositioned 2026-01-01 12:47:51 +00:00
Pavel Penev
b198eec753 Fixed ExtractSpriteSequenceDocsCommand properties
This broke when SpriteSequenceField became a record.
2025-12-31 11:28:21 +02:00
Pavel Penev
f590e806a1 Added filenames to enum documentation 2025-12-31 11:28:21 +02:00
Pavel Penev
3f53abc4c4 Misc housekeeping
These popped up while testing the trait documentation extraction.
2025-12-31 11:28:21 +02:00
Pavel Penev
5de04b0db9 Renamed Utilities -> UtiliyHelpers for readability 2025-12-31 11:28:21 +02:00
Pavel Penev
0c1875db07 Reworked source file name lookups from PDBs 2025-12-31 11:28:21 +02:00
Gustas
638c0d7db1 Fix menu effects ignoring annotations 2025-12-30 22:57:55 +00:00
Gustas
6e98d7f73e Make sure trimmed frames never have floating point offset frames 2025-12-30 22:48:07 +00:00
Gustas
ec7e5021ae Selection can be null 2025-12-30 22:39:24 +00:00
Ashley Newson
b2f32f8907 Add "Plots" terrain type for RA and CnC map generation
Some common feedback about preview generated maps is that they are too
chokey. This change aims to add a new terrain type called "Plots" that
retains the variety of terrain features present in the Gardens
terrain, but opens up the play area a bit more. In essence, it strikes
a balance between the densely packed Gardens terrain type and the
loosely packed Plains and Parks terrain types.

This new terrain type notably uses a smaller noise feature size to
provide smaller but more regular map structure, and expands the
pathway cutouts and contour spacings for forests and cliffs.

The new Plots terrain type is made available for both RA and CNC and
becomes the new default.

This is an additive configuration-only change. Replay and multiplayer
map generation retains forward/backward compatibility.
2025-12-30 09:21:09 +02:00
Gustas
ed3f809982 Fix animations having wrong offsets 2025-12-29 23:11:24 +00:00
Gustas
e5cca8d0b4 Fix d2k crash 2025-12-29 23:05:44 +00:00
Paul Chote
f173b6f604 Account for fractional offsets in world-px to view-px conversion. 2025-12-29 20:57:16 +02:00
Gustas
e495f4eca0 Fix empty move commands being commited 2025-12-29 18:03:40 +00:00
Gustas
c07af73eae Fix viewport zoom repositioning ignoring map bounds 2025-12-29 17:57:39 +00:00
Paul Chote
eb09870a10 Fix landing altitude checks.
Both GetPosition and LandPosition were trying to solve the problem
of dynamic altitude offsets for carryalls with different-sized cargo.

However, because they both apply the offsets, when the two are used
together the two sets of offsets cancel to 0, producing incorrect behaviour.

GetPosition is removed in favor of using CenterPosition with the dynamic
LandAltitude. Fly.Tick and Carryall ignore the z coordinate so their
behaviour is unchanged. Land and Aircraft.AtLandAltitude now return
the correct result.
2025-12-29 16:19:57 +02:00
Paul Chote
0f22e3d4e4 Fix mission options peeking below the FMV playback. 2025-12-29 15:13:56 +02:00
Paul Chote
af199a3a37 Remove long-obsolete reference to the mod chooser. 2025-12-29 15:13:56 +02:00
Paul Chote
330f438a18 Support [IncludeFluentReferences] on non-enumerable custom types. 2025-12-29 15:13:56 +02:00
Paul Chote
c7e67529b5 Expose SDL_OpenURL to mod code. 2025-12-29 15:13:56 +02:00
Paul Chote
de952e6caf Provide the manifest to IFileSystemLoader.Mount. 2025-12-29 15:13:56 +02:00
Paul Chote
2246922118 Fix lobby crash if a map's DefaultCash isn't Selectable. 2025-12-29 13:53:13 +02:00
Paul Chote
fdb4ce4e0f Save map generation args in replays/saves instead of full map. 2025-12-28 16:19:14 +02:00
Paul Chote
0d479e7c18 Move settings tabs to fluent. 2025-12-28 15:53:16 +02:00
Paul Chote
4436f07fbf Move settings to Gameplay tab. 2025-12-28 15:53:16 +02:00
Paul Chote
27fbd3ddeb Restore lobby UI when a force-start is rejected by the server. 2025-12-28 15:35:39 +02:00
Paul Chote
6ae03038cf Fix crash if the host force-starts a game while in the Invalid state. 2025-12-28 15:35:39 +02:00
Paul Chote
17210c1088 Fix Carryalls attempting to deliver units that they aren't carrying. 2025-12-28 15:07:46 +02:00
RoosterDragon
827f4682a5 ActorReference takes MiniYaml as input instead of a Dict.
As the InitDict is lazy, this removes the possibility for a mutable collection to be used but the values not to be consumed until later, which would be confusing. As all existing callers actually create a throw-away dictionary from MiniYaml, this is an easy switch.
2025-12-28 14:12:02 +02:00
RoosterDragon
a546ca2f92 Tweaks for map generator performance.
The map generator is already performant, but we can remove a few rough edges from infrastructure it interacts with.

- Add TypeDictionary constructor to clone from an existing copy. The map editor infrastructure relies heavily on cloning ActorReferences and the underlying dictionaries. A dedicated clone method avoids reflection overhead for CreateTypeContainer on a fresh instance.
- Use ThrowIndexOutOfRangeException helper. This allows the Index method to be inlined.
- Reuse lists in FloodFill. This avoids allocating and resizing new lists on each loop when we can reuse the existing capacity.
- Manage CellEntryChanged at the layer rather than individual previews. We only need to attach/detach from the delegate once at the layer, rather than many times as previews are added and removed.
2025-12-28 14:12:02 +02:00
RoosterDragon
3ea5b08848 Fix usage of MapGeneratorMultiChoiceOption.Default
When this switched to an ImmutableArray, this expression has to be updated as the struct is not nullable. It supports an overloaded equals (==) but not a null-conditional (?.) operator. This expression was rewritten incorrectly and failed to use the first valid choice as a fallback in all cases. Update the code to do this.

Fixes regression from 649e7e8c28.
2025-12-23 20:35:45 +02:00
Gustas
4aec67c0c6 Display map name first 2025-12-22 23:05:07 +00:00
Paul Chote
bca1dd599d Remove remaining non-fluent server->client messages. 2025-12-22 23:05:07 +00:00
Gustas
d69deba56f Use lobby option cache for suppressing old options 2025-12-22 23:05:07 +00:00
Paul Chote
81fec36fbb Resolve lobby option notifications client-side. 2025-12-22 23:05:07 +00:00
Paul Chote
f8b9dfc73e Resolve mission briefing notifications client-side. 2025-12-22 23:05:07 +00:00
Pavel Penev
d4fd66adf9 Renamed SyncAttribute to VerifySyncAttribute 2025-12-22 19:09:53 +02:00
Paul Chote
ec75dcd9e9 Standardize and simplify settings menu settings access. 2025-12-22 15:28:09 +02:00
Paul Chote
2e16ad47e5 Migrate skirmish state to a mod-specific settings module. 2025-12-22 15:28:09 +02:00
Paul Chote
7e9d87a808 Move auto-saving settings to mod code. 2025-12-22 15:28:09 +02:00
Paul Chote
98f7fe0db7 Remove special-case hotkey parsing. 2025-12-22 15:28:09 +02:00
Paul Chote
575f6624a2 Implement modular settings backend. 2025-12-22 15:28:09 +02:00
Paul Chote
962e7e911d Replace MouseButtonPreference with MouseActionType. 2025-12-22 10:34:40 +02:00
Pavel Penev
c588c3ef8d Added missing ISync for conditional traits 2025-12-22 09:28:07 +02:00
Pavel Penev
70624fa44f Stopped syncing IsTraitPaused/Disabled
A discussion on Discord (https://discord.com/channels/153649279762694144/388282819371204608/1181890238759587881) decided we don't need to sync these.
2025-12-22 09:28:07 +02:00
michaeldgg2
06e3e013be ActorIndex: make constructor protected to allow defining custom actor indexes 2025-12-21 14:21:42 +02:00
RoosterDragon
06e4a2e010 Reduce allocations in main game loop.
Target a couple of hot paths:
- In Actor, cache the renderables enumerable by tracking the last used WorldRenderer. It's slightly uncouth but in practise the WorldRenderer will be the same for the lifetime of the Actor and outlive it anyway.
- In RenderSprites, cache the renderables enumerable. If we refresh the palettes directly, we don't need the WorldRenderer for anything else so can return a cached enumerable. This could result in iterating the animation list twice, so use a flag to only do it when required, which is rare.
- In AutoTarget, the compiler has an unfortunate behaviour where it allocates the helper class for a capturing lambda at the top of the loop, but it's on a rare path and so this allocation is usually unused. We can discourage the compiler from allocating until it's actually needed by wrapping the call in a local function.
2025-12-20 15:13:15 +02:00
RoosterDragon
bcfaa44d66 Amortize allocations when sorting renderables.
Using OrderBy provides a stable sort, but internally allocates a buffer every time for storing the sort keys. We'd like to avoid this allocation, but we also can't directly use Array.Sort/Span.Sort as these are unstable sorts.

By calculating sort keys with the item index embedded, we can ensure a stable sorting result whilst being able to reuse the buffer for the sort keys across future calls.
2025-12-15 19:33:19 +02:00
Matthias Mailänder
1539741e71 Don't run the CI build when only Python scripts are edited. 2025-12-15 19:14:12 +02:00
Matthias Mailänder
3ad38ba503 Shorten trait requirement prefix. 2025-12-15 19:14:12 +02:00
dependabot[bot]
7cef83c51c Bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 14:40:21 +01:00
Gustas
493cf3f1eb Update TD map pool 2025-12-12 23:05:10 +00:00
Paul Chote
3c7389f582 Implement InstallShieldPackage.OpenPackage. 2025-12-12 12:39:21 +02:00
Paul Chote
374166e9d3 Add a minimal iso9660 package reader. 2025-12-12 12:39:21 +02:00
Paul Chote
31607bd0cf Stop passing around partially constructed ModData instances. 2025-12-12 12:32:10 +02:00
Paul Chote
2b6977d53f Remove legacy SpriteSequenceLoader metadata plumbing. 2025-12-12 12:32:10 +02:00
Paul Chote
a04cfaec78 Move GlobalModData state to ModData. 2025-12-12 12:32:10 +02:00
Paul Chote
13313b4270 Finish removing GraphicSettings.SheetSize/BatchSize. 2025-12-12 12:32:10 +02:00
Paul Chote
7175e9062d Initialize mod using its manifest. 2025-12-12 12:32:10 +02:00
Paul Chote
94177848a8 Remove CursorProvider. 2025-12-12 12:32:10 +02:00
RoosterDragon
9e548fd0f0 Freeze non-mutable readonly static dictionaries.
Since these will live for the lifetime of the process, it is worth freezing them on creation to improve lookup performance. If the dictionaries were iterated, change to an immutable array instead to preserve the order.
2025-12-12 09:51:27 +02:00
Gustas
f4eb5739d6 Fix PauseShellmap not pausing 2025-12-10 21:42:34 +00:00
Paul Chote
637d8689f5 Fix invalid cast. 2025-12-08 21:37:57 +02:00
Gustas
a3ee6645c1 Fix units being picked up when they no longer want it 2025-12-08 18:04:01 +00:00
RoosterDragon
649e7e8c28 Change classes that use FieldLoader to use read-only collections. 2025-12-08 17:39:28 +00:00
RoosterDragon
797c71e500 Add FieldLoader/Saver support for ImmutableArray, FrozenSet, FrozenDictionary.
As config is often meant to be loaded and then never modified, it is helpful to support collections that are read-only after creation. This allows various classes that load config from YAML and elsewhere to deserialize straight into read-only collections and enforce invariants around data mutation.
2025-12-08 17:39:28 +00:00
Gustas
e53f2b6044 Fix templete palette overwrites not working 2025-12-08 17:32:05 +00:00
Gustas
2b2e2d6fe3 Fix itch integration ruining commander's name 2025-12-06 14:05:09 +02:00
Gustas
027e38ee69 Handle race conditions and manage package ownership 2025-12-05 18:17:15 +02:00
Gustas
9e926b0f53 Fix large dropdowns overlapping 2025-12-05 18:17:15 +02:00
Gustas
f9bce6252c Don't generate maps when not needed 2025-12-05 18:17:15 +02:00
Gustas
dd1528bd4a No need for virtualization 2025-12-05 18:17:15 +02:00
Paul Chote
79567f3f8a Move content mod switching to IFileSystemExternalContent interface. 2025-12-04 14:01:46 +02:00
RoosterDragon
8b8651dcf7 Improve audio parsing performance.
- Improve the AudReader and WavReader by performing fewer, larger stream reads to consume bytes more efficiently.
- Improve ImaAdpcmReader.LoadImaAdpcmSound by accepting an output buffer rather than allocating a new array.
- Improve StreamExts.ReadAllBytes by using MemoryStream.CopyTo. This internally rents a much larger buffer from the array pool, allowing for fewer, larger stream reads.
2025-12-03 12:35:42 +02:00
Gustas
b429ca7879 Don't paste when empty 2025-12-02 22:05:38 +02:00
Gustas
1ad1d18abf Don't copy tiles when empty 2025-12-02 22:05:38 +02:00
Gustas
93602c7c95 Make copy paste message more accurate 2025-12-02 22:05:38 +02:00
dnqbob
62e692063a Add NewProductionChance so bot won't only build production when too much cash 2025-12-01 14:55:22 +02:00
Ashley Newson
9478548bdb Allow empty MultiBrush with Replaceability.Any
Painting a MultiBrush previously required it to be non-empty. When
using Replaceability.Any, this is unlikely to be the desire behavior,
as some utilities (such as the upcoming LatTiler and RampTiler) may
reasonably generate an empty MultiBrush as output.

This changes painting MultiBrushes using Replaceability.Any to be
effectively no-ops. The .Tile and .Actor cases still use checks, as
these still plausibly represent mistakes.
2025-12-01 14:07:28 +02:00
dnqbob
8dc75eec34 Allow bot repairs all buildings. 2025-12-01 14:00:02 +02:00
dnqbob
3e7e4df2ed Bot's GetNearbyIndicesThreat: fix the wrong parameter passing 2025-12-01 11:52:32 +02:00
Gustas
7e8f4a3479 Truncate less of the player name, adda tooltip 2025-11-24 17:43:12 +02:00
Gustas
86a4fb1d6f Fix illegal resources being placed by pasting 2025-11-24 17:41:06 +02:00
Gustas
5ff7c9f269 Fix scaling issues 2025-11-24 12:33:53 +02:00
Gustas
b340c42ab1 We get get sprites earlier 2025-11-24 12:33:53 +02:00
Gustas
ec7461f09f Remove redundant fields 2025-11-24 12:33:53 +02:00
RoosterDragon
ba632fbf8b Revert "In RemoveCellsFromPlayerShroud, don't call RemoveSource unless required."
This reverts commit 86b9227577.

This commit relied on the relationship not changing, but when a player is defeated their relationship can change due to becoming a spectator. This can cause a crash if the shroud is not removed. Revert the commit to resolve this crash.
2025-11-24 12:14:53 +02:00
Gustas
89a4823a83 Add a lint test for running update rules 2025-11-24 11:52:56 +02:00
Gustas
bd53aa734e Handle comments when merging yaml 2025-11-24 11:52:56 +02:00
Ashley Newson
cde5208c54 Add basic Path Tiling for TS
This introduces basic Path Tiler map editor tool functionality to TS
with some segmented MultiBrush definitions. The included MultiBrush
definitions cover both the Temperate and Snow tilesets's Beach, Cliff,
and WaterCliff tiles.

MultiBrush now incorporates Height and Ramp information. Some
associated Terraformer utilities are updated with the API surface
change.

There are some known limitations that are not addressed in this PR:
- The path laying UI doesn't account for the map's current heights.
- The preview does not use a correct height or Z-ordering.
2025-11-23 14:31:31 +02:00
RoosterDragon
e652f95be9 Remove Exts.Enum<T>
This functionality is now built in. We can rely on the built in functions rather than a custom class.
2025-11-23 13:34:31 +02:00
Ashley Newson
00cd438190 Create coordinate system conversion wrapper around Mirror
The Symmetry.Mirror enum was previously documented as defined in terms
of the WPos system, which happens to align with CPos for Rectangular
grid types. However, the existing code gets confused when attempting
to support RectangularIsometric, due to a lack of appropriate
conversion logic.

This change makes Mirror agnostic to specific coordinate systems and
introduces a WMirror wrapper that provides getters for the correct
Mirror configuration given the coordinate system being used.
2025-11-23 13:25:49 +02:00
Ashley Newson
91ddfd6fc1 Fix various map generator bounds and size related issues
- If generating a RectangularIsometric map via lobby, make the height
  twice the width so that the map is approximately world-square.
- Use equal top and bottom cordons for RectangularIsometric maps.
- Change some map bounds checking.
- Check that actors are at least partially inside map.Contains before
  placing. (To avoid frozen actor crashes.)
- Fix editor generated map blitting for RectangularIsometric.
2025-11-23 13:16:29 +02:00
dependabot[bot]
915ad36ddc Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-21 15:03:29 +01:00
Ashley Newson
56e6a9e6dc Improve MultiBrush collection shorthands
This expands upon the limited FromTemplates MultiBrush collection
shorthand:

- Adds a corresponding FromActors shorthand.
- Allows FromTemplates and FromActors to mix in additional properties,
  such as weights and backing tiles.
- Updates the MultiBrush collections used across the official mods to
  make use of these shorthands.

Note that this reorders some MultiBrush definitions for
conciseness. This partially (cosmetically) re-randomizes map generator
output.
2025-11-16 21:01:29 +02:00
RoosterDragon
bc1a901f54 Add tests for FieldLoader and FieldSaver
Test coverage for these classes will help prevent regressions. Major breaking changes are avoided since config files may already rely on various aspects of the behaviour, but some small breaking changes are made:

- Use `value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)` consistently for all comma-separated parsing.
- Rename LoadField to LoadFieldOrProperty for clarity - since this is the one method that cares about properties in a class otherwise focused only on fields.
- Use TryParse instead of Parse and ensure to call InvalidValueAction from all parsers so we throw the intended exception when badly formatted data is encountered.
- BooleanExpression/IntegerExpression exception message updated to align with the generic one.
- Remove FromYamlKey, and update the only user SkirmishLogic to perform this logic manually instead.
- Remove unused includePrivateByDefault parameter on GetTypeLoadInfo.
- Remove unused AllowEmptyEntriesAttribute.
2025-11-16 20:52:54 +02:00
RoosterDragon
7d0340ad41 Add new .NET 10 rules to editorconfig. 2025-11-16 20:29:21 +02:00
RoosterDragon
42ac32dace Fix CA1515 2025-11-16 20:29:21 +02:00
RoosterDragon
24c19f64c9 Fix IDE0350 2025-11-16 20:29:21 +02:00
RoosterDragon
af326bfeb8 Fix IDE0031 2025-11-16 20:29:21 +02:00
RoosterDragon
3745377387 Fix IDE00007 2025-11-16 20:29:21 +02:00
dnqbob
0ed00a5766 increase McvExpansionManagerBotModule enemy base avoidance weight 2025-11-10 19:27:53 +02:00
Ashley Newson
6f6d039650 Fix Path Tiler UI positioning for RectangularIsometric
The on-map path editor for the Path Tiler had incorrect offsets and
direction markers when used with a RectangularIsometric grid. This
fixes the grid alignment.

Note that map height information is currently ignored by the tool.
2025-11-10 17:29:11 +02:00
Ashley Newson
2359dce21b Relax TilingPath constraints on segments that don't make immediate progress
The TilingPath algorithm previously rejected placing segments that do
not make immediate progress, unless they faced towards positive
progress.

However, there are cases in some mods where zero-progress segments
that end facing towards neutral progress are needed to arrive at a
tiling solution. This change allows these neutral progressions, so
long as they aren't for single-point segments.
2025-11-10 15:46:52 +02:00
Ashley Newson
fd5e05ca6b Fix rare PointsChirality infinite loop
Under some inputs, MatrixUtils.PointsChirality could enter into an
infinite loop consuming memory until an OOM occurs. This was due to
the FloodFill-based operation endlessly propagate zero values.

This adds a defensive check to prevent propagating zero values.
2025-11-10 15:40:44 +02:00
Paul Chote
3177de7b2c Add OnBuildingPlaced lua trigger. 2025-11-10 15:24:12 +02:00
Gustas
eb550bbbbb Allow to extract translations from a single map 2025-11-09 19:52:25 +02:00
Gustas
7004b552c9 Add ActorMapOverlay 2025-11-07 13:04:12 +02:00
dnqbob
3f19c81392 Fix suqadmanager crashing on ProtectOwn 2025-11-06 17:05:58 +02:00
Gustas
a62b085f1d Add a name to the server thread 2025-11-04 21:43:18 +02:00
Gustas
a514f3a388 Make map grid immutable 2025-11-04 21:41:31 +02:00
RoosterDragon
ebae15a9fe Fix sprite sequence docs failing to lookup property names.
When SpriteSequenceField was changed to a record struct, the field lookups needed to be changed to property lookups.

Fixes regression from 36660b89e9.
2025-11-04 14:39:06 +02:00
Paul Chote
10db26fa0b Disable lobby options tab if options aren't yet known. 2025-11-03 14:03:35 +02:00
Paul Chote
47105e42cd Populate lobby options when map status changes. 2025-11-03 14:03:35 +02:00
Matthias Mailänder
2cb1e5f8c5 Allow unsigned builds in private forks. 2025-10-31 19:09:21 +01:00
Matthias Mailänder
79288c6ee3 Move code signing into production. 2025-10-31 19:09:21 +01:00
Gustas
93dc94c1a0 Add RemoveAlwaysVisible to update path 2025-10-31 19:43:05 +02:00
dependabot[bot]
79765004fc Bump signpath/github-action-submit-signing-request from 1 to 2
Bumps [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) from 1 to 2.
- [Release notes](https://github.com/signpath/github-action-submit-signing-request/releases)
- [Commits](https://github.com/signpath/github-action-submit-signing-request/compare/v1...v2)

---
updated-dependencies:
- dependency-name: signpath/github-action-submit-signing-request
  dependency-version: '2'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-31 16:25:48 +01:00
Gustas
08d07eeb06 Fix bot traits crashing on missing ResourceMapBotModule 2025-10-31 14:01:18 +02:00
dependabot[bot]
4cd1b4e208 Bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-31 13:37:18 +02:00
dnqbob
bb2a44705f Fix McvExpansionManagerBotModule on CheckCurrentLocation mode. 2025-10-31 13:19:05 +02:00
Robert Nordan
70243850b1 Make WavReader handle buggy Wav Files more robustly
This change makes WavReader correctly read the chunk size of a
chunk as an unsigned int, and cleans up the inconsistent logic around
checking chunk sizes. It also attempts to handle buggy/chopped off
Wav files where the reported data chunk size is larger than the actual file.

Also adds Robert Nordan to AUTHORS file.
2025-10-21 20:15:20 +03:00
Gustas
dff7cd3bbf Fix aircraft failing to reload 2025-10-21 10:05:02 +01:00
dnqbob
24a09a94fe Airstate danger avoidance: check the aircraft nearest target actor, instead of target actor 2025-10-20 16:22:31 +03:00
N.N
8144c5684e fix HasAdequateRefineryCount blocking unit building 2025-10-20 15:47:52 +03:00
N.N
315b26e85c fix harkonnen logo 2025-10-18 18:40:12 +03:00
dnqbob
ea775ab824 Correct and add comments for expansion related 2025-10-16 22:24:18 +03:00
dnqbob
1c693a2c4b Fix bot MCV not persisting when failed to deploy. 2025-10-16 22:24:18 +03:00
N.N
41123d0520 Add damaged VFX for D2k buildings 2025-10-16 21:50:44 +03:00
Gustas
d26b9a10ed Add WithDamageOverlayPropertyRename update rule
Also make trees, ships and vehicles burn a random duration
2025-10-16 21:50:44 +03:00
Gustas
58088ecabe Add a HasChild helper command 2025-10-16 21:50:44 +03:00
Gustas
a2a7b30727 WithDamageOverlay refactor
Rename IdleSequence to StartSequence
Allow WithDamageOverlay to loop multiple times.

- unhardcode StartSequence and EndSequence
- add inital delay to WithDamageOverlay
- make WithDamageOverlay conditional

.
2025-10-16 21:50:44 +03:00
JovialFeline
3dcbdaa56e Add grace period & polish to Situation Critical 2025-10-16 11:45:39 +03:00
dnqbob
0fb42b39d9 Fix Airstate indice check:
1. the indice square should be the inscribed square of DangerScanRadius circle.

2. Use Map Bounds instead of Map Size
2025-10-15 20:58:57 +03:00
N.N
dc2fa932ea fix typo in encyclopedia 2025-10-15 20:14:52 +03:00
Vapre
d08d03274b Avoid occasional 'busy wait' for next render time in main loop. #18491 2025-10-15 16:49:00 +03:00
JovialFeline
b6495113c4 Fix extra prereq for D2k pad/MCV in missions 2025-10-15 10:50:38 +03:00
JovialFeline
de78f4130b Add Proving Grounds (RA) 2025-10-15 10:27:08 +03:00
N.N
a16486fbfc add ResourceMapBotModule 2025-10-14 14:28:31 +03:00
N.N
9b94796a32 Update Encyclopedia 2025-10-12 17:58:19 +03:00
N.N
76b8a949e1 balance patch 2025 2025-10-12 17:58:19 +03:00
dnqbob
b0514bdb87 Bot protection squad: try attack long range unit even not visible 2025-10-12 17:10:01 +03:00
dnqbob
961702d818 Bot mcv expansion: 1. Add ResourceMapBotModule. 2. Add McvExpansionManagerBotModule. 3. Give BaseBuilderBotModule better control over refinery. -- allow bot sell refinery too close. -- allow bot sell refinery far away from resource. (requires ResourceMapBotModule) -- allow bot recieve refinery construct requirement. 4. Give BaseBuilderBotModule control of expansion timing. (requires McvExpansionManagerBotModule) 5. Remove unused VehiclesFactoryTypes and useless BarracksTypes (now all use ProductionTypes). 6. Give HarvesterBotModule ability of order harvesters in low resource area to high resource area. 2025-10-12 17:10:01 +03:00
Ashley Newson
7a2e25af86 Fix editor grid overlay height coloring calculation
The change fixes two issues with the map editor's height-based grid
overlay color calculation:

- If the height exceeded the end of the color scale, the engine would
  crash due to attempting an out of bounds read. Now, the calculation
  caps out to using the last color in the scale.
- The calculation assumed the height step was always 512, even for
  RectangularIsometric maps. Now, the (halved) TileScale of the map's
  grid type is used.
2025-10-12 14:47:02 +03:00
Gustas
f71684a120 Fix CameraOvalMover file name 2025-10-06 17:52:43 +02:00
Matthias Mailänder
361ec67ad9 Setup SignPath for Microsoft Authenticode. 2025-10-03 13:36:50 +01:00
Gustas
48a5b5b9bc Code style 2025-09-27 19:58:06 +02:00
Gustas
2804d103ae Add smooth shellmap movement 2025-09-27 19:58:06 +02:00
Matthias Mailänder
59ecb8d8a3 Move private methods to the bottom. 2025-09-22 15:23:24 +03:00
Matthias Mailänder
148cd911d5 Don't attempt to shutdown master ping in skirmish. 2025-09-22 15:23:24 +03:00
Matthias Mailänder
2c86411eb4 Don't ping the master server in skirmish. 2025-09-22 15:23:24 +03:00
Matthias Mailänder
2d755b010c Log exceptions for dedicated servers. 2025-09-22 12:42:37 +03:00
dnqbob
619c5a13a1 Add PowerDownBotModule to RA 2025-09-15 13:09:01 +03:00
dnqbob
f7e1b4259c Add PowerDownBotModule 2025-09-15 13:09:01 +03:00
Matthias Mailänder
82f35bbe1c Don't crash when a client that was dropped sends another order. 2025-09-14 18:59:24 +02:00
Gustas
b4c767ce48 Remove unnecessary branching from ToCPos 2025-09-14 12:32:49 +01:00
Gustas
fb5aae827a Fix MPos projection 2025-09-14 12:32:49 +01:00
Paul Chote
831cce2f9c Refer to editor marker tiles by their color. 2025-09-13 13:44:31 +03:00
Paul Chote
994eddd46f Remove default hotkey binding for relationship colors. 2025-09-12 12:47:07 +03:00
Paul Chote
85637d580e Fix "Toggle player relationship colors" hotkey name. 2025-09-12 12:47:07 +03:00
Gustas
992305a1c0 Make ModifyCash virtual 2025-09-11 23:06:32 +01:00
Gustas
0da4766300 CashTrickler can be cloaked 2025-09-11 23:06:32 +01:00
Gustas
fe2c227270 Don't show resource cursor when painting 2025-09-11 20:55:19 +01:00
Gustas
955634a520 Don't update resource layer more than needed 2025-09-11 20:55:19 +01:00
Gustas
83bbb357d1 Remove redundant data 2025-09-11 20:55:19 +01:00
Gustas
bd590dfcf4 Add a preview to editor marker brush 2025-09-11 18:19:05 +01:00
Gustas
d4a08e5b58 Switch to count naming 2025-09-11 18:19:05 +01:00
Gustas
9da921756d Add replace init function 2025-09-11 18:19:05 +01:00
Gustas
cccd40ca13 Misc improvements 2025-09-11 18:19:05 +01:00
Gustas
b5b44c048d Significantly improve actor placement and removal speeds 2025-09-11 15:34:58 +01:00
Gustas
7d4a590240 Use consts 2025-09-11 15:34:58 +01:00
Gustas
082f369fe3 Add smooth minimap to world movement 2025-09-11 14:15:37 +01:00
Gustas
1a8a3eca4d Add smooth scrolling 2025-09-11 06:49:52 +01:00
Gustas
270c37764e Reduce MersenneTwister memory allocation 2025-09-11 06:39:55 +01:00
Gustas
a8b13d012b Remove AlwaysVisible 2025-09-11 06:17:04 +01:00
JovialFeline
0230b79269 Fix atreides-05 frigate reinforcement 2025-09-09 10:55:25 +03:00
dnqbob
e126c59e94 Protection: Restore BackoffTicks if target visible 2025-09-08 17:29:43 +03:00
dnqbob
d555baa0c2 Protection squad use idle unit instead of unit around conyard 2025-09-08 17:29:43 +03:00
darkademic
3f73e7a1fb Don't count refunds as income and deduct them from expenses. 2025-09-08 16:18:08 +03:00
dependabot[bot]
4823c3365f Bump actions/setup-dotnet from 4 to 5
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4 to 5.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-05 09:35:15 +02:00
dnqbob
7d27df7053 Avoid AI add other squad unit to Rush squad 2025-09-02 18:12:16 +03:00
Ashley Newson
4c9ae3678b Add watercliff support to ExperimentalMapGenerator
This commit adds watercliffs to generated maps in the RA mod, based on
the path partitioning logic added for the D2K map generator.

Watercliffs are not available in CnC and unofficial mods, so their use
in the ExperimentalMapGenerator configuration is optional.
2025-08-23 17:10:02 +03:00
Ashley Newson
2cf6ac6383 Optimize Terraformer.PartitionPath
The internal Vote method (called in a tight loop) was inefficient due
to expensive iteration and data structures, which meant that
attempting to generate larger maps could take days.

It is changed to leverage some shared pre-computation to reduce its
runtime complexity and replaces the use of Dictionary with a simple
reused array.

The semantics of the Vote method in isolation are slightly modified,
but this should not impact the quality of generated maps, especially
with current usage.
2025-08-23 16:18:27 +03:00
Ashley Newson
7cd7b32042 Adjust point sequences for RA beach segments
The TEMPERAT and SNOW tilesets' MultiBrush segment definitions used
inconsistent conventions for vertical and horizontal beaches. This
meant that:

- Tiling was biased to produce horizontally wider bodies of water.
- Vertical beaches did not line up with watercliffs when connecting.

This change redefines the point sequences to fix these issues.
2025-08-22 19:28:23 +03:00
Ashley Newson
c3b22c9102 Remove Inner types from various MultiBrush segments
Inner types in MultiBrush segment definitions can usually be derived
automatically from the start and end types. This is especially
important for segments with different starts and ends, where the Inner
type could be either one (e.g. beach to watercliff changers). Inner
types are now only needed in two cases:

- The Inner type doesn't match either the start or end. For example,
  road diagonals.
- The Inner type should only match one of the start or the end. For
  example, cliffs which end into clear tiles.

This commit cleans up the unneeded or unwanted Inner type definitions
in the base mods.
2025-08-22 19:20:22 +03:00
Gustas
1124c6e6c2 Fix FireProjectilesOnDeath not working for aircraft husks 2025-08-21 11:50:17 +02:00
Matthias Mailänder
28b02f662f Limit tiling path dropdown height. 2025-08-16 15:38:33 +03:00
dependabot[bot]
792231fbb9 Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 10:22:31 +02:00
Matthias Mailänder
f8c8c36307 Don't rebuild on Markdown updates. 2025-08-10 12:10:25 +03:00
Matthias Mailänder
885274e3d7 Update chat links. 2025-08-10 12:10:25 +03:00
Rampoina
8a0197c88b Make AttackMoveInfo public 2025-07-28 23:56:08 +03:00
Gustas
b2215c6dd1 Only allow using path tool when panel is enabled 2025-07-28 21:44:58 +02:00
Gustas
42181d77a5 Allow tools to disable themselves 2025-07-28 21:44:58 +02:00
Gustas
f8921226b8 Limit path outer type options 2025-07-28 21:44:58 +02:00
JovialFeline
c2c56c6374 allies-08ab: Remove tech center as bomber target 2025-07-28 20:10:40 +02:00
Vapre
f325e9c7b9 Add server setting 'AdvertiseOnLocalNetwork' to be able to disable BeaconLib. 2025-07-27 21:45:22 +02:00
Gustas
c19a7d338b Allow null sequence arrays 2025-07-27 21:34:04 +02:00
Gustas
7206a33d0d Make rules lint utility cleaner 2025-07-27 21:13:21 +02:00
Gustas
c27d6dd1b1 Test mod content 2025-07-27 21:06:25 +02:00
Gustas
28a8f3a01f Window title is optional 2025-07-27 21:06:25 +02:00
Gustas
32750700a4 Lint ModPackage 2025-07-27 21:06:25 +02:00
Gustas
be971c4a34 Mode doesn't need to have terrain defined 2025-07-27 21:06:25 +02:00
Paul Chote
d2780382bc Remove redundant placeholder world definition.
Co-Authored-By: Gustas Kažukauskas <37534529+punkpun@users.noreply.github.com>
2025-07-27 21:06:25 +02:00
Gustas
a66feafa3f System actors can have no traits 2025-07-27 21:06:25 +02:00
Gustas
f0ec8105b3 Players and world don't need to be visible 2025-07-27 21:06:25 +02:00
Gustas
15824ef70f Fix offset not being dynamic on each activation 2025-07-27 20:45:52 +02:00
N.N
c2ca0a1545 Added ResetOnDamaged to FloatEmitter 2025-07-27 20:45:52 +02:00
Gustas
5aa7ee43db Enforce stricter resource API 2025-07-27 19:28:07 +02:00
Gustas
7779d0c27c Add smart deploy 2025-07-27 19:06:12 +02:00
Gustas
288d839456 Stream actor layer renderables 2025-07-27 15:36:11 +02:00
Gustas
cc9fa4b3ef Stream previews, reduce casting 2025-07-27 15:36:11 +02:00
Gustas
bb0a39ba6c Don't allocate priorities 2025-07-27 15:36:11 +02:00
atlimit8
c56d2c1bbe fix possible underflow in BlowfishKeyProvider.LenBigNum() 2025-07-27 14:56:33 +02:00
Gustas
8ce9e01c1c Remove redundant checks 2025-07-27 14:56:57 +03:00
michaeldgg2
3f0126eef4 Fix the check in AllowResourceAt() method of the ResourceLayer class
This fixes incorrect check in AllowResourceAt(), which previously looked at terrain type of the given cell *before* resource was created. Hence the check did not work as expected.
2025-07-26 21:40:49 +03:00
Matthias Mailänder
5f51dec9f9 Revert "Make creationrallypoints public. Required for custom rallypoint functionality"
This reverts commit 0c5e998e31.
2025-07-26 17:01:15 +02:00
Ivan Perez
0c5e998e31 Make creationrallypoints public. Required for custom rallypoint functionality 2025-07-25 17:26:15 +02:00
Damian Ochramowicz
07d3ab2613 Removed msbuild reference from Makefile 2025-07-24 22:09:48 +03:00
Ivan Perez
de49ad6a79 Notify player when an unbuildable item is cancelled. 2025-07-24 22:02:41 +03:00
Matthias Mailänder
88043e297b Allow IDumpSheetsTerrainInfo to be implemented by thirdparties. 2025-07-24 18:56:14 +03:00
Matthias Mailänder
f416e123f0 Replace custom JSON with MiniYAML. 2025-07-24 16:26:32 +03:00
Matthias Mailänder
ec206dc359 Fix serialisation from dictionary. 2025-07-24 16:26:32 +03:00
Ivan Perez
25be2149c1 Fixed bug where infinite production queue does not notify on ready 2025-07-24 12:29:16 +03:00
Paul Chote
44c6747dec Replace ITileCache with IDumpSheetsTerrainInfo. 2025-07-21 22:10:30 +03:00
Matthias Mailänder
5d8e35088f Avoid one letter abbreviations for variables. 2025-07-21 15:25:24 +03:00
Matthias Mailänder
e61db3faea Don't hardcode implementations in interfaces. 2025-07-21 15:25:24 +03:00
darkademic
0fac8d0d31 Add script tags to map editor actor properties. 2025-07-21 14:57:51 +03:00
Matthias Mailänder
53f959e4bf Use name of for exceptions. 2025-07-21 14:11:42 +03:00
Vapre
a4966bc299 TraitContainer, use span to iterate actors and traits. 2025-07-17 18:58:17 +03:00
Matthias Mailänder
fa5a347d2e Drop dependency on Newtonsoft.Json 2025-07-17 18:50:11 +03:00
Matthias Mailänder
3ca951930e Fix AppImage build. 2025-07-17 18:47:48 +03:00
Ashley Newson
5f407211ac Add D2K random map generator
Adds a random map generator tailored for the Dune 2000 mod,
D2kMapGenerator.

This map generator has some limitations compared to the RA/CnC
map generator (ExperimentalMapGenerator):

- Symmetry is far less accurate for non-trivial rotations.
- No circular map shapes.
- Unplayable regions are not obstructed and no symmetry corrections
  are applied. This is partly due to mod limitations (e.g., no
  decorative actors), but I've also considered it less important.
- Somewhat higher map generation failure rates, due to more extreme
  play-space requirements.

In order to support the map generator, some additional features are
added to common map generation utilties:

- A "FromTemplates" MultiBrush collection shorthand.
- Path partitioning logic in Terraformer, use to split paths into
  multiple TilingPaths with different segment types.
2025-07-17 18:43:54 +03:00
Ashley Newson
484784bd4d Refine lobby map generation logic
In early iterations of the lobby map generation logic, a for loop was
used to retry map generation with alternative seeds in case it
failed. This seed randomization logic didn't exist in the final merged
version, but the for loop wasn't cleanup up, and ends up repeating any
map generation failures 5 times deterministically.

Additionally, the logic caught Exception instead of the more specific
MapGenerationException, which is the only exception that is expected
to arise in healthy builds/configurations (due to bad luck rather than
bugs).

This change:

- removes the for loop;

- only catches MapGenerationException;

- adjusts the user-visible failure message to reflect that trying
  again (without adjusting settings) is a valid course of action (now
  that it's not done internally);
2025-07-17 10:42:27 +03:00
darkademic
77baa7a35f Implement ScrollableLineGraphWidget. 2025-07-17 10:34:35 +03:00
darkademic
3a5358ad7c Copy LineGraphWidget. 2025-07-17 10:34:35 +03:00
JovialFeline
74ead504ec Use support sequences for scripted powers 2025-07-14 22:04:08 +03:00
michaeldgg2
8833e95f21 PBOG: remove the resource check and let IsCellBuildable() extension method handle it 2025-07-14 21:52:41 +03:00
michaeldgg2
d657a72107 Remove BuildingInfo.AllowPlacementOnResources and use TerrainTypes to check whether a building can be placed on particular cell 2025-07-14 21:52:41 +03:00
michaeldgg2
abc3a79330 ResourceLayer: use BuildingInfo.TerrainTypes to check, if a resource can be placed underneath the building 2025-07-14 21:52:41 +03:00
Ashley Newson
25571df2b6 Refactor ExperimentalMapGenerator into reusable methods
This change lifts large portions of ExperimentalMapGenerator's logic
into a new "Terraformer" class with highly documented methods that can
theoretically be used by alternative map generator classes. Some
additional refactoring may occur in future, subject to the practical
needs of additional map generators. ClearMapGenerator is also
simplified.

This is not a pure refactor and contains some algorithmic and
behavioral changes, as well as few minor bug fixes. Notably:

- The logic for obstructing unreachable water has been somewhat
  replaced.

- In CnC, where water is already unplayable, EMG no longer obstructs
  water. (RA still does, as water is playable there.)

- Mountain (cliff) generation is now somewhat more effective. This
  increases the number of cliffs seen on many presets. Settings should
  no longer use "Mountains: 1000".

- PlayableSpace logic has been consolidated into Terraformer.

- PlayableSpace.Playability no longer exists as PartiallyPlayable
  became redundant. Its uses have been replaced with a simple
  boolean. Consequently, some defunct code and configuration has been
  removed.

- Adjust MultiBrush replaceability contract painting behavior to be
  more intuitive.

- Fix off-by-one error in map bounds computation.

- Fix some usages of mixed up MersenneTwisters.
2025-07-14 20:09:10 +03:00
Matthias Mailänder
e4f289921c Drop dependency on Newtonsoft.Json 2025-07-14 16:09:36 +03:00
toasted-dev
444e849241 Changelog: Fix typos.
Updated the Chinook Transport helicopter's description to correct the spelling of "detachment".

Corrected the description of the mutated lifeform by changing "expose" to "exposed" for grammatical accuracy.
2025-07-14 15:50:33 +03:00
Vapre
2e3a30c9ca Fix error IDE0028: Collection initialization can be simplified. 2025-07-07 20:33:59 +03:00
N.N
18870cbb03 D2k Starport
- OG fly path and Frigate behaviour
- BulkProductionQueue
- ProductionStarport trait
- OG palette widget behavior for Bulk queue
- Purchase button for each BulkProduction
- Delivery timer for each BulkProduction
2025-06-30 20:19:21 +03:00
N.N
fcae6a26d1 D2k Multibrush 2025-06-30 19:44:36 +03:00
Vapre
aa05081636 DensePathGraph, only traverse layers if enabled for locomotor. 2025-06-30 18:36:44 +03:00
Михаил Васильев
368ac277d0 Fix typo in NOD08a mission description 2025-06-17 15:36:54 +03:00
Михаил Васильев
631cbb1421 Fix 07a mission description 2025-06-17 15:23:35 +03:00
Gustas
6fe039f62e Add cursors to dump sheets command 2025-06-14 16:43:30 +02:00
Gustas
f5ecb9ad99 Add tileset to dump sheets 2025-06-14 16:43:30 +02:00
Gustas
84378969c3 Make palettes and tilesets optional 2025-06-14 16:43:30 +02:00
Gustas
4c04d4fa30 Add death types 2025-06-14 16:12:36 +02:00
Gustas
abec71c42d Use common util and unseal class 2025-06-14 16:12:36 +02:00
Gustas
03c24da611 Give XP to minelayer 2025-06-14 13:22:49 +02:00
Gustas
4be3add30a Dispose of old update rules 2025-06-14 11:58:49 +02:00
Gustas
c27511dd66 Add colors to verbose utilities 2025-06-14 11:56:16 +02:00
Gustas
12cb5997f0 Print the list of warnings 2025-06-14 11:56:16 +02:00
Ashley Newson
ad19506836 Fix TilingPath bugs affecting unofficial mods
TilingPath had some bugs when working with single-point segments,
which exist outside of the official mods. This change fixes a few
problems:

- Avoid unbounded length zero-cost, zero-progression loops that could
  be formed by an arbitrary number of single-point corners. These
  could pop up arbitrarily in the middle of paths.
- Avoid zero-progression loops short-circuiting looped paths.
- Fix missing end type matching during back-tracing stage.
- Enhance lint check to ensure starts/ends have a valid directions.
- Remove some dead code.
2025-06-13 18:52:05 +03:00
Matthias Mailänder
0b13ee3e49 Update to lowest supported Windows server image. 2025-06-12 20:04:35 +03:00
Ashley Newson
c92a6be4af Respect MultiBrush weights when path tiling
Allows biasing path tiling towards or away from using certain
MultiBrushes using their existing weight property. Note that the
probability of brush selection depends on multiple factors, not just
their direct weight.
2025-06-08 16:29:03 +03:00
Ashley Newson
561466d822 Fix Path Tiling tool rally confusion and crash
If the path being planned by the Path Tiling tool overlaps itself such
that a later rally intersects an earlier line, attempting to interact
with this later rally caused the UI to instead select the rally
associated with the earlier line. This at best leads to user confusion
and at worst leads to a crash if they then drag the rally over another
rally.

This commit fixes the UI logic to correctly identify the rally being
clicked on.
2025-06-08 16:14:50 +03:00
Ashley Newson
c6ed381085 Remove empty Path Tiling Tool dropdown tooltips
I'd accidentally copy-pasted the wrong item template in the original
TilingPathToolLogic implementation. This change uses the normal
tooltipless LABEL_DROPDOWN_TEMPLATE.
2025-06-08 16:03:05 +03:00
Bryan Quigley
aae2ec109b Add 2 obstacles to europe to help AI
AI likes to make a naval building in the top right tiny water area.
This PR adds 2 rocks to that area blocking any building so the AI
is not going to waste time on it.
2025-06-08 15:58:18 +03:00
Ashley Newson
9c8470d18e Preview clipboard when copy-pasting in the map editor 2025-05-31 10:39:39 +03:00
Ashley Newson
c4332c808b Fix copy-paste editor crash for RectangularIsometric
EditorBlit was incorrectly using CellRegion.Contains instead of
CellRegion.CellCoords.Contains (i.e. CellCoordsRegion.Contains). This
resulted in a crash upon attempting to paste within the editor for
RectangularIsometric maps (such as in TS).

This change updates the code to use the correct region type and fixes
the crash.
2025-05-31 10:31:23 +03:00
JovialFeline
1cdc823bca Add The Tiberium Strain mission (CnC) 2025-05-30 16:10:51 +03:00
Ashley Newson
5c7188be81 Avoid denoising unbiased resources in map generator
Resource placement is usually biased by mpspawns or resources spawns
(mines or blossom trees), which feed into a mutilplier for the
resource noise. Previously, all cells outside of the circle of
influence of a bias source got a multiplier of zero, essentially
giving them no noise pattern. This could lead to ugly horizontal
streaks of resources for which the layout was determined only by the
internals of PriorityArray.

This change fixes this by ensuring the noise multiplier has a
baseline value of 1.
2025-05-30 15:31:19 +03:00
Ashley Newson
2a0e787ae7 Check for and fix non-unit MultiBrush segment steps
TilingPath's algorithms are designed on the assumption that MultiBrush
segments use unit steps in their point sequences. Non-unit
sequences can result in incorrect or suboptimal tilings.

This change ensures point sequences are validated for unit steps, and
fixes a non-compliant cliff tile across all tilesets.
2025-05-30 15:22:53 +03:00
Ashley Newson
7538943f59 Convert Direction.cs to enums and extensions
- Converts OpenRA.Mods.Common/MapGenerator/Direction.cs over to using
  Direction and DirectionMask enums
- Moves direction-related utilities into Exts classes.
- Simplifies some direction utility logic.

Only very small numerical precision behavioral changes.
2025-05-30 15:15:15 +03:00
JovialFeline
99253702c5 Add IsDead checks to Fall of Greece 1 2025-05-20 22:25:52 +03:00
Ashley Newson
5c09d5d828 Support PickAny templates in MultiBrushes
Allows PickAny templates to be used in MultiBrushes, which can be
painted with random selection. This avoids the need to define various
separate MultiBrushes for each tile combination.

Note that all combinations count as part of a single MultiBrush, which
may influence selection weighting.

This MultiBrush feature is not presently used in OpenRA's official
mods, but is useful for unofficial mods using more general PickAny
templates.
2025-05-19 22:18:12 +03:00
Ashley Newson
daf36769d6 Add additional MultiBrush segments for RA and CnC
Adds MultiBrush definitions for River (RA and CnC) and WaterCliff (RA
only), allowing them to be tiled with the Path Tiling tool.

A small enhancement to the MultiBrush and TilingPathTool code is added
to allow the inner type to be derived from start and end types. This
allows segments that interface between types (such as WaterCliff to
Beach) to be usable when tiling either WaterCliffs or Beach. Whilst
this makes the inner types of many of the existing MultiBrushes
redundant, no simplification is performed as part of this change.
2025-05-19 22:01:25 +03:00
Михаил Васильев
ba444de014 Fix link to Lua API in README.md 2025-05-19 21:33:04 +03:00
Paul Chote
6a67906823 Improve --check-yaml robustness against bogus maps. 2025-05-19 21:30:07 +03:00
Paul Chote
681edb1879 Fix button order in save browser. 2025-05-15 23:14:48 +03:00
Paul Chote
b7d929de5a Precache generated map for the host. 2025-05-15 23:08:13 +03:00
Paul Chote
6ffd786d5b Add Generate Map tab to the lobby map chooser. 2025-05-15 23:08:13 +03:00
Paul Chote
979483b63c Add MapClassification.Generated.
Generated maps are created at runtime using a
mod-defined trait. They are embedded directly
into replays and save games as they exist in
memory only, and disappear after the game exits.
2025-05-15 23:08:13 +03:00
Paul Chote
6be947bbf0 Support serializing/loading ReadWriteZipFile as base64 string. 2025-05-15 23:08:13 +03:00
Paul Chote
5292919917 Move Boundary Shape option before Players. 2025-05-15 23:08:13 +03:00
Paul Chote
497b5e30ad Extract a MapSizeLabel helper method. 2025-05-15 23:08:13 +03:00
Paul Chote
4f1b17b832 Allow MapGeneratorSettings to be initialized from MapGenerationArgs. 2025-05-15 23:08:13 +03:00
Ashley Newson
ad08045e5f Add path auto-tiling tool to map editor
Exposes the TilingPath code used by the map generator as a map editor
tool to help automate the process of tiling sequences of templates
together.

No additional template stitching definitions are added as part of this
commit. As such, the tool currently only supports the same auto tiling
as the map generator does. This includes:

- RA and CnC support.
- Beaches, land cliffs, and roads.

Support for additional mods and templates can be added in follow-ups
by adding the necessary MultiBrush definitions.

Related changes included in this commit to support this work include:

- Add support for sparse EditorBlits.
- Add support for MultiBrush to EditorBlitSource conversion.
- Add support for EditorBlitSource previewing.
- Adjust the "Road" types used in segmented MultiBrush definitions.
2025-05-15 22:41:43 +03:00
michaeldgg2
d3d949176d ProductionTabsWidget: fix updating tabs when new production building is built 2025-05-11 22:20:04 +03:00
michaeldgg2
89d906a613 ProductionTabsWidget: fix updating internal cache for production queue 2025-05-11 22:20:04 +03:00
Михаил Васильев
8f3ca30840 Fix typo in soviet 04b mission description 2025-05-11 21:59:23 +03:00
N.N
c5e4545ecd Add muzzle effect into the sequences
- remove Armament@muzzle
- remove HMG_muzzle, HMGo_muzzle
2025-05-10 19:12:13 +03:00
JovialFeline
894d08adf0 Add missing campaign bots 2025-05-10 16:31:19 +03:00
RoosterDragon
92b3dde789 When pathing, prefer optimal paths over short distances.
The default path weight of 125% allows paths up to 25% longer than the optimal path to be returned, which improves the performance of path searches.

Over short distances, players are likely to be sensitive to suboptimal paths this can produce, so instead use a weight of 100% over short distances to ensure these paths are optimal. As the search area is limited, the additional performance impact is also capped. The hierarchical path finder already has a speculative check for short paths within a 20 cell area (twice the grid size of 10 cells), which allows it to skip a hierarchical search if a path within that area can be found. We can piggy back on this short path logic and use a weight of 100%.

Over longer distances, players are less likely to notice the suboptimal paths, and the performance benefit is more noticeable, so continue to use the 125% weight in these scenarios.
2025-05-10 16:18:52 +03:00
RoosterDragon
7fb88b974e Expose HeuristicWeightPercentage on PathFinder trait.
Maintain the default weight of 125%, but allow this to be modified via config.
2025-05-10 16:18:52 +03:00
Matthias Mailänder
798593bede Enable Dependabot for GitHub Actions. 2025-05-09 17:54:51 +03:00
Paul Chote
3e06749006 Rewrite fluent lint check. 2025-05-09 16:28:04 +03:00
Paul Chote
9ffa7be606 Remove mod-specific keys from common fluent. 2025-05-09 16:28:04 +03:00
Paul Chote
0313d04970 Polish map generator settings. 2025-05-05 20:50:55 +03:00
michaeldgg2
ad13658076 Make WithMuzzleOverlay and Info class public 2025-05-01 20:42:57 +03:00
atlimit8
0b110a0423 ping when non-structures are attacked 2025-04-30 22:22:53 +03:00
atlimit8
34a92f3406 Remove BaseAttackNotifier trait 2025-04-30 22:22:53 +03:00
atlimit8
b792e3eecf add update rule for replacing BaseAttackNotifier 2025-04-30 22:22:53 +03:00
atlimit8
e27aa0d379 replace use of BaseAttackNotifier trait w/ DamageNotifier 2025-04-30 22:22:53 +03:00
atlimit8
2b1121b970 add DamageNotifier 2025-04-30 22:22:53 +03:00
michaeldgg2
5537a194fa DockClientManager: enter cursor per DockHost's DockType 2025-04-30 21:46:56 +03:00
Paul Chote
56c615d890 Fix invalid CachedTransform uses of MapPreview. 2025-04-30 14:06:42 +03:00
Paul Chote
e8d710dee4 Prevent remote map searches overwriting local or generated state. 2025-04-30 13:21:26 +03:00
Ashley Newson
4a4217c3b3 Fix resource calculation in map generator
The editor's resource value calculation logic was fixed in #21820 to
match actual gameplay. The map generator logic originally copied the
editor logic from EditorResourceLayer rather than ResourceLayer and
associated game logic. As such, it inherited incorrect calculations.

This change updates the map generator logic to match the now
consistent logic in both ResourceLayer classes. Map generator resource
settings are also adjusted -25% to continue roughly matching the
previous resource output.
2025-04-30 12:51:14 +03:00
Paul Chote
965a490b70 Dynamically populate map chooser tabs. 2025-04-29 12:17:27 +03:00
Paul Chote
5b7a714ac2 Replace Tileset id with Environment name. 2025-04-28 15:41:54 +03:00
Paul Chote
90013bc6d3 Migrate tileset names to Fluent. 2025-04-28 15:41:54 +03:00
Paul Chote
246a1b9fec Move map installed check to LobbyLogic. 2025-04-28 13:08:38 +03:00
Paul Chote
e5ffed2a4f Require map generators to specify valid tilesets. 2025-04-28 12:54:08 +03:00
Gustas
c41a1302b6 Fix explored map being rendered incorrectly on first frame 2025-04-26 20:45:42 +01:00
Gustas
bad58b5821 Don't fade in shellmaps 2025-04-26 20:45:42 +01:00
Gustas
4c551cef00 Add more customization to fade, shorten fade in 2025-04-26 20:45:42 +01:00
Gustas
d3b7688a7d Fix fading in not working 2025-04-26 20:45:42 +01:00
Paul Chote
9c78c45faf Split IMapGeneratorInfo into core backend and editor frontend. 2025-04-25 20:51:33 +03:00
Paul Chote
ce41ec3fc3 Skip dropdowns that have no valid options. 2025-04-25 15:21:29 +03:00
Ashley Newson
07ef98a578 Improve map-generated roads for awkward symmetries
For awkward symmetries (where there is mirroring and/or non-grid
aligned rotations), road planning now runs an extra filtering step so
that only sufficiently symmetric road plans make it to the tiling
stage.

Road planning also now extends out-of-map-bounds to more accurately
accommodate awkward rotations, improving the symmetry of pre-filtered
road plans.

A previous filter, which removes nearly horizontal or vertical roads
is now removed, as this is not fair to all rotations. This step is
less importance since the introduction of relaxed tiling.

Note that roads may be dropped entirely if the plans come out highly
asymmetric. This is not uncommon for awkward rotations. This is
preferred over presenting a map with asymmetric roads.
2025-04-25 15:09:27 +03:00
Gustas
c541a0e0dc Improve HD sheet creation 2025-04-19 16:34:13 +01:00
Ashley Newson
f1573d4b0c Add map-generated beaches to more CnC tilesets
CnC map generation previously only supported maps with water/beaches
for the DESERT tileset, as it is the only tileset with a complete set
of beach tiles. This change adds support for the other tilesets
(TEMPERAT, SNOW, WINTER) by adding fake beach templates constructed
from actors and other templates' tiles that look reasonable in both
original and remastered graphics.

This means all CnC tilesets have feature parity in the map generator.
2025-04-19 15:45:49 +03:00
Paul Chote
ede12610a8 Change Map.MapSize from int2 to Size. 2025-04-19 14:06:32 +03:00
Paul Chote
2fbbff2860 Change Size.ToString output to support FieldLoader. 2025-04-19 14:06:32 +03:00
Gustas
cd9001e7b9 Don't allocate empty sheets 2025-04-19 11:25:17 +01:00
Gustas
87cff1f6f1 Localise DummyBot 2025-04-19 11:11:53 +01:00
Gustas
39aec39364 Initialise VertexBuffers immediately 2025-04-19 10:40:31 +01:00
Ashley Newson
a2096b6768 Use MultiBrush as the input and output of TilingPath
This generalizes TilingPath to accept MultiBrushes instead of templates
and makes the output also a MultiBrush. As a result:

- Composite or faked templates can be used in tiling. This will (in a
  later change) be used to support CnC beaches outside of the desert
  tileset.
- The tiling result can be previewed without committing it onto the map,
  making it more viable for adoption as an editor tool.
- The full shape of the tiling result can be inspected without messily
  reading it back off of the map, simplifying some map generator logic.
- Separates map generation-specific segment definitions from the core
  template definitions.
- As MultiBrushes can be many-to-one with templates, there is no need to
  have a one-to-many template-segment relationship. Multi-segment
  templates, such as roads, now use multiple brushes instead. This
  simplifies the logic of segment consumers.

Previously, MultiBrushes which contained no offset information in their
definitions were automatically aligned such that the first tile was
under 0,0. This is no longer the case. This was previously done for use
in MultiBrush.PaintArea to improve tile packing success, but the
alignment is now handled automatically by PaintArea instead.

Due to some impure refactoring which changes the randomization of
tiling choices, map generation results may change.
2025-04-18 18:19:11 +03:00
Paul Chote
cf11c6633e Simplify Player and Symmetry options.
Player now specifies the total player count,
and the Symmetry options adjust to show only
valid options.
2025-04-18 13:49:20 +03:00
Paul Chote
31154e3d2d Remove derived properties from Parameters. 2025-04-18 13:49:20 +03:00
michaeldgg2
e10cdc6690 Docking: fix aircraft clients trying to dock with different host than expected 2025-04-18 11:19:27 +03:00
Ashley Newson
f09959ecba Support relaxed end points in TilingPath
Whilst TilingPath has always allowed deviating from the path points
between the start and end point, it didn't allow deviation from the
start or end points themselves. This change allows the end point to
deviate if the tiling would otherwise fail. The start point remains
strictly positioned, as before. Loop end points (which must necessarily
match the start) also remain strict.

The ExperimentalMapGenerator is modified to benefit from the relaxed
tiling constraints. As a result, failed map generation due to tiling
failures is much rarer, and will make some otherwise very inflexible
template categories viable for tiling.

Includes some impure refactoring around the treatment of start,
intermediate, and end segments. As such, this changes map generation
output, even for maps which already tiled perfectly.

A previous workaround used for CnC road templates, whereby additional
segments were defined for road ending templates, is now redundant and
cleaned up.
2025-04-18 11:13:23 +03:00
Paul Chote
b2acc653a0 Promote map generators to stand-alone editor tools. 2025-04-18 11:00:53 +03:00
Paul Chote
78f660124c Rewrite map generator settings UI logic. 2025-04-18 11:00:53 +03:00
RoosterDragon
8f46247dc9 MiniYaml.From* methods support deferred execution.
Previously, the MiniYaml.From* helpers such as FromStream would consume the entire input and then return a list of top-level nodes. Now, the input is processed using deferred execution and top-level nodes are yielded as they are resolved from the input.

The motivating use-case is MapCache, which currently manually buffers nodes before passing to MiniYaml.FromString in order to improve responsiveness when large payloads are processed. Now that MiniYaml.FromStream yields results back as they come in, we can switch to that without disadvantage.

The maintains the performance where map cache can update search results as each node comes in over the network rather than having to wait for the entire batch to be transferred. Now we can also remove the string buffer that captures each node, reducing memory pressure and simplifying the code.
2025-04-13 14:12:25 +03:00
RoosterDragon
5676d7be40 Prevent crash when using QuantizedFacings: 0 on BodyOrientation.
This override allows quantization to be disabled, and is used by the TS mod as the voxel based actors can be rotated to any orientation/facing. To prevent a divide by zero error for these actors, we need to return early from QuantizeFacing just like we do in QuantizeOrientation.

However, when QuantizedFacings is not explicitly set to zero, we still want to catch errors when artwork sequences have zero facings. The new check would cause such errors to be hidden. To prevent this add some exceptions with a detailed error message.
2025-04-13 14:00:22 +03:00
N.N
d459d3883d Add SoundFX for Ornibomb, Devastator Meltdown, Sabotuer 2025-04-06 12:28:08 +02:00
Gustas
af0dee3382 Fix incorrect tile definition 2025-04-06 12:19:26 +02:00
RoosterDragon
a4317fec57 MiniYaml applies removal during merging as well as inheritance.
Currently removal nodes in MiniYaml are only applied whilst resolving inherited nodes. When merging collections to override existing nodes the removals are not resolved, resulting in duplicate key errors if you remove a node to then add it back with different values.

Now, removal nodes are also resolved when merging, allowing nodes to be removed as overridden during merging just like they can be with inheritance.
2025-04-06 12:14:16 +02:00
RoosterDragon
3c2945c1bc Add OpenRA.Test to the solution build configuration.
When the solution is built, previously the Test and WindowsLauncher projects were excluded. Now only the WindowsLauncher project is excluded. This avoids needing to build the Test project separately after building the solution.
2025-04-06 00:04:42 +03:00
abcdefg30
d136ccc350 Don't allow actors to disguise themselves as themselves 2025-04-05 17:12:20 +03:00
Paul Chote
76cbd5582d Show FixedColorPalette options in the asset browser. 2025-04-04 17:39:17 +03:00
Paul Chote
d26d9e0b9c Remove redundant custom tiberium sprites. 2025-04-04 17:39:17 +03:00
Paul Chote
c187d21b95 Remove TD Jungle tileset.
This tileset was community content, and does not have remastered assets.
There is no realistic prospect to creating equivalent remastered assets,
which leaves removal in order to achieve parity between classic and
remastered modes.
2025-04-04 14:29:50 +03:00
RoosterDragon
36660b89e9 Silence IDE0290.
This rule recommends use of primary constructors. Apply the suggestions on simple POCOs, adjusting some to readonly/structs/records at the same time. For more complex classes, the use of primary constructors is more distracting than helpful, so silence the rule. IDEs can still offer fixes for using primary constructors, but it will not show up as a build issue.
2025-04-02 10:18:32 +03:00
Paul Chote
3de6a5fd5a Use the TerrainRenderer for rendering template-based bridges. 2025-04-02 10:08:39 +03:00
Paul Chote
737890d395 Fix PBOG offset not accounting for zoom. 2025-04-02 09:59:40 +03:00
Paul Chote
c04041e328 Add StructureSold notification placeholder for TDHD. 2025-04-01 20:23:05 +03:00
Paul Chote
c028d8ad29 Define ColorShift data for TDHD. 2025-04-01 20:23:05 +03:00
Paul Chote
597e8e1573 Replace fog palette with sequence alpha. 2025-04-01 20:23:05 +03:00
Paul Chote
0b03d7ea31 Enable player color for crushed sequences. 2025-04-01 20:23:05 +03:00
Paul Chote
ee24d0539a Replace custom oil derrick sprite with palette. 2025-04-01 20:23:05 +03:00
Paul Chote
25188e849e Move TileSize definition to terrain info. 2025-04-01 20:10:39 +03:00
Pavel Penev
0f7b24a8c0 Made SupportPower abstract
Because it really needs to be.
2025-04-01 19:28:56 +03:00
Pavel Penev
fc8103098e Formatted .csproj files according to editorconfig 2025-04-01 19:28:56 +03:00
RoosterDragon
79454d8fd2 Fix IDE0028, IDE0300, IDE0301, IDE0302, IDE0303, IDE0304.
Silence IDE0305.
2025-03-31 12:02:41 +03:00
Gustas
0740991c12 Increase perf for parsing remote maps 2025-03-30 18:32:00 +01:00
Gustas
117a47f81f Add a backup for unloaded bots 2025-03-30 18:16:53 +01:00
Gustas
299c9b5506 Make lobby names dynamic, incase players don't have maps 2025-03-30 18:16:53 +01:00
Gustas
30300d18bf Fix game browser tooltips being untranslated 2025-03-30 18:16:53 +01:00
Gustas
665bebc460 Fix custom bot names being untranslated in lobby 2025-03-30 10:20:35 +01:00
Gustas
385999dce9 Add sound to capturable blink 2025-03-30 03:37:48 +02:00
Gustas
13991f12c4 Fix resource area calculation 2025-03-29 16:54:48 +02:00
Gustas
b623c5aa72 Fix rallypoints not showing full target lines 2025-03-29 16:41:52 +02:00
Gustas
37b8912a91 Add missing dispose calls 2025-03-29 13:58:11 +00:00
Gustas
e7d49723ea Fix camera being in an incorrect position of first frame 2025-03-28 21:18:16 +00:00
Gustas
e2cb83436c Fix telemetry data description 2025-03-28 21:12:51 +00:00
Gustas
2b28da97f9 Fix alignment regressions 2025-03-28 21:12:51 +00:00
Paul Chote
5da2f1ba8a Implement sonic blast rendering effect. 2025-03-28 18:36:16 +02:00
N.N
3111d06fcf SonicBlast projectile
-add sonicblast projectile
- Adjust Sonic tank to match original D2k
2025-03-27 23:38:42 +02:00
JovialFeline
6340379e3d Get Fluent bot names in server list 2025-03-27 23:05:51 +02:00
N.N
e78ab81b26 Fix wrong Spread in Warheads 2025-03-23 16:11:23 +02:00
Paul Chote
e8391de36a Work around Raspberry Pi GLSL bug. 2025-03-23 15:44:11 +02:00
JovialFeline
ad3caf7e00 Add explosion effects to RA bridges 2025-03-23 10:17:31 +00:00
Matthias Mailänder
7e669013d9 Avoid a thirdparty action. 2025-03-18 14:57:58 +02:00
N.N
d9425e4119 Fix crush anim 2025-03-18 14:26:46 +02:00
Matthias Mailänder
c4756b78ae Avoid thirdparty action. 2025-03-18 14:18:26 +02:00
Paul Chote
b40b67242f Fix FlashPostProcessEffect channel order. 2025-03-18 14:09:30 +02:00
Ashley Newson
37e5b0d74c Fix CnC generated map oceans becoming debris-filled
Water is not playable in CnC, so, the map generator may fill it in if
it's not attributed to the largest playable region. This would often
happen for circular-in-water maps when DenyWalledAreas is enabled.

This change adds a step to adopt any neighboring partially playable
space (such as beaches and water) into the largest region, which is
exempt from debris filling.

The change is almost purely cosmetic, though the previous debris filling
could slightly reduce playable space (due to filling playable beach
tiles), possibly causing generation to fail. As such, map generation is
now a very tiny bit more reliable.
2025-03-18 13:58:04 +02:00
RoosterDragon
cf3e213e7d Harvesters prefer ore near to the refinery.
When searching for ore, harvesters will prefer ore close to their refinery, the dockPos. However this only works when this is set, and is was not being set when expected. Fixing this prevents harvesters moving in straight lines, particularly when the ore is to the left of the refinery, which is the default path search direction.

Fixes a regression from d0974cfdd2.
2025-03-16 10:39:15 +00:00
Gustas
f679bb1e2e Fix a potential crash when cancelling capture 2025-03-16 10:29:34 +00:00
Gustas
9c6ee3ad02 Fix tree husks on jungle tileset 2025-03-16 10:25:55 +00:00
N.N
a584bd8598 Fix frame on 1141 tile 2025-03-12 16:48:19 +02:00
JovialFeline
efce78179f Add civilian idle sequences 2025-03-05 21:43:21 +02:00
JovialFeline
cdef6413e7 Add AutoTarget & Selectable to CnC civilians 2025-03-05 10:23:52 +02:00
Paul Chote
9913ecb42f Remove save dialog when creating a new map. 2025-03-05 09:31:11 +02:00
Paul Chote
1707dbd92b Add support for in-memory ReadWriteZipFiles. 2025-03-05 09:31:11 +02:00
Paul Chote
fceb8a479d Allow maps to be loaded directly, bypassing the MapCache. 2025-03-05 09:31:11 +02:00
Paul Chote
e186dc428e Rework MapCache API.
This clarifies the ownership of the underlying
IReadOnlyPackage objects, and avoids passing
around some unnecessary state.
2025-03-05 09:31:11 +02:00
Paul Chote
5c0b82e65b Remove unused MapPreview ctor. 2025-03-05 09:31:11 +02:00
Matthias Mailänder
d379258afb Define app brand colors. 2025-03-04 09:22:04 +02:00
Matthias Mailänder
e4ec2dfb4a Quiet the chocolatey install. 2025-03-04 09:10:35 +02:00
Paul Chote
e06d74b417 Simplify map generator naming. 2025-03-04 09:05:59 +02:00
Paul Chote
ab3b6a0e43 Cache Fluent queries. 2025-03-04 09:05:59 +02:00
Paul Chote
e022744b0a Remove Map requirement from IMapGeneratorInfo.GetSettings. 2025-03-04 09:05:59 +02:00
Paul Chote
23b7b56c28 Combine IMapGenerator interfaces.
This removes the need to instantiate a dummy
trait instance and is more idiomatic.
2025-03-04 09:05:59 +02:00
Gustas
7ee79a0e64 Additional punctuation fixup 2025-03-03 21:01:46 +00:00
test1232156
8f59ddff5d Remove unnecessary punctuation from descriptions 2025-03-02 18:58:18 +02:00
RoosterDragon
db90cfb259 Show current pan behaviour & zoom modifier in input settings.
Fixes regression from 0338258b45.
2025-03-02 11:04:05 +02:00
JovialFeline
c892666b8f Fix soviet-06 harvester production crash 2025-02-26 23:40:34 +02:00
Paul Chote
541cf3b720 Sanitize saved skirmish factions. 2025-02-22 15:31:51 +02:00
RoosterDragon
0338258b45 Fix IDE0200 2025-02-22 14:56:25 +02:00
RoosterDragon
ce3ad6fbb3 Some improvements for SpatiallyPartitioned.
- Tweak the Update and Remove methods to reduce the number of dictionary lookups required.
- Change the Update method to an indexer, this allows simplifying callers who wanted to AddOrUpdate a value.
- Implement IDictionary<T, Rectangle>, since the class already implements these semantics by providing a backing store of the bounds for each item.
- Clean up some naming to use the generic `item` instead of `actor`.
- Make the MutateBins action methods static.
2025-02-22 14:50:15 +02:00
RoosterDragon
1b2c119b58 Fix RCS1257, RCS1258, RCS1261, RCS1262, RCS1265, RCS1266, RCS1267 2025-02-22 14:35:57 +02:00
RoosterDragon
a324af1afd Fix CA2024 2025-02-21 16:07:56 +02:00
RoosterDragon
715d882456 Update LangVersion to C# 12.
Now that we have moved to net 8 from net 6, and with us no longer supporting the mono runtime, we can raise the C# version to match that supported by the net 8 runtime.

This unlocks C# 10, C# 11 and C# 12 features previously unavailable to us.
- https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-10
- https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-11
- https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-12

A newer version of StyleCop is required to avoid rules tripping up on the new syntax.

Enable a handful of style rules that don't have many violations.
2025-02-20 12:54:12 +02:00
Ashley Newson
39a7e477c6 Add tooltips to some map generator settings
Adds tooltip descriptions for a few map generator settings. The tooltips
are limited to dropdown choices. No tooltips are added for the options
containing the choices or any non-dropdown settings.

- Terrain type
- Bounds shape
- Buildings

The "Square" bounds shape is rephrased as "Rectangle" for consistency
with the added description.

This is UI-only change.
2025-02-20 12:49:16 +02:00
Gustas
85999d7a40 vs seems more standard 2025-02-19 21:01:26 +00:00
Gustas
6ac33befe5 Adjust to a 4 space standard 2025-02-19 20:50:36 +00:00
RoosterDragon
cfde11556f Enable Net 7 and Net 8 style rules. 2025-02-16 20:01:50 +02:00
Gustas
71ae0aedfb Add z offset to TeslaZap 2025-02-16 12:55:40 +01:00
Gustas
6193da6899 Allow passing map author as an argument 2025-02-16 12:50:28 +01:00
JovialFeline
459bbf0025 Annotate campaign.lua 2025-02-16 12:48:23 +01:00
RoosterDragon
96de59f75a Run spell check over solution. 2025-02-16 12:45:19 +01:00
Gustas
ebf90970b2 Re-add a mini lobby options menu to missionbrowser 2025-02-16 12:39:23 +01:00
Gustas
747f41a8c3 Tooltips should not have punctuation 2025-02-16 12:39:23 +01:00
RoosterDragon
639dc7c288 Fix a bad comparison against Target.Invalid.
Target.Invalid acts like a NaN, and will not compare equal with itself. Compare against the TargetType instead, which performs the intended comparison.
2025-02-15 13:22:23 +00:00
Gustas
6ee274be6b Update SDL to 2.32.0 2025-02-15 13:19:22 +00:00
Gustas
017e645557 Upgrade unit testing 2025-02-15 08:56:24 +00:00
Gustas
bcbf88719f Upgrade NuGet packages 2025-02-15 08:56:24 +00:00
michaeldgg2
81aa068b15 Upgrade to .NET 8.0 and remove/migrate obsolete stuff 2025-02-15 08:56:24 +00:00
michaeldgg2
2a3ef8f910 Drop win-x86 build 2025-02-15 08:56:24 +00:00
Ashley Newson
0d30b19b1b Remove fields, railmine, and snowhut from RA map generation 2025-02-11 12:57:04 +02:00
Ashley Newson
98fe3c98ab Refine civilian building placement and multibrushes 2025-02-11 12:57:04 +02:00
Ashley Newson
cc632660eb Add decorative civilian buildings to random maps
Allows the random map generator to optionally place civilian buildings
around the map for extra decoration. Buildings are placed last with
lowest priority to avoid competing for play area and resources.

Depends on #21738
2025-02-11 12:57:04 +02:00
Ashley Newson
1f65fd234b Use long in CellLayerUtils.CalibrateQuantileInPlace index calculation
This is similar to #21727, but in CellLayerUtils instead of MatrixUtils.
Note that the lack of long does not result in any overflows/crashes in
existing code, but can cause crashes for new features requiring larger
calculations.
2025-02-08 14:11:05 +02:00
JovialFeline
052c74e2d7 Add Lua documentation details 2025-02-08 14:02:12 +02:00
Ashley Newson
611ac8ce54 Add fuzzer for map generator testing
Adds a fuzzer that iterates through map generation settings combinations
to either discover crashes or measure the frequency of rejected maps.

All generated maps are in-memory only and discarded immediately after
generation.

Running the fuzzer on anything other than a small combination of
settings is time expensive. It is added only as a utility for manual
invocation and is not integrated into the regular set of tests.
2025-02-06 14:23:01 +02:00
Ashley Newson
c6d5a1a182 Yield focus for map generator text settings
Makes any selected text-based map generator settings (currently just the
seed input) yield keyboard focus when the user presses escape or enter.
2025-02-06 14:06:04 +02:00
Gustas
09259be796 Match the check inside .CenterPosition 2025-02-01 20:31:07 +00:00
Gustas
409efe7f4a Allow tileset to be entered in lower case 2025-02-01 20:22:43 +00:00
Gustas
0ea4bb10a1 Fix utility commands losing data 2025-02-01 20:22:43 +00:00
Gustas
56109b0ca1 Fix bot harvesters crashing 2025-02-01 17:39:40 +02:00
Ashley Newson
1fda679b9f Refine map generator spawn placement rules
Spawn generation generally tries to place spawns:

- In spacious areas
- Away from the center (true center or mirror lines)
- Away from a symmetry-projected spawn
- Away from previously placed spawns

This commit introduces the following adjustments:

- The spacing between sequentially placed spawns is relaxed.
- Fix spawn mines contributing too much to the space reservation.
- Factor rotations into anti-center biasing calculation.
- Preserve spacing information in center space fallback decisions.
- Use a linear (instead of binary) falloff for anti-center biasing.
- Enforce that spawns have a minimum buildable area around them.
- Enforce that symmetry-projected spawns have as much separation as
  sequentially placed spawns would.
- Allow spawns on or near roads.
2025-01-31 13:58:20 +02:00
JovialFeline
440987c0a4 Fix unrepairable desert bridge 2025-01-30 14:10:09 +02:00
Ashley Newson
cdbcfb9e20 Fix illegal ore rings popping up around spawns in generated maps
Reorders and groups selective clearing of the resource placement plan to
-int.MaxValue to avoid subsequent code from inappropriately trying to
use it in arithmetic, which may just underflow to some completely wrong
value and lead to resources in illegal places.
2025-01-29 13:34:49 +02:00
Ashley Newson
19585ec7ac Fix overflow when calculating array quantile index
Fixes a crash which happens when trying to generate very large maps,
such as 256x256.

```
Exception of type `System.IndexOutOfRangeException`: Index was outside the bounds of the array.
   at OpenRA.Mods.Common.MapGenerator.MatrixUtils.CalibrateQuantileInPlace(Matrix`1 matrix, Int32 target, Int32 count, Int32 outOf) in /home/ashley/devel/OpenRA/OpenRA.Mods.Common/MapGenerator/MatrixUtils.cs:line 719
   at OpenRA.Mods.Common.Traits.RaMapGenerator.Generate(Map map, MiniYaml settings) in /home/ashley/devel/OpenRA/OpenRA.Mods.Common/Traits/World/RaMapGenerator.cs:line 755
...
```
2025-01-28 22:10:52 +02:00
JovialFeline
ddfdfcfee3 D2k shellmap: Hide pickup indicators 2025-01-28 19:04:04 +02:00
JovialFeline
49fd39b4b6 Fix some campaign Fluent keys 2025-01-28 18:57:39 +02:00
JovialFeline
55787fb7b7 Restore StartGameNotification in missions 2025-01-28 16:23:23 +02:00
JovialFeline
6a8e5c7c83 Hide owner row for D2k special mission Outposts 2025-01-28 16:11:35 +02:00
Ashley Newson
037326024b Remove floating points from map generation
Removes all use of floating point from the RaMapGenerator map generator
and its dependencies.

Floating point behavior is potentially non-portable across client
hardware. Removing them should make the map generation logic
consistent even across clients with different floating point hardware
or compiler behavior. This may be useful for sync-safe multiplayer map
generation where clients independently generate the map from settings.

Most previously fractional public-facing settings are now represented
as numbers out of 1000, with some exceptions using 1000000. Most
internal logic which relies on fixed-point concepts now uses 1024ths,
though some floating point mechanisms have been replaced with
alternative discrete approximations (e.g. gaussian to binomial).
2025-01-27 20:42:38 +02:00
Ashley Newson
04e9cef38e Fix bounding of circle drawing in CellLayerUtils
Previously, only the RectangularIsometric case was handled, despite
this currently only being used in Rectangular mode. Additionally,
the calculation was slightly off.
2025-01-27 14:17:47 +02:00
Ashley Newson
2126f3c5a2 Add CnC map generator support
Adds map generator support for CnC (all tilesets), largely on par with
RA.

Most tilesets do not have a complete set of beach templates and
therefore do not support terrain settings involving water. DESERT is the
only tileset supporting water. Unlike in RA, water is not playable space
in CnC (no naval units), so only terrain settings with small bodies of
water are available.

Most changes are configuration (tileset and map generator config), with
just a small number of code changes.
2025-01-20 12:55:36 +02:00
Gustas
bdc50899de Fix bleed documentation
GitHub runner as it runs an older version of Python
2025-01-18 12:24:16 +00:00
Dan Stoian
75d4fa2509 update AUTHORS 2025-01-18 13:34:26 +02:00
Ashley Newson
9d77ca2bf8 Generate more elaborate roads
Updates the random map generator to produce more elaborate roads:

- Roads extend beyond map bounds.
- Shorter, unviable roads are pruned and merged to create longer roads.
- Roads can loop.
2025-01-18 13:32:56 +02:00
Pavel Penev
5f9e0ffd43 Update Python documentation script 2025-01-17 18:09:02 +02:00
Pavel Penev
dfa922de91 Pulled out shared code from documentation commands 2025-01-17 18:09:02 +02:00
Pavel Penev
c66f63805a Added types for Utility documentation output 2025-01-17 18:09:02 +02:00
Pavel Penev
f2e582ce83 Moved documentation utility commands to a folder 2025-01-17 18:09:02 +02:00
Dan Stoian
e903baf680 Implement auto-save 2025-01-17 18:01:20 +02:00
JovialFeline
94f2ce4f56 Fix ordos-04 to use internal bot names 2025-01-16 13:09:21 +02:00
Ashley Newson
dc4c596fa0 Fix an out-of-bounds CellLayer access in RaMapGenerator
Adds a bounds check for the water obstruction code, where a rotation
projection can be outside of the map area (on rotations other than
1, 2, or 4.)
2025-01-16 12:56:44 +02:00
Paul Chote
b9adbf8089 Only expose Facing for actors that define a facing. 2025-01-15 18:07:10 +02:00
Paul Chote
427e56d718 Only expose Location and CenterPosition for actors that occupy space. 2025-01-15 18:07:10 +02:00
JovialFeline
01819714a2 Correct nod-06c capture objective 2025-01-15 17:39:41 +02:00
RoosterDragon
aa121dcb2f Use covariant return types.
Use the covariant return type feature of C# 9 to allow method overrides to be more specific about their return type. By exposing this information, e.g. for Widget.Clone(), callers will have more information. This allows various casts to be removed as the information is now available within the type system.
2025-01-15 17:29:02 +02:00
Ashley Newson
9c568aba33 Fix map generator failures due to broken TilingPath edge extension
Coastlines and cliffs at the edge of a map should have been getting
extended beyond the map edge help them tile reliably. This was not
happening due to a conditional that accidentally got inverted during a
refactor.

Fixing the conditional makes beach and cliff tiling significantly more
reliable. (Note that there is no effect on road tiling.)
2025-01-15 17:23:41 +02:00
Pavel Penev
22d3b5e5a1 Added link to GH source code to documentation 2025-01-11 15:50:10 +02:00
Ashley Newson
04b61a20ea Fix tileset MiniYaml keys to be consistent with Ids
Makes the `Template@...` number consistent with the `Id: ...` (if any).

The keys are ignored by the engine, so this has no behavioral change.
2025-01-11 15:07:41 +02:00
Ashley Newson
417f787294 Add experimental RA procedural map generator
Add an experimental procedural map generator for the Red Alert mod,
along with supporting code that may assist in the development of map
generators for other mods.

Map generation may be accessed as a tool in the Map Editor. This change
does not presently introduce direct lobby options for generated maps.

Features:

- Terrain with land, water, beaches, cliffs, roads, debris, and trees.
- Placement of mpspawns, neutral buildings, and resources.
- Rotational and mirror symmetry options.
- Various configurable parameters with presets.
- Deterministic with configurable seed.
- Performant.
2025-01-09 16:47:10 +02:00
louxia
0536c58b78 Avoid units continuing to follow the unattackable targets 2025-01-08 21:45:44 +02:00
Paul Chote
711055a3bc Add filenames to Trait/Weapon/SpriteSequence docs metadata. 2025-01-07 19:21:29 +02:00
Gustas
0efbefc62a OpenRA FreeBSD release is no longer maintained 2025-01-07 18:07:13 +02:00
michaeldgg2
c7cc9a68fd Eradicate ☠ Mono ☠
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
2025-01-07 18:07:13 +02:00
Paul Chote
5450572e0a Allow maps to override the player viewport size. 2025-01-06 23:57:08 +02:00
Gustas
8b4478260e Raname GrantConditionOnDeployWithCharge to GrantChargedConditionOnToggle
Also allow deploying without cancelling current activity, and make saboteurs use it
2024-12-27 16:37:49 +02:00
Gustas
d9cfb2c278 Cancel movement in invalid states 2024-12-27 16:30:38 +02:00
Gustas
1904012604 Fix harvesters refusing to queue dock orders whenn empty 2024-12-27 16:30:38 +02:00
test1232156
2c221fdc68 Fix translation key
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
2024-12-21 19:30:22 +02:00
Pavel Penev
955bf96a84 Added descriptions to ContentInstallerFileSystemLoader 2024-12-21 16:01:21 +02:00
Pavel Penev
f210b39bbc Just some package definition reordering
Mostly superficial, but in case of file name collisions we want the mod-specific files to override the common ones.
2024-12-21 16:01:21 +02:00
Pavel Penev
59c0c908cc Added missing D2k packages 2024-12-21 16:01:21 +02:00
Pavel Penev
640aa6545b Fixed whitespace issues in mod.yaml files
An oversight from PR 21598.
2024-12-21 16:01:21 +02:00
Gustas
269542c4b0 Add delete hotkey 2024-12-20 23:56:56 +02:00
Gustas
f820adab51 Add delete button to the map editor 2024-12-20 23:56:56 +02:00
Gustas
48d5b3c8d9 Update extractors to preserve indentation 2024-12-20 22:00:21 +02:00
Gustas
81875a1f3f Fixup new lines in encyclopedias 2024-12-20 22:00:21 +02:00
Gustas
0bd139b061 Fix unit descriptions having incorrect indentation 2024-12-20 22:00:21 +02:00
test1232156
9719c1a3b0 Encyclopedia fixup 2024-12-20 22:00:21 +02:00
RoosterDragon
0051d6fd63 Fix BaseBuilderBotModule.LocomotorsForProducibles.
Account for per-actor production (e.g. ProductionQueue) and per-player production (e.g. ClassicProductionQueue). This requires resolving the Production and ProductionQueue traits on both the producing actor, and the owning player actor.

When setting rally points, check the actor didn't die first.
2024-12-19 19:36:28 +02:00
Gustas
2a1eeae7ca Reduce the size of battlefield news when messages are short 2024-12-19 18:16:07 +02:00
Gustas
43325fdee7 Fix Package 'NuGet.CommandLine' 4.4.1 has a known moderate severity vulnerability 2024-12-12 16:00:19 +02:00
Mustafa Alperen Seki
087aebe058 Fix broken condition support for InstantlyRepairable. 2024-12-12 00:30:01 +02:00
Pavel Penev
18450aaff0 Revert "Restore threaded renderer on Windows in windowed mode."
This reverts commit 874fa67036.
2024-12-12 00:25:46 +02:00
test1232156
1af1ebd9e3 Translation key fixes
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
2024-12-04 22:07:13 +02:00
RoosterDragon
f76009d095 Fix Animation shadow to account for height.
If a unit is above the terrain, the shadow shouldn't display directly underneath the unit, it should take the height into account and display further down. This fix uses the same adjustment as applied by the WithShadow trait.
2024-11-30 14:45:06 +02:00
Matthias Mailänder
79875056bc Update dependency model to fix CVE warnings. 2024-11-30 13:39:50 +02:00
1878 changed files with 61091 additions and 15123 deletions

View File

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

View File

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

6
.github/dependabot.yml vendored Normal file
View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Prepare Environment - name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV} run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -40,12 +40,12 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Install .NET 6.0 - name: Install .NET 8.0
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: '6.0.x' dotnet-version: '8.0.x'
- name: Prepare Environment - name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV} run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -65,15 +65,15 @@ jobs:
macos: macos:
name: macOS Disk Image name: macOS Disk Image
runs-on: macos-13 runs-on: macos-14
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Install .NET 6.0 - name: Install .NET 8.0
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: '6.0.x' dotnet-version: '8.0.x'
- name: Prepare Environment - name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV} run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -101,12 +101,12 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Clone Repository - name: Clone Repository
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Install .NET 6.0 - name: Install .NET 8.0
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: '6.0.x' dotnet-version: '8.0.x'
- name: Prepare Environment - name: Prepare Environment
run: | run: |
@@ -119,9 +119,37 @@ jobs:
mkdir -p build/windows mkdir -p build/windows
./packaging/windows/buildpackage.sh "${GIT_TAG}" "${PWD}/build/windows" ./packaging/windows/buildpackage.sh "${GIT_TAG}" "${PWD}/build/windows"
- name: Upload Installer
id: unsigned-artifact
if: github.repository == 'openra/openra'
uses: actions/upload-artifact@v6
with:
path: build/windows/*.exe
- name: Microsoft Authenticode
id: signpath
if: github.repository == 'openra/openra'
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '${{ secrets.SIGNPATH_ORGANISATION_ID }}'
project-slug: 'OpenRA'
signing-policy-slug: 'release-signing'
github-artifact-id: '${{ steps.unsigned-artifact.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'build/windows/signed/'
parameters: |
version: ${{ toJSON(github.ref_name) }}
- name: Upload Packages - name: Upload Packages
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash shell: bash
run: | run: |
gh release upload ${{ github.ref_name }} build/windows/* gh release upload "${{ github.ref_name }}" build/windows/*.zip
if [ "${GITHUB_REPOSITORY}" = "openra/openra" ]; then
gh release upload "${{ github.ref_name }}" build/windows/signed/*.exe
else
gh release upload "${{ github.ref_name }}" build/windows/*.exe
fi

4
.gitignore vendored
View File

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

View File

@@ -36,6 +36,7 @@ Also thanks to:
* Andreas Beck (baxtor) * Andreas Beck (baxtor)
* Ang Soon Li (asl97) * Ang Soon Li (asl97)
* Arik Lirette (Angusm3) * Arik Lirette (Angusm3)
* Ashley Newson
* Barnaby Smith (mvi) * Barnaby Smith (mvi)
* Bellator * Bellator
* Bernd Stellwag (burned42) * Bernd Stellwag (burned42)
@@ -54,6 +55,7 @@ Also thanks to:
* Constantin Helmig (CH4Code) * Constantin Helmig (CH4Code)
* D2k Sardaukar * D2k Sardaukar
* D'Arcy Rush (r34ch) * D'Arcy Rush (r34ch)
* Dan Stoian (stoiandan)
* Daniel Derejvanik (Harisson) * Daniel Derejvanik (Harisson)
* Danny Keary (Dan9550) * Danny Keary (Dan9550)
* David Jiménez (Rydra) * David Jiménez (Rydra)
@@ -141,6 +143,7 @@ Also thanks to:
* Riderr3 * Riderr3
* riiga * riiga
* Rikhardur Bjarni Einarsson (WolfGaming) * Rikhardur Bjarni Einarsson (WolfGaming)
* Robert Nordan (robpvn)
* Sascha Biedermann (bidifx) * Sascha Biedermann (bidifx)
* Sean Hunt (coppro) * Sean Hunt (coppro)
* Shawn Collins (UberWaffe) * Shawn Collins (UberWaffe)

View File

@@ -24,6 +24,6 @@ Don't forget to add yourself to [AUTHORS](https://github.com/OpenRA/OpenRA/blob/
Please propose a [CHANGELOG](https://github.com/OpenRA/OpenRA/wiki/CHANGELOG) entry in the pull-request comments. Please propose a [CHANGELOG](https://github.com/OpenRA/OpenRA/wiki/CHANGELOG) entry in the pull-request comments.
While your pull-request is in review it will be helpful if you join [IRC](irc://chat.freenode.net/openra) to discuss the changes. While your pull-request is in review it will be helpful if you join [Discord](https://discord.openra.net) or [IRC](ircs://irc.libera.chat:6697/openra) to discuss the changes.
See also the in-depth guide on [contributing](https://github.com/OpenRA/OpenRA/wiki/Contributing) to the OpenRA project. See also the in-depth guide on [contributing](https://github.com/OpenRA/OpenRA/wiki/Contributing) to the OpenRA project.

View File

@@ -5,7 +5,7 @@
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Optimize>true</Optimize> <Optimize>true</Optimize>
<LangVersion>9</LangVersion> <LangVersion>12</LangVersion>
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
<EngineRootPath Condition="'$(EngineRootPath)' == ''">..</EngineRootPath> <EngineRootPath Condition="'$(EngineRootPath)' == ''">..</EngineRootPath>
<OutputPath>$(EngineRootPath)/bin</OutputPath> <OutputPath>$(EngineRootPath)/bin</OutputPath>
@@ -18,8 +18,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework Condition="'$(MSBuildRuntimeType)'!='Mono'">net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<TargetFramework Condition="'$(MSBuildRuntimeType)'=='Mono'">netstandard2.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
@@ -53,9 +52,8 @@
<!-- StyleCop/Roslynator --> <!-- StyleCop/Roslynator -->
<ItemGroup> <ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" PrivateAssets="All" />
<!-- Roslynator analyzers fail to run under Mono (AD0001) --> <PackageReference Include="Roslynator.Analyzers" Version="4.13.0" PrivateAssets="All" />
<PackageReference Include="Roslynator.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" /> <PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.13.0" PrivateAssets="All" />
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,17 +78,9 @@ namespace OpenRA
return new MPos(X, Y); return new MPos(X, Y);
// Convert from RectangularIsometric cell (x, y) position to rectangular map position (u, v) // Convert from RectangularIsometric cell (x, y) position to rectangular map position (u, v)
// - The staggered rows make this fiddly (hint: draw a diagram!) // The staggered rows make this fiddly.
// (a) Consider the relationships:
// - +1x (even -> odd) adds (0, 1) to (u, v)
// - +1x (odd -> even) adds (1, 1) to (u, v)
// - +1y (even -> odd) adds (-1, 1) to (u, v)
// - +1y (odd -> even) adds (0, 1) to (u, v)
// (b) Therefore:
// - ax + by adds (a - b)/2 to u (only even increments count)
// - ax + by adds a + b to v
var u = (X - Y) / 2;
var v = X + Y; var v = X + Y;
var u = (v - (v & 1)) / 2 - Y;
return new MPos(u, v); return new MPos(u, v);
} }

View File

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

View File

@@ -20,7 +20,7 @@ namespace OpenRA
public static class CryptoUtil public static class CryptoUtil
{ {
// Fixed byte pattern for the OID header // Fixed byte pattern for the OID header
static readonly byte[] OIDHeader = { 0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0 }; static readonly byte[] OIDHeader = [0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0];
static readonly char[] HexUpperAlphabet = "0123456789ABCDEF".ToArray(); static readonly char[] HexUpperAlphabet = "0123456789ABCDEF".ToArray();
static readonly char[] HexLowerAlphabet = "0123456789abcdef".ToArray(); static readonly char[] HexLowerAlphabet = "0123456789abcdef".ToArray();
@@ -210,8 +210,7 @@ namespace OpenRA
using (var rsa = new RSACryptoServiceProvider()) using (var rsa = new RSACryptoServiceProvider())
{ {
rsa.ImportParameters(parameters); rsa.ImportParameters(parameters);
using (var csp = SHA1.Create()) return Convert.ToBase64String(rsa.SignHash(SHA1.HashData(data), CryptoConfig.MapNameToOID("SHA1")));
return Convert.ToBase64String(rsa.SignHash(csp.ComputeHash(data), CryptoConfig.MapNameToOID("SHA1")));
} }
} }
catch (Exception e) catch (Exception e)
@@ -236,8 +235,7 @@ namespace OpenRA
using (var rsa = new RSACryptoServiceProvider()) using (var rsa = new RSACryptoServiceProvider())
{ {
rsa.ImportParameters(parameters); rsa.ImportParameters(parameters);
using (var csp = SHA1.Create()) return rsa.VerifyHash(SHA1.HashData(data), CryptoConfig.MapNameToOID("SHA1"), Convert.FromBase64String(signature));
return rsa.VerifyHash(csp.ComputeHash(data), CryptoConfig.MapNameToOID("SHA1"), Convert.FromBase64String(signature));
} }
} }
catch (Exception e) catch (Exception e)
@@ -252,14 +250,12 @@ namespace OpenRA
public static string SHA1Hash(Stream data) public static string SHA1Hash(Stream data)
{ {
using var csp = SHA1.Create(); return ToHex(SHA1.HashData(data), true);
return ToHex(csp.ComputeHash(data), true);
} }
public static string SHA1Hash(byte[] data) public static string SHA1Hash(byte[] data)
{ {
using var csp = SHA1.Create(); return ToHex(SHA1.HashData(data), true);
return ToHex(csp.ComputeHash(data), true);
} }
public static string SHA1Hash(string data) public static string SHA1Hash(string data)

View File

@@ -9,17 +9,14 @@
*/ */
#endregion #endregion
using OpenRA.Traits; using System;
namespace OpenRA.Mods.Common.Traits namespace OpenRA
{ {
[Desc("The actor is always considered visible for targeting and rendering purposes.")] // Mirrors DescriptionAttribute from System.ComponentModel but we don't want to have to use that everywhere.
public class AlwaysVisibleInfo : TraitInfo<AlwaysVisible>, IDefaultVisibilityInfo { } [AttributeUsage(AttributeTargets.All)]
public class AlwaysVisible : IDefaultVisibility public sealed class DescAttribute(params string[] lines) : Attribute
{ {
public bool IsVisible(Actor self, Player byPlayer) public readonly string[] Lines = lines;
{
return true;
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@
using System; using System;
namespace OpenRA.Mods.Cnc.FileFormats namespace OpenRA.FileFormats
{ {
/// <summary> /// <summary>
/// Static class that uses a lookup table to calculates CRC32 /// Static class that uses a lookup table to calculates CRC32
@@ -22,8 +22,8 @@ namespace OpenRA.Mods.Cnc.FileFormats
/// <summary> /// <summary>
/// The CRC32 lookup table. /// The CRC32 lookup table.
/// </summary> /// </summary>
static readonly uint[] LookUp = new uint[256] static readonly uint[] LookUp =
{ [
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
@@ -88,7 +88,7 @@ namespace OpenRA.Mods.Cnc.FileFormats
0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
}; ];
/// <summary> /// <summary>
/// A CRC32 implementation that can be used on spans of bytes. /// A CRC32 implementation that can be used on spans of bytes.
@@ -118,5 +118,16 @@ namespace OpenRA.Mods.Cnc.FileFormats
{ {
return Calculate(data, 0xFFFFFFFF); return Calculate(data, 0xFFFFFFFF);
} }
// Chaining method: accepts the current CRC state
public static uint Update(uint crc, ReadOnlySpan<byte> data)
{
for (var i = 0; i < data.Length; i++)
crc = (crc >> 8) ^ LookUp[(crc & 0xFF) ^ data[i]];
return crc;
}
// Standard PNG start: 0xFFFFFFFF, Finalize: XOR 0xFFFFFFFF
public static uint Finish(uint crc) => crc ^ 0xFFFFFFFF;
} }
} }

View File

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

View File

@@ -27,8 +27,7 @@ namespace OpenRA.FileFormats
public ReplayMetadata(GameInformation info) public ReplayMetadata(GameInformation info)
{ {
if (info == null) ArgumentNullException.ThrowIfNull(info);
throw new ArgumentNullException(nameof(info));
GameInfo = info; GameInfo = info;
} }

View File

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

View File

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

View File

@@ -98,11 +98,11 @@ namespace OpenRA.FileSystem
{ {
readonly MemoryStream pkgStream = new(); readonly MemoryStream pkgStream = new();
public ReadWriteZipFile(string filename, bool create = false) public ReadWriteZipFile(string filename = null, bool create = false)
{ {
// SharpZipLib breaks when asked to update archives loaded from outside streams or files // SharpZipLib breaks when asked to update archives loaded from outside streams or files
// We can work around this by creating a clean in-memory-only file, cutting all outside references // We can work around this by creating a clean in-memory-only file, cutting all outside references
if (!create) if (!string.IsNullOrEmpty(filename) && !create)
{ {
using (var copy = new MemoryStream(File.ReadAllBytes(filename))) using (var copy = new MemoryStream(File.ReadAllBytes(filename)))
{ {
@@ -120,8 +120,26 @@ namespace OpenRA.FileSystem
entry.ExtraData = null; entry.ExtraData = null;
} }
public ReadWriteZipFile(byte[] data)
{
using (var copy = new MemoryStream(data))
{
pkgStream.Capacity = (int)copy.Length;
copy.CopyTo(pkgStream);
}
pkgStream.Position = 0;
pkg = new ZipFile(pkgStream);
Name = null;
// Remove subfields that can break ZIP updating.
foreach (ZipEntry entry in pkg)
entry.ExtraData = null;
}
void Commit() void Commit()
{ {
if (!string.IsNullOrEmpty(Name))
File.WriteAllBytes(Name, pkgStream.ToArray()); File.WriteAllBytes(Name, pkgStream.ToArray());
} }

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@@ -27,6 +28,7 @@ using OpenRA.Widgets;
namespace OpenRA namespace OpenRA
{ {
[IncludeStaticFluentReferences(typeof(Server.Server), typeof(Player), typeof(UnitOrders), typeof(OrderManager))]
public static class Game public static class Game
{ {
[FluentReference("filename")] [FluentReference("filename")]
@@ -166,6 +168,7 @@ namespace OpenRA
{ {
return ModData.WidgetLoader.LoadWidget(new WidgetArgs(args) return ModData.WidgetLoader.LoadWidget(new WidgetArgs(args)
{ {
{ "modData", ModData },
{ "world", world }, { "world", world },
{ "orderManager", OrderManager }, { "orderManager", OrderManager },
{ "worldRenderer", worldRenderer }, { "worldRenderer", worldRenderer },
@@ -181,7 +184,16 @@ namespace OpenRA
public static event Action BeforeGameStart = () => { }; public static event Action BeforeGameStart = () => { };
public static event Action AfterGameStart = () => { }; public static event Action AfterGameStart = () => { };
internal static void StartGame(string mapUID, WorldType type) internal static void StartGame(string uid, WorldType type)
{
var preview = ModData.MapCache[uid];
if (preview.Status != MapStatus.Available)
throw new InvalidDataException($"Invalid map uid: {uid}");
StartGame(preview.ToMap(), type);
}
internal static void StartGame(Map map, WorldType type)
{ {
// Dispose of the old world before creating a new one. // Dispose of the old world before creating a new one.
worldRenderer?.Dispose(); worldRenderer?.Dispose();
@@ -190,7 +202,23 @@ namespace OpenRA
BeforeGameStart(); BeforeGameStart();
using (new PerfTimer("NewWorld")) using (new PerfTimer("NewWorld"))
OrderManager.World = new World(mapUID, ModData, OrderManager, type); {
ModData.PrepareMap(map);
// The depth buffer needs to be initialized with enough range to cover:
// - the height of the screen
// - the z-offset of tiles from MaxTerrainHeight below the bottom of the screen (pushed into view)
// - additional z-offset from actors on top of MaxTerrainHeight terrain
// - a small margin so that tiles rendered partially above the top edge of the screen aren't pushed behind the clip plane
// We need an offset of mapGrid.MaximumTerrainHeight * mapGrid.TileSize.Height / 2 to cover the terrain height
// and choose to use mapGrid.MaximumTerrainHeight * mapGrid.TileSize.Height / 4 for each of the actor and top-edge cases
var margin = 0;
if (map.Grid.EnableDepthBuffer)
margin = map.Rules.TerrainInfo.TileSize.Height * map.Grid.MaximumTerrainHeight;
Renderer.SetDepthMargin(margin);
OrderManager.World = new World(map, ModData, OrderManager, type);
}
OrderManager.World.GameOver += FinishBenchmark; OrderManager.World.GameOver += FinishBenchmark;
@@ -344,7 +372,7 @@ namespace OpenRA
var explicitModPaths = Array.Empty<string>(); var explicitModPaths = Array.Empty<string>();
if (modID != null && (File.Exists(modID) || Directory.Exists(modID))) if (modID != null && (File.Exists(modID) || Directory.Exists(modID)))
{ {
explicitModPaths = new[] { modID }; explicitModPaths = [modID];
modID = Path.GetFileNameWithoutExtension(modID); modID = Path.GetFileNameWithoutExtension(modID);
} }
@@ -359,6 +387,48 @@ namespace OpenRA
Log.AddChannel("nat", "nat.log"); Log.AddChannel("nat", "nat.log");
Log.AddChannel("client", "client.log"); Log.AddChannel("client", "client.log");
Nat.Initialize();
var modSearchArg = args.GetValue("Engine.ModSearchPaths", null);
var modSearchPaths = modSearchArg != null ?
FieldLoader.GetValue<ImmutableArray<string>>("Engine.ModsPath", modSearchArg) :
[Path.Combine(Platform.EngineDir, "mods")];
Mods = new InstalledMods(modSearchPaths, explicitModPaths);
Console.WriteLine("Internal mods:");
foreach (var mod in Mods)
Console.WriteLine($"\t{mod.Key} ({mod.Value.Metadata.Version})");
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
ExternalMods = new ExternalMods();
if (modID == null)
throw new InvalidOperationException("Game.Mod argument missing.");
if (Mods.TryGetValue(modID, out var manifest))
{
var launchPath = args.GetValue("Engine.LaunchPath", null);
var launchArgs = new List<string>();
// Sanitize input from platform-specific launchers
// Process.Start requires paths to not be quoted, even if they contain spaces
if (launchPath != null && launchPath[0] == '"' && launchPath[^1] == '"')
launchPath = launchPath[1..^1];
// Metadata registration requires an explicit launch path
if (launchPath != null)
ExternalMods.Register(Mods[modID], launchPath, launchArgs, ModRegistration.User);
ExternalMods.ClearInvalidRegistrations(ModRegistration.User);
}
else
throw new InvalidOperationException($"Unknown or invalid mod '{modID}'.");
Console.WriteLine("External mods:");
foreach (var mod in ExternalMods)
Console.WriteLine($"\t{mod.Key} ({mod.Value.Version})");
var platforms = new[] { Settings.Game.Platform, "Default", null }; var platforms = new[] { Settings.Game.Platform, "Default", null };
foreach (var p in platforms) foreach (var p in platforms)
{ {
@@ -369,7 +439,7 @@ namespace OpenRA
try try
{ {
var platform = CreatePlatform(p); var platform = CreatePlatform(p);
Renderer = new Renderer(platform, Settings.Graphics); Renderer = new Renderer(platform, Settings.Graphics, manifest.RendererConstants.VertexBatchSize);
Sound = new Sound(platform, Settings.Sound); Sound = new Sound(platform, Settings.Sound);
break; break;
@@ -385,67 +455,23 @@ namespace OpenRA
} }
} }
Nat.Initialize(); InitializeMod(manifest, args);
var modSearchArg = args.GetValue("Engine.ModSearchPaths", null);
var modSearchPaths = modSearchArg != null ?
FieldLoader.GetValue<string[]>("Engine.ModsPath", modSearchArg) :
new[] { Path.Combine(Platform.EngineDir, "mods") };
Mods = new InstalledMods(modSearchPaths, explicitModPaths);
Console.WriteLine("Internal mods:");
foreach (var mod in Mods)
Console.WriteLine($"\t{mod.Key} ({mod.Value.Metadata.Version})");
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
ExternalMods = new ExternalMods();
if (modID != null && Mods.TryGetValue(modID, out _))
{
var launchPath = args.GetValue("Engine.LaunchPath", null);
var launchArgs = new List<string>();
// Sanitize input from platform-specific launchers
// Process.Start requires paths to not be quoted, even if they contain spaces
if (launchPath != null && launchPath[0] == '"' && launchPath.Last() == '"')
launchPath = launchPath[1..^1];
// Metadata registration requires an explicit launch path
if (launchPath != null)
ExternalMods.Register(Mods[modID], launchPath, launchArgs, ModRegistration.User);
ExternalMods.ClearInvalidRegistrations(ModRegistration.User);
}
Console.WriteLine("External mods:");
foreach (var mod in ExternalMods)
Console.WriteLine($"\t{mod.Key} ({mod.Value.Version})");
InitializeMod(modID, args);
} }
public static IPlatform CreatePlatform(string platformName) public static IPlatform CreatePlatform(string platformName)
{ {
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + platformName + ".dll"); var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + platformName + ".dll");
#if NET5_0_OR_GREATER
var loader = new AssemblyLoader(rendererPath); var loader = new AssemblyLoader(rendererPath);
var platformType = loader.LoadDefaultAssembly().GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t)); var platformType = loader.LoadDefaultAssembly().GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
#else
// NOTE: This is currently the only use of System.Reflection in this file, so would give an unused using error if we import it above
var assembly = System.Reflection.Assembly.LoadFile(rendererPath);
var platformType = assembly.GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
#endif
if (platformType == null) if (platformType == null)
throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation."); throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation.");
return (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null); return (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null);
} }
public static void InitializeMod(string mod, Arguments args) public static void InitializeMod(Manifest manifest, Arguments args)
{ {
// Clear static state if we have switched mods // Clear static state if we have switched mods
LobbyInfoChanged = () => { }; LobbyInfoChanged = () => { };
@@ -469,34 +495,25 @@ namespace OpenRA
ModData = null; ModData = null;
if (mod == null) Console.WriteLine($"Loading mod: {manifest.Id}");
throw new InvalidOperationException("Game.Mod argument missing.");
if (!Mods.ContainsKey(mod))
throw new InvalidOperationException($"Unknown or invalid mod '{mod}'.");
Console.WriteLine($"Loading mod: {mod}");
Sound.StopVideo(); Sound.StopVideo();
ModData = new ModData(Mods[mod], Mods, true); ModData = new ModData(manifest, Mods, true);
LocalPlayerProfile = new LocalPlayerProfile(Path.Combine(Platform.SupportDir, Settings.Game.AuthProfile), ModData.Manifest.Get<PlayerDatabase>()); LocalPlayerProfile = new LocalPlayerProfile(Path.Combine(Platform.SupportDir, Settings.Game.AuthProfile), ModData.GetOrCreate<PlayerDatabase>());
if (!ModData.LoadScreen.BeforeLoad()) if (!ModData.LoadScreen.BeforeLoad(ModData))
return; return;
ModData.InitializeLoaders(ModData.DefaultFileSystem); ModData.InitializeLoaders(ModData.DefaultFileSystem);
Renderer.InitializeFonts(ModData); Renderer.InitializeFonts(ModData);
using (new PerfTimer("LoadMaps")) using (new PerfTimer("LoadMaps"))
ModData.MapCache.LoadMaps(); ModData.MapCache.LoadMaps(ModData);
var grid = ModData.Manifest.Contains<MapGrid>() ? ModData.Manifest.Get<MapGrid>() : null;
Renderer.InitializeDepthBuffer(grid);
Cursor?.Dispose(); Cursor?.Dispose();
Cursor = new CursorManager(ModData.CursorProvider, ModData.Manifest.CursorSheetSize); Cursor = new CursorManager(ModData);
var metadata = ModData.Manifest.Metadata; var metadata = ModData.Manifest.Metadata;
if (!string.IsNullOrEmpty(metadata.WindowTitleTranslated)) if (!string.IsNullOrEmpty(metadata.WindowTitleTranslated))
@@ -514,10 +531,16 @@ namespace OpenRA
ModData.LoadScreen.StartGame(args); ModData.LoadScreen.StartGame(args);
} }
public static void LoadEditor(string mapUid) public static void LoadEditor(string uid)
{ {
JoinLocal(); JoinLocal();
StartGame(mapUid, WorldType.Editor); StartGame(uid, WorldType.Editor);
}
public static void LoadEditor(Map map)
{
JoinLocal();
StartGame(map, WorldType.Editor);
} }
public static void LoadShellMap() public static void LoadShellMap()
@@ -591,7 +614,7 @@ namespace OpenRA
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
var filename = TimestampedFilename(true); var filename = TimestampedFilename(true);
var path = Path.Combine(directory, string.Concat(filename, ".png")); var path = Path.Combine(directory, $"{filename}.png");
Log.Write("debug", "Taking screenshot " + path); Log.Write("debug", "Taking screenshot " + path);
Renderer.SaveScreenshot(path); Renderer.SaveScreenshot(path);
@@ -638,7 +661,7 @@ namespace OpenRA
world.Tick(); world.Tick();
PerfHistory.Tick(); PerfHistory.Tick(!world.Paused);
} }
// Wait until we have done our first world Tick before TickRendering // Wait until we have done our first world Tick before TickRendering
@@ -702,7 +725,7 @@ namespace OpenRA
// Use worldRenderer.World instead of OrderManager.World to avoid a rendering mismatch while processing orders // Use worldRenderer.World instead of OrderManager.World to avoid a rendering mismatch while processing orders
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave) if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
{ {
Renderer.BeginWorld(worldRenderer.Viewport.Rectangle); Renderer.BeginWorld(worldRenderer.Viewport.CenterLocation, worldRenderer.Viewport.ViewportSize);
Sound.SetListenerPosition(worldRenderer.Viewport.CenterPosition); Sound.SetListenerPosition(worldRenderer.Viewport.CenterPosition);
using (new PerfSample("render_world")) using (new PerfSample("render_world"))
worldRenderer.Draw(); worldRenderer.Draw();
@@ -717,15 +740,12 @@ namespace OpenRA
Ui.Draw(); Ui.Draw();
if (ModData != null && ModData.CursorProvider != null)
{
if (HideCursor) if (HideCursor)
Cursor.SetCursor(null); Cursor?.SetCursor(null);
else else
{ {
Cursor.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default"); Cursor?.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default");
Cursor.Render(Renderer); Cursor?.Render(Renderer);
}
} }
} }
@@ -739,12 +759,13 @@ namespace OpenRA
} }
} }
PerfHistory.Items["render"].Tick(); var isActive = !(worldRenderer?.World.Paused ?? true);
PerfHistory.Items["batches"].Tick(); PerfHistory.Items["render"].Tick(isActive);
PerfHistory.Items["render_world"].Tick(); PerfHistory.Items["batches"].Tick(isActive);
PerfHistory.Items["render_widgets"].Tick(); PerfHistory.Items["render_world"].Tick(isActive);
PerfHistory.Items["render_flip"].Tick(); PerfHistory.Items["render_widgets"].Tick(isActive);
PerfHistory.Items["terrain_lighting"].Tick(); PerfHistory.Items["render_flip"].Tick(isActive);
PerfHistory.Items["terrain_lighting"].Tick(isActive);
} }
static void Loop() static void Loop()
@@ -841,8 +862,13 @@ namespace OpenRA
var haveSomeTimeUntilNextLogic = now < nextLogic; var haveSomeTimeUntilNextLogic = now < nextLogic;
var isTimeToRender = now >= nextRender; var isTimeToRender = now >= nextRender;
if (!Renderer.WindowIsSuspended && ((isTimeToRender && haveSomeTimeUntilNextLogic) || forceRender)) if (!Renderer.WindowIsSuspended)
{ {
if (isTimeToRender || forceRender)
{
if (haveSomeTimeUntilNextLogic || forceRender)
RenderTick();
nextRender = now + renderInterval; nextRender = now + renderInterval;
// Pick the minimum allowed FPS (the lower between 'minReplayFPS' // Pick the minimum allowed FPS (the lower between 'minReplayFPS'
@@ -853,12 +879,13 @@ namespace OpenRA
var maxRenderInterval = Math.Max(1000 / MinReplayFps, renderInterval); var maxRenderInterval = Math.Max(1000 / MinReplayFps, renderInterval);
forcedNextRender = now + maxRenderInterval; forcedNextRender = now + maxRenderInterval;
RenderTick();
renderBeforeNextTick = false; renderBeforeNextTick = false;
} }
}
else
{
// Simulate a render tick if it was time to render but we skip actually rendering // Simulate a render tick if it was time to render but we skip actually rendering
if (Renderer.WindowIsSuspended && isTimeToRender) if (isTimeToRender || forceRender)
{ {
// Make sure that nextUpdate is set to a proper minimum interval // Make sure that nextUpdate is set to a proper minimum interval
nextRender = now + renderInterval; nextRender = now + renderInterval;
@@ -869,6 +896,12 @@ namespace OpenRA
// Ensure that we still logic tick despite not rendering // Ensure that we still logic tick despite not rendering
renderBeforeNextTick = false; renderBeforeNextTick = false;
} }
else
{
// Avoid busy wait.
Thread.Sleep((int)(nextRender - now));
}
}
} }
else else
Thread.Sleep((int)(nextUpdate - now)); Thread.Sleep((int)(nextUpdate - now));
@@ -947,7 +980,8 @@ namespace OpenRA
{ {
Name = "Skirmish Game", Name = "Skirmish Game",
Map = map, Map = map,
AdvertiseOnline = false AdvertiseOnline = false,
AdvertiseOnLocalNetwork = !isSkirmish
}; };
// Always connect to local games using the same loopback connection // Always connect to local games using the same loopback connection
@@ -985,7 +1019,7 @@ namespace OpenRA
Order.Command($"state {Session.ClientState.Ready}") Order.Command($"state {Session.ClientState.Ready}")
}; };
var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.PackageName) == launchMap); var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.Path) == launchMap);
if (map == null) if (map == null)
throw new ArgumentException($"Could not find map '{launchMap}'."); throw new ArgumentException($"Could not find map '{launchMap}'.");

View File

@@ -10,6 +10,7 @@
#endregion #endregion
using System; using System;
using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using OpenRA.Network; using OpenRA.Network;
@@ -39,17 +40,32 @@ namespace OpenRA
public TimeSpan Duration => EndTimeUtc > StartTimeUtc ? EndTimeUtc - StartTimeUtc : TimeSpan.Zero; public TimeSpan Duration => EndTimeUtc > StartTimeUtc ? EndTimeUtc - StartTimeUtc : TimeSpan.Zero;
public IList<Player> Players { get; } public IList<Player> Players { get; }
public HashSet<int> DisabledSpawnPoints = new(); public FrozenSet<int> DisabledSpawnPoints = FrozenSet<int>.Empty;
public MapPreview MapPreview => Game.ModData.MapCache[MapUid];
public IEnumerable<Player> HumanPlayers { get { return Players.Where(p => p.IsHuman); } } public IEnumerable<Player> HumanPlayers { get { return Players.Where(p => p.IsHuman); } }
public bool IsSinglePlayer => HumanPlayers.Count() == 1; public bool IsSinglePlayer => HumanPlayers.Count() == 1;
[FieldLoader.Ignore]
public MapGenerationArgs MapGenerationArgs;
readonly Dictionary<OpenRA.Player, Player> playersByRuntime; readonly Dictionary<OpenRA.Player, Player> playersByRuntime;
public GameInformation() public GameInformation()
{ {
Players = new List<Player>(); Players = [];
playersByRuntime = new Dictionary<OpenRA.Player, Player>(); playersByRuntime = [];
}
public MapPreview GetMapPreview(ModData modData)
{
var preview = modData.MapCache[MapUid];
if (preview.Status != MapStatus.Available && MapGenerationArgs != null)
{
preview.UpdateFromGenerationArgs(MapGenerationArgs);
preview.Generate();
}
return preview;
} }
public static GameInformation Deserialize(string data, string path) public static GameInformation Deserialize(string data, string path)
@@ -72,6 +88,10 @@ namespace OpenRA
case "Player": case "Player":
info.Players.Add(FieldLoader.Load<Player>(node.Value)); info.Players.Add(FieldLoader.Load<Player>(node.Value));
break; break;
case "MapGenerationArgs":
info.MapGenerationArgs = FieldLoader.Load<MapGenerationArgs>(node.Value);
break;
} }
} }
@@ -94,17 +114,18 @@ namespace OpenRA
for (var i = 0; i < Players.Count; i++) for (var i = 0; i < Players.Count; i++)
nodes.Add(new MiniYamlNode($"Player@{i}", FieldSaver.Save(Players[i]))); nodes.Add(new MiniYamlNode($"Player@{i}", FieldSaver.Save(Players[i])));
if (MapGenerationArgs != null)
nodes.Add(new MiniYamlNode("MapGenerationArgs", new MiniYaml("", MapGenerationArgs.Serialize())));
return nodes.WriteToString(); return nodes.WriteToString();
} }
/// <summary>Adds the player information at start-up.</summary> /// <summary>Adds the player information at start-up.</summary>
public void AddPlayer(OpenRA.Player runtimePlayer, Session lobbyInfo) public void AddPlayer(OpenRA.Player runtimePlayer, Session lobbyInfo)
{ {
if (runtimePlayer == null) ArgumentNullException.ThrowIfNull(runtimePlayer);
throw new ArgumentNullException(nameof(runtimePlayer));
if (lobbyInfo == null) ArgumentNullException.ThrowIfNull(lobbyInfo);
throw new ArgumentNullException(nameof(lobbyInfo));
// We don't care about spectators and map players // We don't care about spectators and map players
if (runtimePlayer.NonCombatant || !runtimePlayer.Playable) if (runtimePlayer.NonCombatant || !runtimePlayer.Playable)

View File

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

View File

@@ -11,6 +11,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using OpenRA.FileSystem; using OpenRA.FileSystem;
@@ -268,7 +269,7 @@ namespace OpenRA
if (mapRules.Value != null) if (mapRules.Value != null)
{ {
var mapFiles = FieldLoader.GetValue<string[]>("value", mapRules.Value); var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", mapRules.Value);
foreach (var f in mapFiles) foreach (var f in mapFiles)
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f))) if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
return true; return true;

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@
#endregion #endregion
using System; using System;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Support; using OpenRA.Support;
@@ -73,10 +74,10 @@ namespace OpenRA.Graphics
shadow, pos, offset - new WVec(0, 0, height), CurrentSequence.ShadowZOffset + zOffset + height, palette, shadow, pos, offset - new WVec(0, 0, height), CurrentSequence.ShadowZOffset + zOffset + height, palette,
CurrentSequence.Scale, 1f, float3.Ones, tintModifiers, CurrentSequence.Scale, 1f, float3.Ones, tintModifiers,
true, rotation); true, rotation);
return new IRenderable[] { shadowRenderable, imageRenderable }; return [shadowRenderable, imageRenderable];
} }
return new IRenderable[] { imageRenderable }; return [imageRenderable];
} }
public IRenderable[] RenderUI(WorldRenderer wr, int2 pos, in WVec offset, int zOffset, PaletteReference palette, float scale = 1f, float rotation = 0f) public IRenderable[] RenderUI(WorldRenderer wr, int2 pos, in WVec offset, int zOffset, PaletteReference palette, float scale = 1f, float rotation = 0f)
@@ -92,10 +93,10 @@ namespace OpenRA.Graphics
{ {
var shadowPos = pos - new int2((int)(scale * shadow.Size.X / 2), (int)(scale * shadow.Size.Y / 2)); var shadowPos = pos - new int2((int)(scale * shadow.Size.X / 2), (int)(scale * shadow.Size.Y / 2));
var shadowRenderable = new UISpriteRenderable(shadow, WPos.Zero + offset, shadowPos, CurrentSequence.ShadowZOffset + zOffset, palette, scale, 1f, rotation); var shadowRenderable = new UISpriteRenderable(shadow, WPos.Zero + offset, shadowPos, CurrentSequence.ShadowZOffset + zOffset, palette, scale, 1f, rotation);
return new IRenderable[] { shadowRenderable, imageRenderable }; return [shadowRenderable, imageRenderable];
} }
return new IRenderable[] { imageRenderable }; return [imageRenderable];
} }
public Rectangle ScreenBounds(WorldRenderer wr, WPos pos, in WVec offset) public Rectangle ScreenBounds(WorldRenderer wr, WPos pos, in WVec offset)
@@ -250,9 +251,9 @@ namespace OpenRA.Graphics
return sequences.GetSequence(Name, sequenceName); return sequences.GetSequence(Name, sequenceName);
} }
public string GetRandomExistingSequence(string[] sequences, MersenneTwister random) public string GetRandomExistingSequence(ImmutableArray<string> sequences, MersenneTwister random)
{ {
return sequences.Where(s => HasSequence(s)).RandomOrDefault(random); return sequences.Where(HasSequence).RandomOrDefault(random);
} }
} }
} }

View File

@@ -10,7 +10,9 @@
#endregion #endregion
using System; using System;
using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using OpenRA.FileSystem; using OpenRA.FileSystem;
using OpenRA.Primitives; using OpenRA.Primitives;
@@ -47,9 +49,9 @@ namespace OpenRA.Graphics
public readonly string Image2x = null; public readonly string Image2x = null;
public readonly string Image3x = null; public readonly string Image3x = null;
public readonly int[] PanelRegion = null; public readonly ImmutableArray<int> PanelRegion = default;
public readonly PanelSides PanelSides = PanelSides.All; public readonly PanelSides PanelSides = PanelSides.All;
public readonly Dictionary<string, Rectangle> Regions = new(); public readonly FrozenDictionary<string, Rectangle> Regions = FrozenDictionary<string, Rectangle>.Empty;
} }
public static IReadOnlyDictionary<string, Collection> Collections => collections; public static IReadOnlyDictionary<string, Collection> Collections => collections;
@@ -71,11 +73,11 @@ namespace OpenRA.Graphics
dpiScale = Game.Renderer.WindowScale; dpiScale = Game.Renderer.WindowScale;
fileSystem = modData.DefaultFileSystem; fileSystem = modData.DefaultFileSystem;
collections = new Dictionary<string, Collection>(); collections = [];
cachedSheets = new Dictionary<string, (Sheet, int)>(); cachedSheets = [];
cachedSprites = new Dictionary<string, Dictionary<string, Sprite>>(); cachedSprites = [];
cachedPanelSprites = new Dictionary<string, Sprite[]>(); cachedPanelSprites = [];
cachedCollectionSheets = new Dictionary<Collection, (Sheet, int)>(); cachedCollectionSheets = [];
var stringPool = new HashSet<string>(); // Reuse common strings in YAML var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var chrome = MiniYaml.Merge(modData.Manifest.Chrome var chrome = MiniYaml.Merge(modData.Manifest.Chrome
@@ -170,7 +172,7 @@ namespace OpenRA.Graphics
var sheetDensity = SheetForCollection(collection); var sheetDensity = SheetForCollection(collection);
if (cachedCollection == null) if (cachedCollection == null)
{ {
cachedCollection = new Dictionary<string, Sprite>(); cachedCollection = [];
cachedSprites.Add(collectionName, cachedCollection); cachedSprites.Add(collectionName, cachedCollection);
} }
@@ -240,11 +242,11 @@ namespace OpenRA.Graphics
// PERF: We don't need to search for images if there are no definitions. // PERF: We don't need to search for images if there are no definitions.
// PERF: It's more efficient to send an empty array rather than an array of 9 nulls. // PERF: It's more efficient to send an empty array rather than an array of 9 nulls.
if (collection.Regions.Count == 0) if (collection.Regions.Count == 0)
return Array.Empty<Sprite>(); return [];
// Support manual definitions for unusual dialog layouts // Support manual definitions for unusual dialog layouts
sprites = new[] sprites =
{ [
TryGetImage(collectionName, "corner-tl"), TryGetImage(collectionName, "corner-tl"),
TryGetImage(collectionName, "border-t"), TryGetImage(collectionName, "border-t"),
TryGetImage(collectionName, "corner-tr"), TryGetImage(collectionName, "corner-tr"),
@@ -254,7 +256,7 @@ namespace OpenRA.Graphics
TryGetImage(collectionName, "corner-bl"), TryGetImage(collectionName, "corner-bl"),
TryGetImage(collectionName, "border-b"), TryGetImage(collectionName, "border-b"),
TryGetImage(collectionName, "corner-br") TryGetImage(collectionName, "corner-br")
}; ];
} }
cachedPanelSprites.Add(collectionName, sprites); cachedPanelSprites.Add(collectionName, sprites);

View File

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

View File

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

View File

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

View File

@@ -21,11 +21,11 @@ namespace OpenRA.Graphics
public ITexture ColorShifts { get; } public ITexture ColorShifts { get; }
public int Height { get; private set; } public int Height { get; private set; }
readonly Dictionary<string, ImmutablePalette> palettes = new(); readonly Dictionary<string, ImmutablePalette> palettes = [];
readonly Dictionary<string, MutablePalette> mutablePalettes = new(); readonly Dictionary<string, MutablePalette> mutablePalettes = [];
readonly Dictionary<string, int> indices = new(); readonly Dictionary<string, int> indices = [];
byte[] buffer = Array.Empty<byte>(); byte[] buffer = [];
float[] colorShiftBuffer = Array.Empty<float>(); float[] colorShiftBuffer = [];
public HardwarePalette() public HardwarePalette()
{ {

View File

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

View File

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

View File

@@ -11,6 +11,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO; using System.IO;
using OpenRA.Primitives; using OpenRA.Primitives;
@@ -64,18 +65,18 @@ namespace OpenRA.Graphics
Buffer.BlockCopy(colors, 0, destination, destinationOffset * 4, Palette.Size * 4); Buffer.BlockCopy(colors, 0, destination, destinationOffset * 4, Palette.Size * 4);
} }
public ImmutablePalette(string filename, int[] remapTransparent, int[] remap) public ImmutablePalette(string filename, ImmutableArray<int> remapTransparent, ImmutableArray<int> remap)
{ {
using (var s = File.OpenRead(filename)) using (var s = File.OpenRead(filename))
LoadFromStream(s, remapTransparent, remap); LoadFromStream(s, remapTransparent, remap);
} }
public ImmutablePalette(Stream s, int[] remapTransparent, int[] remapShadow) public ImmutablePalette(Stream s, ImmutableArray<int> remapTransparent, ImmutableArray<int> remapShadow)
{ {
LoadFromStream(s, remapTransparent, remapShadow); LoadFromStream(s, remapTransparent, remapShadow);
} }
void LoadFromStream(Stream s, int[] remapTransparent, int[] remapShadow) void LoadFromStream(Stream s, ImmutableArray<int> remapTransparent, ImmutableArray<int> remapShadow)
{ {
using (var reader = new BinaryReader(s)) using (var reader = new BinaryReader(s))
for (var i = 0; i < Palette.Size; i++) for (var i = 0; i < Palette.Size; i++)

View File

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

View File

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

View File

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

View File

@@ -191,7 +191,7 @@ namespace OpenRA.Graphics
{ {
var tr = new float3(br.X, tl.Y, tl.Z); var tr = new float3(br.X, tl.Y, tl.Z);
var bl = new float3(tl.X, br.Y, br.Z); var bl = new float3(tl.X, br.Y, br.Z);
DrawPolygon(new[] { tl, tr, br, bl }, width, color, blendMode); DrawPolygon([tl, tr, br, bl], width, color, blendMode);
} }
public void FillRect(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha) public void FillRect(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,25 +23,7 @@ namespace OpenRA
} }
public enum MouseInputEvent { Down, Move, Up, Scroll } public enum MouseInputEvent { Down, Move, Up, Scroll }
public struct MouseInput public record struct MouseInput(MouseInputEvent Event, MouseButton Button, int2 Location, int2 Delta, Modifiers Modifiers, int MultiTapCount);
{
public MouseInputEvent Event;
public MouseButton Button;
public int2 Location;
public int2 Delta;
public Modifiers Modifiers;
public int MultiTapCount;
public MouseInput(MouseInputEvent ev, MouseButton button, int2 location, int2 delta, Modifiers mods, int multiTapCount)
{
Event = ev;
Button = button;
Location = location;
Delta = delta;
Modifiers = mods;
MultiTapCount = multiTapCount;
}
}
[Flags] [Flags]
public enum MouseButton public enum MouseButton
@@ -65,7 +47,7 @@ namespace OpenRA
public static class ModifiersExts public static class ModifiersExts
{ {
[FluentReference] [FluentReference]
public const string Cmd = "keycode-modifier.cmd"; const string Cmd = "keycode-modifier.cmd";
[FluentReference(Traits.LintDictionaryReference.Values)] [FluentReference(Traits.LintDictionaryReference.Values)]
public static readonly IReadOnlyDictionary<Modifiers, string> ModifierFluentKeys = new Dictionary<Modifiers, string>() public static readonly IReadOnlyDictionary<Modifiers, string> ModifierFluentKeys = new Dictionary<Modifiers, string>()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,58 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using OpenRA.Primitives;
namespace OpenRA
{
public class MapGenerationArgs
{
[FieldLoader.Require]
public string Uid = null;
[FieldLoader.Require]
public string Generator = null;
[FieldLoader.Require]
public string Tileset = null;
[FieldLoader.Require]
public Size Size = default;
[FieldLoader.Require]
public string Title = null;
[FieldLoader.Require]
public string Author = null;
[FieldLoader.LoadUsing(nameof(LoadSettings))]
public MiniYaml Settings = null;
static MiniYaml LoadSettings(MiniYaml yaml)
{
return yaml.NodeWithKey("Settings").Value;
}
public List<MiniYamlNode> Serialize()
{
return
[
new("Uid", Uid),
new("Generator", Generator),
new("Tileset", Tileset),
new("Size", FieldSaver.FormatValue(Size)),
new("Settings", Settings),
new("Title", Title),
new("Author", Author)
];
}
}
}

View File

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

View File

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

View File

@@ -11,6 +11,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -23,10 +24,11 @@ using OpenRA.FileSystem;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Support; using OpenRA.Support;
using OpenRA.Traits;
namespace OpenRA namespace OpenRA
{ {
public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError } public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError, Generatable, Generating }
// Used for grouping maps in the UI // Used for grouping maps in the UI
[Flags] [Flags]
@@ -35,7 +37,8 @@ namespace OpenRA
Unknown = 0, Unknown = 0,
System = 1, System = 1,
User = 2, User = 2,
Remote = 4 Remote = 4,
Generated = 8
} }
[SuppressMessage("StyleCop.CSharp.NamingRules", [SuppressMessage("StyleCop.CSharp.NamingRules",
@@ -48,10 +51,10 @@ namespace OpenRA
{ {
public readonly string title; public readonly string title;
public readonly string author; public readonly string author;
public readonly string[] categories; public readonly ImmutableArray<string> categories;
public readonly int players; public readonly int players;
public readonly Rectangle bounds; public readonly Rectangle bounds;
public readonly short[] spawnpoints = Array.Empty<short>(); public readonly ImmutableArray<short> spawnpoints = [];
public readonly MapGridType map_grid_type; public readonly MapGridType map_grid_type;
public readonly string minimap; public readonly string minimap;
public readonly bool downloading; public readonly bool downloading;
@@ -69,12 +72,12 @@ namespace OpenRA
{ {
public int MapFormat; public int MapFormat;
public string Title; public string Title;
public string[] Categories; public ImmutableArray<string> Categories;
public string Author; public string Author;
public string TileSet; public string TileSet;
public MapPlayers Players; public MapPlayers Players;
public int PlayerCount; public int PlayerCount;
public CPos[] SpawnPoints; public ImmutableArray<CPos> SpawnPoints;
public MapGridType GridType; public MapGridType GridType;
public Rectangle Bounds; public Rectangle Bounds;
public Png Preview; public Png Preview;
@@ -82,6 +85,7 @@ namespace OpenRA
public MapClassification Class; public MapClassification Class;
public MapVisibility Visibility; public MapVisibility Visibility;
public DateTime ModifiedDate; public DateTime ModifiedDate;
public MapGenerationArgs GenerationArgs;
public MiniYaml RuleDefinitions; public MiniYaml RuleDefinitions;
public MiniYaml WeaponDefinitions; public MiniYaml WeaponDefinitions;
@@ -113,7 +117,7 @@ namespace OpenRA
return key == "world" || key == "player"; return key == "world" || key == "player";
} }
public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary<string, MiniYaml> yaml, IEnumerable<List<MiniYamlNode>> modDataRules) public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary<string, MiniYaml> yaml, MiniYamlNode[][] modDataRules)
{ {
RuleDefinitions = LoadRuleSection(yaml, "Rules"); RuleDefinitions = LoadRuleSection(yaml, "Rules");
WeaponDefinitions = LoadRuleSection(yaml, "Weapons"); WeaponDefinitions = LoadRuleSection(yaml, "Weapons");
@@ -128,9 +132,9 @@ namespace OpenRA
{ {
if (FluentMessageDefinitions != null) if (FluentMessageDefinitions != null)
{ {
var files = Array.Empty<string>(); var files = ImmutableArray<string>.Empty;
if (FluentMessageDefinitions.Value != null) if (FluentMessageDefinitions.Value != null)
files = FieldLoader.GetValue<string[]>("value", FluentMessageDefinitions.Value); files = FieldLoader.GetValue<ImmutableArray<string>>("value", FluentMessageDefinitions.Value);
string text = null; string text = null;
if (FluentMessageDefinitions.Nodes.Length > 0) if (FluentMessageDefinitions.Nodes.Length > 0)
@@ -156,8 +160,8 @@ namespace OpenRA
var files = Enumerable.Empty<string>(); var files = Enumerable.Empty<string>();
if (RuleDefinitions.Value != null) if (RuleDefinitions.Value != null)
{ {
var mapFiles = FieldLoader.GetValue<string[]>("value", RuleDefinitions.Value); var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", RuleDefinitions.Value);
files = files.Append(mapFiles); files = files.Concat(mapFiles);
} }
var stringPool = new HashSet<string>(); // Reuse common strings in YAML var stringPool = new HashSet<string>(); // Reuse common strings in YAML
@@ -195,22 +199,26 @@ namespace OpenRA
} }
} }
static readonly CPos[] NoSpawns = Array.Empty<CPos>(); readonly object syncRoot = new();
readonly MapCache cache; readonly MapCache cache;
readonly ModData modData; readonly ModData modData;
IReadOnlyPackage package;
public readonly string Uid; public readonly string Uid;
public string PackageName { get; private set; }
IReadOnlyPackage package; public string Path { get; private set; }
public IReadOnlyPackage Package
void LoadPackage()
{ {
get if (package == null && parentPackage != null)
{ package = parentPackage.OpenPackage(Path, modData.ModFiles);
package ??= parentPackage.OpenPackage(PackageName, modData.ModFiles);
return package;
} }
private set => package = value; public Map ToMap()
{
LoadPackage();
using (new PerfTimer("Map"))
return new Map(modData, package);
} }
IReadOnlyPackage parentPackage; IReadOnlyPackage parentPackage;
@@ -219,12 +227,12 @@ namespace OpenRA
public int MapFormat => innerData.MapFormat; public int MapFormat => innerData.MapFormat;
public string Title => innerData.Title; public string Title => innerData.Title;
public string[] Categories => innerData.Categories; public ImmutableArray<string> Categories => innerData.Categories;
public string Author => innerData.Author; public string Author => innerData.Author;
public string TileSet => innerData.TileSet; public string TileSet => innerData.TileSet;
public MapPlayers Players => innerData.Players; public MapPlayers Players => innerData.Players;
public int PlayerCount => innerData.PlayerCount; public int PlayerCount => innerData.PlayerCount;
public CPos[] SpawnPoints => innerData.SpawnPoints; public ImmutableArray<CPos> SpawnPoints => innerData.SpawnPoints;
public MapGridType GridType => innerData.GridType; public MapGridType GridType => innerData.GridType;
public Rectangle Bounds => innerData.Bounds; public Rectangle Bounds => innerData.Bounds;
public Png Preview => innerData.Preview; public Png Preview => innerData.Preview;
@@ -240,6 +248,8 @@ namespace OpenRA
public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo; public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo;
public DateTime ModifiedDate => innerData.ModifiedDate; public DateTime ModifiedDate => innerData.ModifiedDate;
public MapGenerationArgs GenerationArgs => innerData.GenerationArgs;
public long DownloadBytes { get; private set; } public long DownloadBytes { get; private set; }
public int DownloadPercentage { get; private set; } public int DownloadPercentage { get; private set; }
@@ -317,12 +327,12 @@ namespace OpenRA
{ {
MapFormat = 0, MapFormat = 0,
Title = "Unknown Map", Title = "Unknown Map",
Categories = new[] { "Unknown" }, Categories = ["Unknown"],
Author = "Unknown Author", Author = "Unknown Author",
TileSet = "unknown", TileSet = "unknown",
Players = null, Players = null,
PlayerCount = 0, PlayerCount = 0,
SpawnPoints = NoSpawns, SpawnPoints = [],
GridType = gridType, GridType = gridType,
Bounds = Rectangle.Empty, Bounds = Rectangle.Empty,
Preview = null, Preview = null,
@@ -332,57 +342,28 @@ namespace OpenRA
}; };
} }
// For linting purposes only! /// <summary>
public MapPreview(Map map, ModData modData) /// Updates internal state from a map without taking ownership of its package.
/// A new copy of the map package will be opened lazily when needed.
/// </summary>
public void UpdateFromMapWithoutOwningPackage(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification,
MapGridType? gridType = null, MiniYamlNode[][] modDataRules = null)
{ {
this.modData = modData; UpdateFromMap(p, classification, gridType, modDataRules);
cache = modData.MapCache; parentPackage = parent;
package = null;
Uid = map.Uid;
PackageName = map.Package.Name;
var mapPlayers = new MapPlayers(map.PlayerDefinitions);
var spawns = new List<CPos>();
foreach (var kv in map.ActorDefinitions.Where(d => d.Value.Value == "mpspawn"))
{
var s = new ActorReference(kv.Value.Value, kv.Value.ToDictionary());
spawns.Add(s.Get<LocationInit>().Value);
} }
innerData = new InnerData /// <summary>
/// Updates internal state from a map and takes ownership of its package.
/// The package remains in memory and must not be disposed.
/// </summary>
public void UpdateFromMap(IReadOnlyPackage p, MapClassification classification,
MapGridType? gridType = null, MiniYamlNode[][] modDataRules = null)
{ {
MapFormat = map.MapFormat, Path = p.Name;
Title = map.Title, package = p;
Categories = map.Categories,
Author = map.Author,
TileSet = map.Tileset,
Players = mapPlayers,
PlayerCount = mapPlayers.Players.Count(x => x.Value.Playable),
SpawnPoints = spawns.ToArray(),
GridType = map.Grid.Type,
Bounds = map.Bounds,
Preview = null,
Status = MapStatus.Available,
Class = MapClassification.Unknown,
Visibility = map.Visibility,
};
innerData.SetCustomRules(modData, this, new Dictionary<string, MiniYaml>()
{
{ "Rules", map.RuleDefinitions },
{ "FluentMessages", map.FluentMessageDefinitions },
{ "Weapons", map.WeaponDefinitions },
{ "Voices", map.VoiceDefinitions },
{ "Music", map.MusicDefinitions },
{ "Notifications", map.NotificationDefinitions },
{ "Sequences", map.SequenceDefinitions },
{ "ModelSequences", map.ModelSequenceDefinitions }
}, null);
}
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification,
string[] mapCompatibility, MapGridType gridType, IEnumerable<List<MiniYamlNode>> modDataRules)
{
Dictionary<string, MiniYaml> yaml; Dictionary<string, MiniYaml> yaml;
using (var yamlStream = p.GetStream("map.yaml")) using (var yamlStream = p.GetStream("map.yaml"))
{ {
@@ -392,12 +373,9 @@ namespace OpenRA
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, $"{p.Name}:map.yaml", stringPool: cache.StringPool)).ToDictionary(); yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, $"{p.Name}:map.yaml", stringPool: cache.StringPool)).ToDictionary();
} }
PackageName = p.Name;
parentPackage = parent;
var newData = innerData.Clone(); var newData = innerData.Clone();
newData.GridType = gridType;
newData.Class = classification; newData.Class = classification;
newData.GridType = gridType ?? modData.GetOrCreate<MapGrid>().Type;
if (yaml.TryGetValue("MapFormat", out var temp)) if (yaml.TryGetValue("MapFormat", out var temp))
{ {
@@ -410,7 +388,7 @@ namespace OpenRA
newData.Title = temp.Value; newData.Title = temp.Value;
if (yaml.TryGetValue("Categories", out temp)) if (yaml.TryGetValue("Categories", out temp))
newData.Categories = FieldLoader.GetValue<string[]>("Categories", temp.Value); newData.Categories = FieldLoader.GetValue<ImmutableArray<string>>("Categories", temp.Value);
if (yaml.TryGetValue("Tileset", out temp)) if (yaml.TryGetValue("Tileset", out temp))
newData.TileSet = temp.Value; newData.TileSet = temp.Value;
@@ -431,7 +409,7 @@ namespace OpenRA
if (yaml.TryGetValue("MapFormat", out temp)) if (yaml.TryGetValue("MapFormat", out temp))
newData.MapFormat = FieldLoader.GetValue<int>("MapFormat", temp.Value); newData.MapFormat = FieldLoader.GetValue<int>("MapFormat", temp.Value);
newData.Status = mapCompatibility == null || mapCompatibility.Contains(requiresMod) ? newData.Status = modData.Manifest.MapCompatibility.Contains(requiresMod) ?
MapStatus.Available : MapStatus.Unavailable; MapStatus.Available : MapStatus.Unavailable;
try try
@@ -442,18 +420,18 @@ namespace OpenRA
var spawns = new List<CPos>(); var spawns = new List<CPos>();
foreach (var kv in actorDefinitions.Nodes.Where(d => d.Value.Value == "mpspawn")) foreach (var kv in actorDefinitions.Nodes.Where(d => d.Value.Value == "mpspawn"))
{ {
var s = new ActorReference(kv.Value.Value, kv.Value.ToDictionary()); var s = new ActorReference(kv.Value.Value, kv.Value);
spawns.Add(s.Get<LocationInit>().Value); spawns.Add(s.Get<LocationInit>().Value);
} }
newData.SpawnPoints = spawns.ToArray(); newData.SpawnPoints = spawns.ToImmutableArray();
} }
else else
newData.SpawnPoints = Array.Empty<CPos>(); newData.SpawnPoints = [];
} }
catch (Exception) catch (Exception)
{ {
newData.SpawnPoints = Array.Empty<CPos>(); newData.SpawnPoints = [];
newData.Status = MapStatus.Unavailable; newData.Status = MapStatus.Unavailable;
} }
@@ -477,31 +455,122 @@ namespace OpenRA
using (var dataStream = p.GetStream("map.png")) using (var dataStream = p.GetStream("map.png"))
newData.Preview = new Png(dataStream); newData.Preview = new Png(dataStream);
newData.ModifiedDate = File.GetLastWriteTime(p.Name); newData.ModifiedDate = p.Name != null ? File.GetLastWriteTime(p.Name) : DateTime.Now;
// Assign the new data atomically // Assign the new data atomically
// Local maps have higher precedence than remote/generated maps,
// so should always replace their metadata
lock (syncRoot)
innerData = newData; innerData = newData;
} }
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, string[] mapCompatibility, Action<MapPreview> parseMetadata = null) public void UpdateFromGenerationArgs(MapGenerationArgs args)
{ {
var newData = innerData.Clone(); var newData = innerData.Clone();
newData.Status = status; newData.Class = MapClassification.Generated;
newData.Class = MapClassification.Remote; newData.Status = MapStatus.Generatable;
newData.Title = args.Title;
newData.Author = args.Author;
newData.TileSet = args.Tileset;
newData.GenerationArgs = args;
newData.MapFormat = Map.CurrentMapFormat;
if (status == MapStatus.DownloadAvailable) try
{
var generator = modData.DefaultRules.Actors[SystemActors.EditorWorld]
.TraitInfos<IMapGeneratorInfo>()
.FirstOrDefault(info => info.Type == args.Generator);
if (generator == null)
throw new Exception($"Unknown map generator type {args.Generator}");
if (!generator.TryGenerateMetadata(modData, args, out var players, out var ruleDefinitions))
throw new Exception("Failed to generate map metadata");
newData.Players = players;
newData.PlayerCount = newData.Players.Players.Count(x => x.Value.Playable);
newData.SetCustomRules(modData, this, ruleDefinitions, null);
// Placeholder to satisfy server-side lint checks
newData.SpawnPoints = Exts.MakeArray(newData.PlayerCount, i => new CPos(i, i)).ToImmutableArray();
}
catch (Exception e)
{
Log.Write("debug", "Map generation failed with error:");
Log.Write("debug", e);
newData.Status = MapStatus.Unavailable;
}
lock (syncRoot)
innerData = newData;
}
public void Generate()
{
if (Class != MapClassification.Generated || Status != MapStatus.Generatable)
return;
lock (syncRoot)
innerData.Status = MapStatus.Generating;
Task.Run(() =>
{
try
{
var generator = modData.DefaultRules.Actors[SystemActors.EditorWorld]
.TraitInfos<IMapGeneratorInfo>()
.FirstOrDefault(info => info.Type == GenerationArgs.Generator);
if (generator == null)
throw new Exception($"Unknown map generator type {GenerationArgs.Generator}");
var map = generator.Generate(modData, GenerationArgs);
// Uid is generated when the map is saved
map.Save(new ZipFileLoader.ReadWriteZipFile());
if (map.Uid != GenerationArgs.Uid)
throw new InvalidOperationException("Map generation UID mismatch");
Game.RunAfterTick(() => UpdateFromMap(map.Package, MapClassification.Generated));
}
catch (Exception e)
{
Log.Write("debug", "Map generation failed with error:");
Log.Write("debug", e);
lock (syncRoot)
innerData.Status = MapStatus.Unavailable;
}
});
}
public void BeginRemoteSearch()
{
var newData = innerData.Clone();
newData.Class = MapClassification.Remote;
newData.Status = MapStatus.Searching;
// We may have been resolved to a local/generated map by another
// async task. Make sure we don't stomp over their state!
lock (syncRoot)
if (innerData.Class == MapClassification.Unknown || innerData.Class == MapClassification.Remote)
innerData = newData;
}
public void CompleteRemoteSearch(MiniYaml yaml, Action<MapPreview> parseMetadata = null)
{
var newData = innerData.Clone();
newData.Class = MapClassification.Remote;
newData.Status = MapStatus.Unavailable;
if (yaml != null)
{ {
try try
{ {
var r = FieldLoader.Load<RemoteMapData>(yaml); var r = FieldLoader.Load<RemoteMapData>(yaml);
newData.Status = r.downloading ? MapStatus.DownloadAvailable : MapStatus.Unavailable;
// Map download has been disabled server side
if (!r.downloading)
{
newData.Status = MapStatus.Unavailable;
return;
}
newData.Title = r.title; newData.Title = r.title;
newData.Categories = r.categories; newData.Categories = r.categories;
newData.Author = r.author; newData.Author = r.author;
@@ -513,7 +582,7 @@ namespace OpenRA
var spawns = new CPos[r.spawnpoints.Length / 2]; var spawns = new CPos[r.spawnpoints.Length / 2];
for (var j = 0; j < r.spawnpoints.Length; j += 2) for (var j = 0; j < r.spawnpoints.Length; j += 2)
spawns[j / 2] = new CPos(r.spawnpoints[j], r.spawnpoints[j + 1]); spawns[j / 2] = new CPos(r.spawnpoints[j], r.spawnpoints[j + 1]);
newData.SpawnPoints = spawns; newData.SpawnPoints = spawns.ToImmutableArray();
newData.GridType = r.map_grid_type; newData.GridType = r.map_grid_type;
if (cache.LoadPreviewImages) if (cache.LoadPreviewImages)
{ {
@@ -541,29 +610,37 @@ namespace OpenRA
// Map is for a different mod: update its information so it can be displayed // Map is for a different mod: update its information so it can be displayed
// in the cross-mod server browser UI, but mark it as unavailable so it can't // in the cross-mod server browser UI, but mark it as unavailable so it can't
// be selected in a server for the current mod. // be selected in a server for the current mod.
if (!mapCompatibility.Contains(r.game_mod)) if (!modData.Manifest.MapCompatibility.Contains(r.game_mod))
newData.Status = MapStatus.Unavailable; newData.Status = MapStatus.Unavailable;
} }
catch (Exception e) catch (Exception e)
{ {
Log.Write("debug", "Failed parsing mapserver response:"); Log.Write("debug", "Failed parsing mapserver response:");
Log.Write("debug", e); Log.Write("debug", e);
newData.Status = MapStatus.Unavailable;
}
} }
// Commit updated data before running the callbacks // We may have been resolved to a local/generated map by another
// async task. Make sure we don't stomp over their state!
MapClassification mapClassification;
lock (syncRoot)
{
mapClassification = innerData.Class;
if (mapClassification == MapClassification.Remote)
innerData = newData; innerData = newData;
}
if (mapClassification == MapClassification.Remote)
{
if (innerData.Preview != null) if (innerData.Preview != null)
cache.CacheMinimap(this); cache.CacheMinimap(this);
parseMetadata?.Invoke(this); parseMetadata?.Invoke(this);
} }
// Update the status and class unconditionally
innerData = newData;
} }
public void Install(string mapRepositoryUrl, Action onSuccess) public void Install(string mapRepositoryUrl)
{ {
if ((Status != MapStatus.DownloadError && Status != MapStatus.DownloadAvailable) || !Game.Settings.Game.AllowDownloading) if ((Status != MapStatus.DownloadError && Status != MapStatus.DownloadAvailable) || !Game.Settings.Game.AllowDownloading)
return; return;
@@ -616,14 +693,11 @@ namespace OpenRA
mapInstallPackage.Update(mapFilename, fileStream.ToArray()); mapInstallPackage.Update(mapFilename, fileStream.ToArray());
Log.Write("debug", $"Downloaded map to '{mapFilename}'"); Log.Write("debug", $"Downloaded map to '{mapFilename}'");
var package = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles); var p = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles);
if (package == null) if (p == null)
innerData.Status = MapStatus.DownloadError; innerData.Status = MapStatus.DownloadError;
else else
{ UpdateFromMapWithoutOwningPackage(p, mapInstallPackage, MapClassification.User, GridType);
UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType, null);
Game.RunAfterTick(onSuccess);
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -636,34 +710,31 @@ namespace OpenRA
public void Invalidate() public void Invalidate()
{ {
lock (syncRoot)
{
innerData.Class = MapClassification.Unknown;
innerData.Status = MapStatus.Unavailable; innerData.Status = MapStatus.Unavailable;
} }
}
public void Dispose() public void Dispose()
{ {
DisposePackage(); package?.Dispose();
}
public void DisposePackage()
{
if (package != null)
{
package.Dispose();
package = null; package = null;
} }
}
public void Delete() public void Delete()
{ {
Invalidate(); Invalidate();
(parentPackage as IReadWritePackage)?.Delete(Package.Name); (parentPackage as IReadWritePackage)?.Delete(Path);
} }
Stream IReadOnlyFileSystem.Open(string filename) Stream IReadOnlyFileSystem.Open(string filename)
{ {
// Explicit package paths never refer to a map // Explicit package paths never refer to a map
if (!filename.Contains('|') && Package.Contains(filename)) LoadPackage();
return Package.GetStream(filename); if (!filename.Contains('|') && package.Contains(filename))
return package.GetStream(filename);
return modData.DefaultFileSystem.Open(filename); return modData.DefaultFileSystem.Open(filename);
} }
@@ -679,7 +750,8 @@ namespace OpenRA
// Explicit package paths never refer to a map // Explicit package paths never refer to a map
if (!filename.Contains('|')) if (!filename.Contains('|'))
{ {
s = Package.GetStream(filename); LoadPackage();
s = package.GetStream(filename);
if (s != null) if (s != null)
return true; return true;
} }
@@ -690,7 +762,8 @@ namespace OpenRA
bool IReadOnlyFileSystem.Exists(string filename) bool IReadOnlyFileSystem.Exists(string filename)
{ {
// Explicit package paths never refer to a map // Explicit package paths never refer to a map
if (!filename.Contains('|') && Package.Contains(filename)) LoadPackage();
if (!filename.Contains('|') && package.Contains(filename))
return true; return true;
return modData.DefaultFileSystem.Exists(filename); return modData.DefaultFileSystem.Exists(filename);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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