Compare commits

...

1239 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
Gustas
0eb173e046 Install missing linux utilities 2024-11-16 18:39:22 +00:00
Gustas
7d9aa58103 Update SDL 2024-11-16 19:55:12 +02:00
RoosterDragon
874fa67036 Restore threaded renderer on Windows in windowed mode.
Previously, when running in windowed-mode on Windows, the threaded renderer would cause the minimize/restore behaviour to become unreliable. It was disabled under this configuration to avoid the problem.

This no longer occurs (perhaps fixed by an intervening SDL update?), so we can remove the workaround and always use the threaded renderer for a performance improvement.
2024-11-16 19:18:36 +02:00
RoosterDragon
bf7f81b7fe Improve some String.Split calls. 2024-11-15 22:28:18 +02:00
RoosterDragon
cf7f57252e Fix CA2265 2024-11-15 22:28:18 +02:00
RoosterDragon
e2296ad2d1 Fix CA2263 2024-11-15 22:28:18 +02:00
RoosterDragon
332ab244a7 Fix CA1862 2024-11-15 22:28:18 +02:00
RoosterDragon
ed90322a0b Fix IDE0032 2024-11-15 22:28:18 +02:00
RoosterDragon
9809f6ed08 Add new .NET 9 rules to editorconfig.
Don't enforce all the rules yet, since we are still targeting .NET 6.

Adjust cast to nullable in UnloadCargo.ChooseExitSubCell. Avoid this cast tripping up CA2021 on older NET SDK versions.
2024-11-15 22:28:18 +02:00
Ashley Newson
7401182a1b Refactor editor clipboard logic as blitting logic
- Refactors internal editor clipboard logic into reusable map contents
  "Blitting" functionality.
- Fix actor processing being unnecessarily (cell) looped within
  CopySelectionContents (now CopyRegionContents).
- Deduplicates largely repeated code.
- Minor code simplifications and renames.
2024-11-15 19:18:38 +02:00
RoosterDragon
2c68964566 Fix RA mod content.
Regression from 147cb566f2.
2024-11-03 16:11:40 +00:00
Gustas
c639905119 Add BI 4.3 balance 2024-11-03 15:29:47 +00:00
Paul Chote
9bcf1b3295 Overhaul TD EVA Database. 2024-11-03 17:19:30 +02:00
Paul Chote
feef5be143 Allow mods to override Encyclopedia preview owners. 2024-11-03 17:19:30 +02:00
Paul Chote
f805d67741 Allow mods to display Encyclopedia production info. 2024-11-03 17:19:30 +02:00
Paul Chote
497251c0eb Allow mods to display Encyclopedia portraits. 2024-11-03 17:19:30 +02:00
Paul Chote
c111cea7fc Allow mods to display Encyclopedia titles in description panel. 2024-11-03 17:19:30 +02:00
Paul Chote
eff4c66410 Allow Encyclopedia entries to scale the actor preview. 2024-11-03 17:19:30 +02:00
abcdefg30
78e47ea70a Support using player palettes as effect palette for SpawnActorPower 2024-11-03 17:02:00 +02:00
Paul Chote
147cb566f2 Remove "en" from fluent file paths. 2024-11-03 16:52:47 +02:00
Paul Chote
8452f71481 Rename Lua UserInterface.Translate to .GetFluentMessage. 2024-11-03 16:52:47 +02:00
Paul Chote
8b11b499ed Rename Translations yaml keys to FluentMessages. 2024-11-03 16:52:47 +02:00
Paul Chote
ecaa414508 Simplify method name in CheckFluentReferences. 2024-11-03 16:52:47 +02:00
Paul Chote
c09d7cbdea Remove magic ftl file naming. 2024-11-03 16:52:47 +02:00
Paul Chote
5a0c8439fc Add map support for inline base64 fluent messages.
This enables the RC to parse and share custom messages
as part of the map's custom rules without any additional
API changes.
2024-11-03 16:52:47 +02:00
Paul Chote
43219e16da Reorganize FluentBundle ctors to allow inline text. 2024-11-03 16:52:47 +02:00
Paul Chote
e4539e9cb5 Use nameof() when reflecting over Map field names. 2024-11-03 16:52:47 +02:00
Paul Chote
67254e0b39 Rename Fluent *GetString methods to GetMessage. 2024-11-03 16:52:47 +02:00
Paul Chote
09063d23da Determine pixel-to-texel ratio for each sprite individually.
This fixes rendering artifacts when sprites are scaled > 1.
2024-11-03 16:38:32 +02:00
DrSheppard
42989c74aa delete libfuse redundand installation 2024-11-01 12:22:04 +00:00
DrSheppard
637faac90e use appimagetool with non-glibc linux support 2024-11-01 12:22:04 +00:00
Gustas
065040e5b8 Replace modcontent 2024-10-25 11:42:33 +01:00
Gustas
b62c883951 Localise lobby notification 2024-10-23 19:08:12 +03:00
test1232156
63e8835fa0 Miscellaneous lua translation fixups
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
2024-10-23 19:08:12 +03:00
test1232156
70f62dba4b Fix spelling errors and add naming consistency in en.ftl files
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
2024-10-23 19:08:12 +03:00
test1232156
564da2fb8d Fix prerequisites spelling error
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
2024-10-23 19:08:12 +03:00
Paul Chote
c6af758ee5 Fix Makefile "version" rule. 2024-10-20 16:56:04 +03:00
Paul Chote
b57be1cc08 Rework mod content installation. 2024-10-20 15:04:25 +03:00
Paul Chote
c84d088dfa Fix lint test. 2024-10-20 15:04:25 +03:00
test1232156
631cf4ba74 Fix spelling errors and add naming consistency to campaign
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
2024-10-18 18:48:37 +03:00
test1232156
02e1c7444b Mission briefing rewording and fact checking
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
2024-10-18 18:48:37 +03:00
Gustas
60a752cdef Rename TS defense to support 2024-10-17 17:18:14 +01:00
Gustas
48a2e5c7a1 Rename TD defense to support 2024-10-17 17:18:14 +01:00
Gustas
52a605787b Fix defense spelling 2024-10-17 17:18:14 +01:00
Gustas
f43d2fb98e Fix lua failing to translate strings with arguments 2024-10-17 17:16:19 +01:00
Gustas
fb55764887 Fix database blocking the load button 2024-10-17 17:12:18 +01:00
Gustas
346d267e26 Fix common mission objective panel not filling the background uniformly 2024-10-17 17:12:18 +01:00
Gustas
8cd08e6a58 Make briefing less glitchy by not changing panel type before new briefing has been loaded 2024-10-17 17:12:18 +01:00
Gustas
53218414f8 Extract translations from modcontent chrome 2024-10-17 17:10:57 +01:00
RoosterDragon
ef1390bbdf Improve the context when logging missing translation fields. 2024-10-17 15:10:59 +03:00
RoosterDragon
6bd64677ec Use short context names in hotkey yaml, generate translation key at runtime. 2024-10-17 15:10:59 +03:00
RoosterDragon
559a76be8d Lowercase all hotkey-description- translation keys. 2024-10-17 15:10:59 +03:00
RoosterDragon
6f6fb5b393 Expose hotkeys to localisation.
Allows the Settings > Hotkeys screen to be localised, including hotkey decriptions, groups and contexts.

The hotkey names are exposed to localisation via KeycodeExts. Hotkey modifiers are similarly exposed via ModifersExts.

The Settings > Input screen has a Zoom Modifier dropdown, which shows the localised modifier name.

The --check-yaml utility command is taught to recognise all hotkey translation, so it can validate their usage.
2024-10-17 15:10:59 +03:00
Paul Chote
10856ccfd0 Fix weather overlay viewport size for spectators. 2024-10-17 14:27:31 +03:00
Paul Chote
8f14dd6113 Fix crash when a sequences defines both Alpha and Reverses. 2024-10-15 10:53:05 +03:00
Ivaylo Draganov
791595e3ae Extract player profile loading chrome logic to a dedicated class 2024-10-15 10:14:32 +03:00
Ivaylo Draganov
ca3ab78cf8 Rename file to match class name and extract other classes out of it for readability 2024-10-15 10:14:32 +03:00
Gustas
67855f2adf Don't crash if 2 DockHosts overlap 2024-10-13 14:35:59 +01:00
Gustas
f06d7d29ef Add support for not displaying target lines 2024-10-13 14:35:59 +01:00
Gustas
b72d4ab7c6 Fix clients acquiring hosts on order creations 2024-10-13 14:35:59 +01:00
Gustas
d22bdbe944 Only allow docking to allied refineries if directly ordered 2024-10-13 14:35:59 +01:00
Gustas
d24533d561 Simplify IDockHost interface 2024-10-13 14:35:59 +01:00
Gustas
1334575ba9 Add RequireForceMoveCondition to DockClientManager 2024-10-13 14:35:59 +01:00
Gustas
9c6cb501a4 We should notify host even when it's not in world 2024-10-13 14:35:59 +01:00
Gustas
5048a50403 Fix refineries not uncloaking on dock 2024-10-13 14:35:59 +01:00
darkademic
d450ef43c6 Download failure error. 2024-10-12 14:34:04 +03:00
Gregor Kališnik
6dd076b27f Improve NAT logging (#21611) 2024-10-07 21:00:07 +02:00
Ivaylo Draganov
79ae71a517 Rename *_RIGHT to *_WIDTH and *_BOTTOM to *_HEIGHT in integer expressions for widgets
The terms "width" and "height" are clearer and they match what the values actually represent (window or parent width/height). The YAML changes are generated with the update rule.
2024-10-07 12:59:25 +03:00
RoosterDragon
bb17cfa179 Expose mod.yaml content to localisation.
Mod metadata, load screens and mod content is all now sourced from ftl files, allowing these items to be translated.

Translations are now initialized as part of ModData creation, as currently they are made available too late for the usage we need here.

The "modcontent" mod learns a new parameter for "Content.TranslationFile" - this allows a mod to provide the path of a translation file to the mod which it can load. This allows mods such as ra, cnc, d2k, ts to own the translations for their ModContent, yet still make them accessible to the modcontent mod.

CheckFluentReference learns to validate all these new fields to ensure translations have been set.
2024-10-07 12:38:40 +03:00
RoosterDragon
d1583e8587 Fix LabelWidget positioning of text when WordWrap is true.
If WordWrap is enabled, the wrapping must be applied before any TextAlign is applied, or the final position will be incorrect.

Introduce a IncreaseHeightToFitCurrentText to make such WordWrap labels easier to use, and apply to CreditsLogic.
2024-10-07 12:38:40 +03:00
RoosterDragon
86b9227577 In RemoveCellsFromPlayerShroud, don't call RemoveSource unless required.
Since AddCellsToPlayerShroud only adds for players with a valid relationship, we can skip a dictionary lookup in RemoveSource by only attempting the remove if the relationship check passes as well.
2024-10-07 12:14:45 +03:00
RoosterDragon
d010157611 Improve performance of FrozenActorLayer.Tick
By adding a UpdateVisibilityNextTick flag against every FrozenActor to track when a visibility update is required, we can remove the dirtyFrozenActorIds set in FrozenActorLayer. In the Tick method we can now avoid a set lookup.

Also, don't create the frozenActorsToRemove list until we need one to avoid an allocation.
2024-10-05 21:17:59 +02:00
MHecker-code
6794b2dc40 Update OrderEffects.cs
Pass order name to OrderEffects
2024-10-05 10:06:33 +01:00
Gustas
00f504f6aa Don't create an unnecessary HPF layer 2024-10-04 21:02:29 +01:00
Gustas
014bca449f Automated fixup 2024-10-04 20:54:23 +01:00
Paul Chote
464e0dc7d2 Rename Localized to Fluent. 2024-10-04 15:11:27 +03:00
Paul Chote
d6285affec Remove FluentBundle.Arguments helper method. 2024-10-04 15:11:27 +03:00
Paul Chote
b29b685058 Rename Fluent-related code to be more precise. 2024-10-04 15:11:27 +03:00
Ivaylo Draganov
771b9ddfda Remove key handling from ContainerWidget
It has no business handling key input. This was used only for opening the ingame chat and replacing it with a `LogicKeyListener` was trivial.
2024-10-04 14:32:21 +03:00
Ivaylo Draganov
a69ea79d83 Remove PARENT_TOP and PARENT_LEFT from integer expressions for widgets
PARENT_TOP and PARENT_LEFT should be 0 so they are not very useful substitutions. They are replaced with 0 or removed if that was the whole value for a field.
2024-10-03 18:37:13 +03:00
Ivaylo Draganov
c0839d4521 Extract version label chrome logic to a dedicated class 2024-10-03 13:26:30 +03:00
Paul Chote
fff41a44ed Make Mod Content button optional. 2024-10-02 18:37:46 +03:00
Paul Chote
eac6d4b617 Route content check via FileSystemLoader.
This allows mods with custom content requirements
to use the default load screen implementations.
2024-10-02 18:37:46 +03:00
Paul Chote
720b925fd5 Move file system mounting into mod code. 2024-10-02 18:37:46 +03:00
Paul Chote
b60b1e369a Extract ObjectCreator.GetLoader for single objects. 2024-10-02 18:37:46 +03:00
Paul Chote
014cbc0cbd Remove FileSystem.ResolveAssemblyPath 2024-10-02 18:37:46 +03:00
Ivaylo Draganov
806f0fd270 Fix being able to click through some areas of the production palette in RA
The production palette in RA is assembled from a foreground and a background. The foreground provides most of the visible graphics (such as the metallic chrome) but it has to let clicks go through to the production icons. The background is the one that has to stop the clicks then but it was not wide enough (because the art is only for the background behind production icons and not the whole chrome). Trying to fix that by wrapping the image in wider container that has `ClickThrough` set to `false` revealed that there is a bug with the cloning logic for `ContainerWidget`. It simply did not copy the `ClickThrough` field and it was always `true` for cloned widgets. So the value in YAML was lost when the template was cloned.
2024-10-02 11:44:03 +03:00
Ivaylo Draganov
4e9ef7a334 Adjust width of command bar widget so it matches the artwork and the mouse cursor cannot click through it 2024-10-02 11:44:03 +03:00
Ivaylo Draganov
a44e956ed9 Simplify mouse input handling check in BakgroundWidget
Make it the same as in `ContainerWidget`.
2024-09-29 12:37:00 +03:00
Ivaylo Draganov
6bcfc20533 Remove drag support from BackgroundWidget
This was not used anywhere, was somewhat buggy and could be implemented in a better way when needed.
2024-09-29 12:37:00 +03:00
Smittytron
e1db0b89bd Assign bleep5 in notifications 2024-09-29 12:16:11 +03:00
Smittytron
168d5171f4 Add Allies13 2024-09-29 12:16:11 +03:00
RoosterDragon
b4882a8b03 Avoid some allocations in MiniYaml.Merge.
During the merge operation, it is quite common to be dealing with a node that has no child nodes. When there are no such nodes, we can return early from some functions to avoid allocating new collections that will not be used.

In the MergePartial operation, reuse a dictionary as scratch space when checking for conflicts. We introduce a IntoDictionaryWithConflictLog helper to allow this. This avoids allocating a new dictionary for the conflict log that gets thrown away at each check.
2024-09-23 16:18:00 +02:00
Gustas
03dd99699b Fix missile having no facing set on spawn 2024-09-21 00:06:55 +02:00
michaeldgg2
9524db20fe RenderMouseBounds: debug trait for tweaking mouse bounds of Interactable/Selectable trait 2024-09-20 23:38:26 +02:00
michaeldgg2
96235654f1 Interactable: allow specifying arbitrary 2D polygon for mouse interaction. 2024-09-20 23:38:26 +02:00
RoosterDragon
833e6bd652 Fix CreditsLogic to word-wrap the text.
This allows the text to be word-wrapped automatically, rather than the current approach of manually wrapping the source text.
2024-09-20 23:23:34 +02:00
michaeldgg2
073ce4a718 Editor: ActorEditLogic: support for dynamic generation of items in dropdown 2024-09-20 15:03:06 +03:00
michaeldgg2
507cdf1256 Fix ProductionTabsWidget not picking up ProductionQueue getting enabled/disabled during its lifetime 2024-09-19 18:10:07 +02:00
tjk-ws
b5b16df9e0 Fix parallel queues not pausing production when more than one item is queued 2024-09-19 18:07:02 +02:00
Gustas
0e438dd508 Automated fixup 2024-09-19 17:53:43 +02:00
Gustas
b0899d0ee4 Added RenameOnDeath update rule 2024-09-19 17:53:43 +02:00
Gustas
9c60ac23a3 Rename ThrowsShrapnel to FireProjectilesOnDeath 2024-09-19 17:53:43 +02:00
Gustas
c050b211eb Rename Explodes to FireWarheadsOnDeath 2024-09-19 17:53:43 +02:00
Gustas
29c4aebe19 Add missing descriptions to AirstrikePower and ParatroopersPower 2024-09-19 17:53:43 +02:00
RoosterDragon
87aa7c11c5 Provide buffer size in ShpRemasteredSprite.
As the expected size is quite small here, providing an explicit buffer size helps as otherwise default buffers for 1024 characters are allocated by the StreamReader which must be cleaned up by the GC afterwards. These smaller buffers still need cleanup but waste less memory.
2024-09-18 12:32:44 +03:00
RoosterDragon
ab50182c92 Change ActorIndex to work in terms of TraitInfo, instead of Trait.
This allows actor.Info.HasTraitInfo to be used when checking if an actor needs to be added to the index, which is a cheaper call than actor.TraitsImplementing.
2024-09-18 12:29:28 +03:00
Matthias Mailänder
327c1ba23b Adapt to upcoming environment special folder change. 2024-09-17 23:11:27 +02:00
Matthias Mailänder
2aa37d9392 Adapt to upcoming environment special folder change. 2024-09-16 19:50:06 +03:00
Gustas
b09c4f3770 Polish up UI 2024-09-16 12:00:41 +01:00
Gustas
251680056d Rename selection tab 2024-09-16 12:00:41 +01:00
Gustas
e9eb0da2b3 Improve map editor Copy-Paste tooltip 2024-09-16 12:00:41 +01:00
Gustas
e2b26754fe Fix switching tabs not yielding keyboard focus
And some extra polish
2024-09-16 12:00:41 +01:00
Gustas
87850378c7 Make brush rendering self-contained 2024-09-16 12:00:41 +01:00
Gustas
b073155018 Polish map editor code 2024-09-16 12:00:41 +01:00
RoosterDragon
7775b42b59 Change constructOrderCache to an array. 2024-09-15 22:02:14 +03:00
Gustas
a9e5744a37 Fix opening up skirmish menu making it impossible to leave map editor 2024-09-15 20:05:42 +02:00
RoosterDragon
b3b82b97ea Teach ConvertSpriteToPngCommand to handle frames that extend outside FrameSize.
The rectangle defined by a frames's Offset+Size usually fits within the rectangle given by FrameSize. However it can sometimes extend outside this region due to padding added by the engine. This causes an array-out-of-bounds crash as we only allocate space to cover the FrameSize region.

Now, we teach the copying process to only copy the portion of the data that lies within the FrameSize region. If any data extends outside the region, it won't be copied.
2024-09-15 20:00:32 +02:00
michaeldgg2
2612e7f297 Update Production's Faction when its owner changes 2024-09-14 17:44:02 +03:00
Gustas
c39d10b780 Fix AddMarkerLayerOverlay update rule 2024-09-13 19:08:08 +02:00
Gustas
f21572d22a Fix DynamicFacingInit not being dynamic 2024-09-13 19:04:38 +02:00
Gustas
ab6dc5db32 Fix OrderManager being deleted in map editor 2024-09-13 00:31:40 +02:00
Gustas
b070c0818f Add sounds on joining and leaving skirmish menu 2024-09-08 16:41:22 +02:00
Gustas
ec4449f092 Fixed load-game menu staying open for too long 2024-09-08 16:31:13 +02:00
Matthias Mailänder
5d7a24d5d0 Fix namespace. 2024-09-01 22:31:35 +02:00
Matthias Mailänder
41f28f2519 Guard the trait lookup. 2024-08-31 09:47:33 +03:00
Matthias Mailänder
25dd0508c2 Remove unused movement class bit set. 2024-08-30 21:04:35 +02:00
test1232156
a79b449fb2 Fixup D2K Encyclopedia
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
2024-08-30 19:24:43 +02:00
Gustas
56fabb4561 Deduplicate the back button 2024-08-30 19:24:43 +02:00
JovialFeline
bdce0570bd Remove IRON offset from RA1 map imports 2024-08-29 21:26:35 +03:00
Paul Chote
0a7e802d4b Fix TFD asset installation. 2024-08-29 21:22:11 +03:00
RoosterDragon
dd9aca83dd Expose the default font and cursor sheet size settings to mod.yaml 2024-08-27 19:25:07 +03:00
RoosterDragon
32bc99a11a Restored missing IDisposable to CursorManager. 2024-08-27 19:25:07 +03:00
RoosterDragon
1218ca09ae Use a smaller sheet in CursorManager.
Provide an explicit size of 512, smaller than the default 2048. The cursors for all mods still pack onto this single sheet, so we avoid wasting memory on larger sheets.

Sorting the cursors also helps pack them onto the sheets more efficiently.
2024-08-27 19:25:07 +03:00
RoosterDragon
db2cf125f8 Update Pfim to 0.11.3.
This version reduces memory allocation necessary to decode images.
2024-08-26 16:43:52 +03:00
Orb370
f090f1a02b TD Encyclopedia
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
Co-Authored-By: test1232156 <126501603+test1232156@users.noreply.github.com>
2024-08-25 21:18:02 +02:00
test1232156
536ffb3f31 Fix spelling errors and add naming consistency to translation keys
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
2024-08-20 17:34:09 +02:00
test1232156
c124684197 Fix chrome typos 2024-08-20 17:34:09 +02:00
RoosterDragon
0c4dff77c9 Fix moves being reported as blocked when already at the destination.
When a move is made where the source and target locations are the same and no actual moving is required, a path of length 0 is returned. When a move cannot be made as there is no valid route, a path of length 0 is also returned. This means Move is unable to tell the difference between no movement required, and no path is possible. Currently it will hit the `hadNoPath` case and report CompleteDestinationBlocked.

To fix the scenario where the source and target location match, track a alreadyAtDestination field. When this scenario triggers, report CompleteDestinationReached instead.

This fixes activities that were using this result to inform their next actions. e.g. MoveOntoAndTurn would previously cancel the Turn portion of the activity, believing that the destination could not be reached. Now, it knows the destination was reached (since we are already there!) and will perform the turn.
2024-08-19 14:33:38 +03:00
RoosterDragon
8101c70c91 Allow sheet buffers to be reused in SheetBuilder.
Sheets can be buffered - where a copy of their data is kept in RAM. This allows for modifications to the sheet to be batched in the RAM buffer, before later being committed to VRAM in a single operation. The SheetBuilder class allows for sprites to be allocated onto one or more sheets. Each time a sheet is filled, it will allocate a new sheet. Sheets allocated by the sheet builder will typically use the buffering mechanism.

Previously each time the builder allocated a new sheet, the buffer would get thrown away, and the next sheet would allocate a fresh buffer. These buffers can be large and may accumulate before the GC cleans them up. So although only one buffer will be live at a time, they can cause a spike in memory used by the process during loading.

We can avoid this issue by allowing the buffer from the previous sheet to be reused by the next sheet. This is possible because the sheet builder only has one live sheet for modifications at a time, and they are all the same type and size. By allocating only one buffer per builder instead of one per sheet, we reduce the peak memory required during loading.
2024-08-19 14:08:04 +03:00
RoosterDragon
7f05227e56 Remove some unrequired Sheet.CreateBuffer or Sheet.ReleaseBuffer calls.
- In CursorManager, we can release the buffer on the final sheet after loading cursors. We don't need to release the buffer on every sheet in the builder, as the builder will handle that.
- In SpriteCache, we don't need to call CreateBuffer explicitly, as the builder will do that for us.
- In RadarWidget, we don't need to call CreateBuffer explicitly, as GetData will do that for us.
2024-08-19 14:08:04 +03:00
RoosterDragon
323204014c Flush logs when crashing.
When the process is running, we use a finally block to call Log.Dispose and flush any outstanding logs to disk before the process exits. This works when we handle any exception in a matching catch block.

When the exception is unhandled, then the finally block will not run and instead the process will just exit. To fix this, flush the logs inside a catch block instead before rethrowing the error. This ensures we get logs even when crashing.
2024-08-16 17:49:35 +03:00
RoosterDragon
8a56e14d7a Allow SpawnStartingUnits to have an immovable BaseActor
Fixes a regression from 2c435c0506 - where the support actors must be able to path to the base actor in order to prevent them from spawning in isolated areas. If the base actor is immovable, they cannot path onto it because the base actor blocks them, so no support actors will spawn.

Fix this by allowing the support actors to path back to any cell adjacent to an immovable base actor, rather than requiring them to be able to path to its location directly.
2024-08-16 17:38:17 +03:00
David Wilson
77eba309b2 Fix sizing of exported ts/ra/d2k glyphs-3x.png and cnc chrome-3x.png from ArtSrc 2024-08-15 19:44:15 +03:00
RoosterDragon
a2edc82b0c Fix tooltips in the Encyclopedia.
If multiple tooltips are defined with conditionals attached, the Encyclopedia could not handle this. Now, it selects the tooltip EnabledByDefault to match usage across the rest of the code.
2024-08-13 23:10:46 +02:00
JovialFeline
00203201f4 Lock starting units in Fort Lonestar & Oil Spill 2024-08-11 15:59:38 +02:00
Matthias Mailänder
c779f4de8d Update macOS runner. 2024-08-11 12:38:53 +03:00
Matthias Mailänder
fc8c533129 Fix click through for disabled command bar buttons. 2024-08-11 12:33:20 +03:00
JovialFeline
723ba5a507 Adjust difficulty, add speeches to allies-06a 2024-08-11 12:28:01 +03:00
Matthias Mailänder
2d0b5f5fea Avoid possibly incompatible ZIP extra data. 2024-08-10 20:46:16 +03:00
RoosterDragon
da8eb68d9d AI prefers resources near to a refinery, rather than the idle harvester.
When HarvesterBotModule is ordering idle harvesters to nearby resources, it previously scanned from the harvester's current location. Instead, it now scans from the location of the nearest refinery. As the harvester will likely make many runs between the resource and the refinery, it is better to choose a location near the refinery. This will minimise overall distance travelled to harvest the resource patch.
2024-08-07 19:17:00 +03:00
RoosterDragon
058b725ca9 Reduce lag spikes from HarvesterBotModule.
The AI uses HarvesterBotModule to check for idle harvesters, and give them harvest orders. By default it scans every 50 ticks (2 seconds at normal speed), and for any idle harvesters locates an ore patch and issues a harvest order. This FindNextResource to scan for a suitable ore path is quite expensive. If the AI has to scan for ore patches for several harvesters, then this can produce a noticeable lag spike. Additionally, when there are no available ore patches, the scan will just keep repeating since the harvesters will always be idle - thus the lag spikes repeat every 50 ticks.

To reduce the impact, there already exists a randomization on the first scan interval so that multiple different AIs scan on different ticks. By ensuring the AI players scan at different times, we avoid a huge lag spike where they all operate on the same tick.

To reduce the impact even more, we make four additional changes:
- Scans continue to be done every 50 ticks to detect harvesters. But we spread out the searches for ore patches over multiple later ticks. We'll only perform one ore patch search per tick. This means instead of ordering e.g. 30 harvesters on a single tick and creating a spike, we order one on each tick over the next 30 ticks instead. This spreads out the performance impact.
- When a harvester fails to locate any suitable ore patch, we put it on a longer cooldown, by default 5x the regular cooldown. We don't need to scan as often for these harvesters, since it'll take time for new resources to appear.
- We change the path search in FindNextResource from FindPathToTargetCellByPredicate to FindPathToTargetCells. The format in an undirected path search that must flood fill from the start location. If ore is on the other side of the map, this entails searching the whole map which is very expensive. By maintaining a lookup of resource types per cell, we can instead give the target locations directly to the path search. This lookup requires a small overhead to maintain, but allows for a far more efficient path search to be carried out. The search can be directed towards the target locations, and the hierarchical path finder can be employed resulting in a path search that explores far fewer cells. A few tweaks are made to ResourceClaimLayer to avoid it creating empty list entries when this can be avoided.
- We adjust how the enemy avoidance cost is done. Previously, this search used world.FindActorsInCircle to check for nearby enemies, but this check was done for every cell that was searched, and is itself quite expensive. Now, we create a series of "bins" and cache the additional cost for that bin. This is a less fine grained approach but is sufficient for our intended goal of "avoid resource patches with too many enemies nearby". The customCost function is now less expensive so we can reuse the avoidance cost stored for each bin, rather than calculating fresh for every cell.
2024-08-07 19:17:00 +03:00
RoosterDragon
1cd3e1bf3f Fix PathFinder.FindPathToTargetCells.
When this hits the case that "As both ends are accessible, we can freely swap them." - we must note that we are reversing the path search and pass the information into the HierarchicalPathFinder. When a normal path search occurs, the actor trying to pathfind will never check its own location - and thus never gets blocked by itself. When a search is reversed, the search will check the actors location. If we inform the search it is doing done in reverse, it will special case this scenario and avoid the actor blocking itself. But if it is not told about this scenario, then this special case is not applied and no path will be found when in fact a path is possible.
2024-08-07 19:17:00 +03:00
Matthias Mailänder
c24913ea24 Add a UI indicator for bot players. 2024-08-05 12:55:59 +03:00
Matthias Mailänder
9761a68cd4 Don't disallow players chosing bot names. 2024-08-05 12:55:59 +03:00
Matthias Mailänder
4e5556dccc Expose player names to localization. 2024-08-05 12:55:59 +03:00
Paul Chote
9a46f3053a Enable "Game Mode" on macOS >= 14. 2024-08-05 12:51:48 +03:00
Matthias Mailänder
bc3c97398b Add localized message support to warheads. 2024-08-05 12:41:16 +03:00
Paul Chote
50d4936e51 Add SpriteCache.LoadFramesUncached.
This allows users (currently TDHD) to load ISpriteFrames
directly, without them being stored in the cache.
2024-08-05 12:30:33 +03:00
Paul Chote
8c5a286574 Call AdjustFrame with frame index and total. 2024-08-05 12:30:33 +03:00
Paul Chote
0c0c65df78 Fix D2kSpriteSequence namespace. 2024-08-05 12:30:33 +03:00
Paul Chote
5c741821a2 Remove obsolete D2k ResolveSprites override. 2024-08-05 12:30:33 +03:00
Matthias Mailänder
04d45e1f54 Revert "TS: EMP Cannon should only be able to fire via the support power. Fix…"
This reverts commit cd5eb89ebc.
2024-08-05 12:29:01 +03:00
Gustas
014163d7d3 Add a more helpful crash 2024-08-04 20:17:26 +02:00
RoosterDragon
2c435c0506 Ensure starting units or units granted by a crate are not isolated.
When spawning starting units, or spawning units when collecting a crate, nearby locations will be used. If a nearby location cannot be reached, e.g. it is on top of a cliff or blocked in by trees, then any unit spawned there will be isolated which is not ideal.

Use the PathMightExistForLocomotorBlockedByImmovable method to filter nearby locations to those where the spawned unit can path back to the original location. This ensures the spawned unit is not isolated.
2024-08-04 19:11:58 +02:00
JovialFeline
6bc613e93f Fix editor crash from added mission fencing 2024-08-04 18:56:59 +02:00
RoosterDragon
b313f47660 Implement Stream.Read(Span<byte>) overloads.
The default Stream implementation of this method has to rent an array so it can call the overload that accepts an array, and then copy the output over. This is because the array overload is required and the span overload was only added more recently.

We can avoid the overhead of this by implementing the span overload and working with the destination span directly. Do so for all classes we have that derive from Stream, and redirect their array overload to the span overload for code reuse.
2024-08-04 10:40:43 +01:00
RoosterDragon
d05b07a5b0 AI uses better rally point placement
- AI places rally points at pathable locations. A pathable location isn't strictly required, but avoids the AI setting rally points at seemingly dumb locations. This is an addtional check on top of the existing buildability check.
- AI now evaluates rally points every AssignRallyPointsInterval (default 100 ticks). Invalid rally points aren't that harmful, so no need to check them every tick. Additionally we do a rolling update so rally points for multiple locations are spread across multiple ticks to reduce any potential lag spikes.
2024-08-03 20:08:03 +03:00
RoosterDragon
ab28e6a75a Improve Lua type documentation and bindings.
The ExtractEmmyLuaAPI utility command, invoked with `--emmy-lua-api`, produces a documentation file that is used by the [OpenRA Lua Language Extension](https://marketplace.visualstudio.com/items?itemName=openra.vscode-openra-lua) to provide documentation and type information is VSCode and VSCode compatible editors when editing the Lua scripts.

We improve the documentation and types produced by this utility in a few ways:
- Require descriptions to be provided for all items.
- Fix the type definitions of the base engine types (cpos, wpos, wangle, wdist, wvec, cvec) to match with the actual bindings on the C# side. Add some extra bindings for these types to increase their utility.
- Introduce ScriptEmmyTypeOverrideAttribute to allow the C# side of the bindings to provide a more specific type. The utility command now requires this to be used to avoid accidentally exporting poor type information.
- Fix a handful of scripts where the new type information revealed warnings.

The ability to ScriptEmmyTypeOverrideAttribute allows parameters and return types to provide a more specific type compared to the previous, weak, type definition. For example LuaValue mapped to `any`, LuaTable mapped to `table`, and LuaFunction mapped to `function`. These types are all non-specific. `any` can be anything, `table` is a table without known types for its keys or values, `function` is a function with an unknown signature.

Now, we can provide specific types. , e.g. instead of `table`, ReinforcementsGlobal.ReinforceWithTransport is able to specify `{ [1]: actor, [2]: actor[] }` - a table with keys 1 and 2, whose values are an actor, and a table of actors respectively. The callback functions in MapGlobal now have signatures, e.g. instead of `function` we have `fun(a: actor):boolean`. In UtilsGlobal, we also make use of generic types. These work in a similar fashion to generics in C#. These methods operate on collections, we can introduce a generic parameter named `T` for the type of the items in those collections. Now the return type and callback parameters can also use that generic type. This means the return type or callback functions operate on the same type as whatever type is in the collection you pass in. e.g. Utils.Do accepts a collection typed as `T[]` with a callback function invoked on each item typed as `fun(item: T)`. If you pass in actors, the callback operates on an actor. If you pass in strings, the callback operates on a string, etc.

Overall, these changes should result in an improved user experience for those editing OpenRA Lua scripts in a compatible IDE.
2024-08-03 19:12:51 +03:00
Matthias Mailänder
14ef6b5774 Fix formatting rule warnings. 2024-08-03 13:27:18 +03:00
RoosterDragon
578a9fe457 Fix mod content installers.
In 4312a4d3f4 MiniYaml merging was adjusted. One effect of this change was that duplicate keys in files that did not previously require merging was previously allowed, but was now an error. (Test case `TestMergeConflictsNoMerge`)

The installer files were relying on the previous behaviour to allow multiple `ContentPackage` keys. The above change caused a regression where attempting to manage mod content would crash due to now erroring on the duplicate keys.

We fix the issue by applying a unique ID suffix, as is a common pattern elsewhere in our yaml files, and teach InstallFromSourceLogic to recognise and strip it.
2024-08-03 12:10:08 +03:00
Matthias Mailänder
ebaed9966b Update Linguini. 2024-08-03 12:00:16 +03:00
Matthias Mailänder
b8756c4737 Move CachedTransform to OpenRA.Primitives. 2024-08-03 11:56:43 +03:00
RoosterDragon
88fb83bc57 Remove caching of CurrentAdjacentCells in Cargo
In 05ed9d9a73 we stopped caching the values with ToArray to resolve a desync. But even caching the enumerable can lead to a desync, so remove the caching entirely.

----

Let's explain how the code that cached values via ToArray could desync.

Usually, the cell given by `self.Location` matches with the cell given by `self.GetTargetablePositions()`. However if the unit is moving and close to the boundary between two cells, it is possible for the targetable position to be an adjacent cell instead.

Combined with the fact hovering over the unit will evaluate `CurrentAdjacentCells` only for the local player and not everybody, the following sequence becomes possible to induce a desync:
- As the APC is moving into the last cell before unloading, the local player hovers over it. `self.Location` is the last cell, but `self.GetTargetablePositions()` gives the *previous* cell (as the unit is close to the boundary between the cells)
- The local player then caches `CurrentAdjacentCells`. The cache key of `self.Location` is the final cell, but the values are calculated for `self.GetTargetablePositions()` of an *adjacent* cell.
- When the order to unload is resolved, the cache key of `CurrentAdjacentCells` is already `self.Location` and so `CurrentAdjacentCells` is *not* updated.
- The units unload into cells based on the *adjacent* cell.

Then, for other players in the game:
- The hover does nothing for these players.
- When the order is resolved, `CurrentAdjacentCells` is out of date and is re-evaluated.
- `self.Location` and `self.GetTargetablePositions()` are both the last cell, because the unit has finished moving.
- So the cache is updated with a key of `self.Location` and values from the *same* cell.
- The units unload into cells based on the *current* cell.

As the units unload into different cells, a desync occurs. Ultimately the cause here is that cache key is insufficient - `self.Location` can have the same value but the output can differ. The function isn't a pure function so memoizing the result via `ToArray()` isn't sound.

Reverting it to cache the enumerable, which is then lazily re-evaluated reduces the scope of possible desyncs but is NOT a full solve. The cached enumerable caches the result of `Actor.GetTargetablePositions()` which isn't a fully lazy sequence. A different result is returned depending on `EnabledTargetablePositions.Any()`. Therefore, if the traits were to enable/disable inbetween, then we can still end up with different results. Memoizing the enumerable isn't sound either!

Currently our only trait is `HitShape` which is enabled based on conditions. A condition that enables/disables it based on movement would be one way to trigger this scenario. Let's say you have a unit where you toggle between two hit shapes when it is moving and when it stops moving. That would allow you to replicate the above scenario once again.

Instead of trying to come up with a sound caching mechanism in the face of a series of complex inputs, we just give up on trying to cache this information at all.
2024-08-01 22:58:15 +02:00
Matthias Mailänder
05ed9d9a73 Revert "Fix CurrentAdjacentCells cache not acting as a cache"
This reverts commit 6040187844.
2024-07-31 22:51:46 +02:00
Matthias Mailänder
261bf88382 Update .NET Test SDK. 2024-07-31 12:17:34 +02:00
Gustas
7b9a173f5a Trim empty space around edges of Shp(TD) frames.
Co-Authored-By: Paul Chote <pchote@users.noreply.github.com>
2024-07-30 13:27:16 +02:00
JovialFeline
5164a11c15 Fix typo in HarvesterAttackNotifier 2024-07-30 12:48:57 +02:00
RoosterDragon
0649f3dc32 RCS0056 - roslynator_max_line_length = 160 2024-07-29 21:56:36 +02:00
RoosterDragon
9d5d2ab493 RCS0056 - roslynator_max_line_length = 180 2024-07-29 21:56:36 +02:00
Matthias Mailänder
822a29aa76 Remove invalid squad members before responding to attack. 2024-07-28 23:49:11 +03:00
Matthias Mailänder
ff276b4877 Fetch the player name from itch.io 2024-07-28 23:33:36 +03:00
RoosterDragon
bd809e5af7 Prevent community mods from warning on unused translations in the common assets
Currently when linting translations, we check for any unused translation keys. This works fine for the default mods, which own the entire sets of translation files. For community mods, they often import the translation files from the common mod assets, but they may only use some of the translations provided. Currently, they would get warnings about not using translations from the common files they have imported.

Since the community mods don't own those translations, getting warnings about it is annoying. To solve this issue, introduce a AllowUnusedTranslationsInExternalPackages in the mod.yaml which defaults to true. This will prevent reporting of unused translation keys from external assets. Keys that are used for external assets will still be validated, and keys from the mod assets will be both validated and unused keys will be reported.

We default the new flag to true and don't provide an update rule. This means community mods will get the new behaviour. For the default mods, we do want to check the "external" assets, since we control those assets. So the default mods have their mod.yaml updated to disable the flag and retain the existing behaviour of checking everything.
2024-07-28 22:43:14 +03:00
JovialFeline
0aac5885fb Add base failure, Nod defense to gdi05b & gdi05c 2024-07-26 23:23:29 +03:00
Matthias Mailänder
bfb159b9b3 Immediately hide the single-player menu
when skirmish is selected.
2024-07-26 23:08:33 +03:00
RoosterDragon
c45e78cf1d Improve performance of AIUtils.FindQueues
The AI would often invoke this method inside of loops, searching for a different category of queue each time. This would result in multiple searches against the trait dictionary to locate matching queues. Now we alter the method to create a lookup of all the queues keyed by category. This allows a single trait search to be performed.

UnitBuilderBotModule and BaseBuilderBotModule are updated to fetch this lookup once when required, and pass the results along to avoid calling the method more times than necessary. This improves their performance.
2024-07-26 23:04:52 +03:00
Moritz Heller
62d1f002dd Update Renderer.cs
Fix graphical error when adding mulitplayer spawn points in the map editor.
2024-07-26 21:55:37 +03:00
RoosterDragon
84f8c6a4c6 Order help commands by name. 2024-07-26 21:35:19 +03:00
RoosterDragon
c1f99cb094 HPF handles searches from unreachable source cells into cut off areas.
The scenario is that an actor is on an unreachable tile, and would like to path. As long as it is immediately adjacent to some reachable tiles, it can still move onto them and path. Now imagine a map split into two by a one tile wide line of impassable cliffs. It is important which side it chooses to jump into, as once it has moved off the cliff it loses access to the other side. Jumping off the correct side will allow a valid path, jumping off the wrong side will prevent a path from being possible.

In d8ebb96077, handling was added to prevent a crash where the path search would simulate having the actor jump off the wrong side and then get confused that it could not find a path when one was expected. This fix works by remembering the `unpathableNodes` - the nodes where you jump onto the wrong side. If we encounter them later, we can ignore them.

In 5157bc375d we added domain checks - this allows the HPF to bail on impossible paths early by checking if they belong to different domains. For example islands on a water map will belong to different domains.

This caused a regression where `sourcesWithReachableNodes` was now badly named. Some reachable nodes because they were in the wrong domain. Later when `sourcesWithPathableNodes` and `unpathableNodes` are built - we don't populate the `unpathableNodes` correctly, because we already excluded the unpathable nodes earlier on! This means we don't ignore them any more, and we reintroduce the crash.

Now that we are checking the domain, we can simplify the code and resolve the crash at the same time. `sourcesWithReachableNodes` becomes `sourcesWithPathableNodes` because the domain check has been done. We can build `unpathableNodes` at the same time. This allows us to remove the later code that explored the abstract graph, as the domain check succinctly achieves the same end goal of determining whether the node has a path or not.
2024-07-26 18:04:10 +02:00
JovialFeline
c64eea7872 Fix untranslated factions in GameInfoStats 2024-07-26 17:16:07 +02:00
RoosterDragon
bebe3f710b Remove duplicated definitions in utils.lua 2024-07-22 17:27:58 +02:00
RoosterDragon
0bfa53b58d Teach CheckTranslationReference about translations in Lua scripts
Using the glory of regex, we can scrape any Lua script files that a map includes and locate calls to the UserInterface.Translate method. We can then treat them in the same way as C# fields marked with a TranslationReferenceAttribute. This allows the lint check to validate the translation invoked in the .lua script has a matching entry in the translation .ftl files, with all the required arguments (if any).

We can also locate any calls to AddPrimaryObjective or AddSecondaryObjective defined by the utils.lua script, which also accept translation keys.

The are a couple of restrictions:
- When linting the map, we don't check for keys in the ftl file that are unused. This is because the linter doesn't load all the keys when checking maps.
- In order to validate translation arguments with the regex, we require the Lua script to pass the table of arguments inline at the callsite. If it does not, we raise a warning so the user can adjust the code.
2024-07-22 17:27:58 +02:00
JovialFeline
e8d5c005a2 Restore missions' barbed wire 2024-07-22 14:11:32 +03:00
JovialFeline
c58621e036 Add bombers, fixes to Production Disruption 2024-07-22 14:03:47 +03:00
JovialFeline
ff5b5149b3 Add navy orders, fixes to Soviet 06a 2024-07-22 14:03:14 +03:00
Moritz Heller
81bcff0f8a Fix movement on ramps when moving to subcell
Fix movement on ramps when moving to subcell
2024-07-22 13:52:24 +03:00
tjk-ws
bdc142ae51 Fix a crash caused by invalid target in FlyAttack 2024-07-21 11:31:37 +02:00
Gustas
7b01204eed Spawn aircraft landed and occupying land or at cruise altitude 2024-07-20 18:47:58 +02:00
Gustas
734afd8bcf No need for dummy value 2024-07-20 18:47:58 +02:00
Matthias Mailänder
5ddc7b1177 Localize the faction dropdown. 2024-07-18 22:10:03 +03:00
Matthias Mailänder
59f6a6a2c2 Fix localisation on case sensitive file systems. 2024-07-18 22:10:03 +03:00
RoosterDragon
3716ffb3b5 Fix the remap for sprites in d2k.
Follow-up from 34ff23d030, the previous `-Remap:` line was not working and has since been removed. Without the remap being unapplied, there are rendering artefacts introduced as a result. Fix this by explicitly setting the remap to something else instead.
2024-07-18 21:55:02 +03:00
RoosterDragon
4312a4d3f4 Make MiniYaml inherits and removal more flexible
- Previously the Inherits syntax was only resolved when used for top-level nodes. Now it is also resolved for nested nodes as well.
- Previously the MiniYAML Merge feature supported the ability to remove nodes, but this only worked within the context of inherited nodes. Now, we allow node removal to work outside of the inheritance context.
2024-07-18 21:55:02 +03:00
Gustas
dccab8fd21 Fix PortableChrono not working 2024-07-17 20:36:57 +03:00
JovialFeline
0138bc9b3f Fix untranslated speakers 2024-07-17 19:39:17 +03:00
RoosterDragon
a5dd4ffe36 Cache teams in observer logic
The player state and teams are fixed after the game starts, so it is safe to cache the resulting teams in the observer logic rather than re-evaluating them each time.
2024-07-16 13:01:27 +03:00
Tocho Tochev
fb420e88bd Fix Survival01 mission objective 2024-07-16 12:54:49 +03:00
RoosterDragon
0d84804d81 Reduce lag spikes from SquadManagerBotModule.
Updating squads is the most expensive part of SquadManagerBotModule. It involves ticking the current squad state. This usually involves finding nearby enemies and evaluating the fuzzy state machine to decide whether to interact with those enemies. Since all the AI squads for a player get ordered on the same tick, this can result in a lag spike.

To reduce the impact, we'll spread out the updates over multiple ticks. This means overall all the AI squads will still be refreshed every interval, but it'll be a rolling update rather than all at once. By spreading out the updates we avoid a lag spike from the cumulative updates of all the squads. Now the lag spike is reduced to the worst any single squad update can incur.
2024-07-08 18:20:33 +03:00
RoosterDragon
b3168928c6 Remove unused previous state in AI StateMachine
The StateMachine offered a feature to remember the previous state and allow reverting to it. However this feature is unused. Remove it to allow the previous states to be reclaimed by the GC earlier.
2024-07-08 17:44:02 +03:00
RoosterDragon
c1de85f700 Improve performance of path-debug command.
If a long path was being visualized with the path-debug command it would generate renderables for everything on the path, even for parts of the path that would be offscreen. Add some simplistic culling so the performance impact is reduced.
2024-07-08 17:30:46 +03:00
Matthias Mailänder
387554acbb Fixed a crash on empty mission options. 2024-07-05 16:11:20 +03:00
michaeldgg2
446d37b832 WithDockingAnimation: remove unnecessary dependency on Harvester trait 2024-07-01 23:26:52 +03:00
Gustas
e9e6c4b988 Fix multiqueue 2024-07-01 23:13:13 +03:00
RoosterDragon
2ed0656d1b Introduce MoveCooldownHelper to prevent lag spikes from failed pathfinding
Several activities that queue child Move activities can get into a bad scenario where the actor is pathfinding and then gets stuck because the destination is unreachable. When the Move activity then completes, then parent activity sees it has yet to reach the destination and tries to move again. However, the actor is still blocked in the same spot as before and thus the movment finishes immediately. This causes a performance death spiral where the actor attempts to pathfind every tick. The pathfinding attempt can also be very expensive if it must exhaustively check the whole map to determine no route is possible.

In order to prevent blocked actors from running into this scenario, we introduce MoveCooldownHelper. In its default setup it allows the parent activity to bail out if the actor was blocked during a pathfinding attempt. This means the activity will be dropped rather than trying to move endlessly. It also has an option to allow retrying if pathfinding was blocked, but applies a cooldown to avoid the performance penalty. For activities such as Enter, this means the actors will still try and enter their target if it is unreachable, but will only attempt once a second now rather than every tick.

MoveAdjacentTo will now cancel if it fails to reach the destination. This fixes MoveOntoAndTurn to skip the Turn if the move didn't reach the intended destination. Any other derived classes will similarly benefit from skipping follow-up actions.
2024-07-01 15:56:11 +03:00
tjk-ws
fe35c42ead Make InstantlyRepairs properly support multiple types 2024-06-29 14:47:05 +03:00
tjk-ws
a16542e052 Always complete FlyAttack for a zero MaximumRange to avoid stalling 2024-06-29 14:15:39 +03:00
N.N
06cc47847d Spicebloom refactor
- pieces represent Min and Max instead of random interval
- More control where spice spread
- fix AoE in  SpiceExplosion weapon
- delay interval between each pieces

Co-Authored-By: Gustas <37534529+PunkPun@users.noreply.github.com>
2024-06-29 00:09:13 +03:00
tjk-ws
387e594f4a Allow hotkeying completed buildings whose icons are not visible on the production palette 2024-06-29 00:00:50 +03:00
Matthias Mailänder
0d97f3374c Fix a crash when the target is destroyed. 2024-06-26 21:46:30 +03:00
N.N
ccb1bd7a74 Enable paying upfront
Fix tab availability on low money

Co-Authored-By: Gustas <37534529+PunkPun@users.noreply.github.com>
2024-06-17 13:21:42 +03:00
tjk-ws
2a3271b0d0 Fix autotarget not checking all attack traits for targets 2024-06-17 12:01:07 +03:00
tjk-ws
6a7159e9a1 Fix aircraft that don't rearm stalling over invalid targets 2024-06-17 11:51:19 +03:00
RoosterDragon
cf0e73e75e Improve performance of copy-paste in map editor.
- EditorActorLayer now tracks previews on map with a SpatiallyPartitioned instead of a Dictionary. This allows the copy-paste logic to call an efficient PreviewsInCellRegion method, instead of asking for previews cell-by-cell.
- EditorActorPreview subscribes to the CellEntryChanged methods on the map. Previously the preview was refreshed regardless of which cell changed. Now the preview only regenerates if the preview's footprint has been affected.
2024-06-16 13:35:13 +03:00
RoosterDragon
34a68cd2ca Avoid keeping ActorInitializers in memory.
The SupportPowerManager and WithSpriteBody trait captured the ActorInitializer in lambda expressions, which keeps it alive as long as the trait. The lambdas didn't need to capture the ActorInitializer, so rejig them to allow the ActorInitializer to be reclaimed after the traits have been created. As the TypeDictionary in the ActorInitializer can be quite large, this helps reduce memory usage.
2024-06-16 13:19:07 +03:00
RoosterDragon
cac0438d48 Fix map editor copy-paste for isometric maps.
When dealing with isometric maps, the copy paste region needs to copy the CellCoords area covered by the CellRegion. This is equivalent to the selected rectangle on screen. Using the cell region itself invokes cell->map->cell conversion that doesn't roundtrip and thus some of the selected cells don't get copied.

Also when pasting terrain + actors, we need to fix the sequencing to clear actors on the current terrain, before adjusting the height and pasting in new actors. The current sequencing means we are clearing actors after having adjusted the terrain height, and affects the wrong area.
2024-06-15 15:53:42 +03:00
N.N
9302bac619 Replace BLOXMAS.R8 with R16 eqivalent
- file BLOXXMAS.R8 removed from mod content as its no longer needed
2024-06-08 22:11:50 +03:00
N.N
616046179f Fix engineer cannot restore ally vehicles 2024-06-03 12:46:44 +03:00
N.N
0c572a862c Fix actorSpawnManager use only one spawnpoint at the moment 2024-06-03 12:33:06 +03:00
Gustas
64b2bd4735 Update to HTTPS 2024-06-03 10:25:06 +02:00
Gustas
f337450348 Link no longer exists 2024-06-03 10:25:06 +02:00
Matthias Mailänder
bc29707e87 Mention infrastructure Patreon account. 2024-06-03 10:25:06 +02:00
Gustas
a20f5981e2 Fix TSVeinRenderer not having tooltip translated 2024-06-02 16:54:40 +02:00
darkademic
b693f20789 Fix crash caused by queuing multiple pick up orders for a single unit. 2024-05-27 15:03:21 +03:00
Matthias Mailänder
69d9bbb400 Unhardcode support power blocked cursor. 2024-05-19 14:43:59 +03:00
Matthias Mailänder
fc4ebf332d Deduplicate directional support powers. 2024-05-19 14:43:59 +03:00
dnqbob
1a3d3cd31e Add Firestrom generator (incomplete) 2024-05-19 14:35:09 +03:00
dnqbob
056420c834 Add WithSwitchableOverlay 2024-05-19 14:35:09 +03:00
Gustas
2af417bf49 Revert part of 377bb4b6cc 2024-05-11 22:14:38 +02:00
Kevin Streser
cbcd413ed1 Consider spectators for waiting filter condition 2024-05-07 22:24:37 +03:00
Matthias Mailänder
8c174551f5 Map pool update
- add Destroyable tiles into d2k mod
- add ability engineer to repair destroyed cliffs
2024-05-04 16:55:01 +02:00
Gustas
6b463f9d9e Remove color caches 2024-05-04 16:31:35 +02:00
Gustas
239891070d Make sure palettes are updated if colors are toggled in game runtime
And fix MutablePalette's not getting updated
2024-05-04 16:31:35 +02:00
Gustas
5fc36bd45f Make player stance colours universally respected 2024-05-04 16:31:35 +02:00
Gustas
2f331548e1 Remove player colors from healthbars 2024-05-04 16:31:35 +02:00
Gustas
34262fb33c Make PlayerRelationShipColor static 2024-05-04 16:31:35 +02:00
N.N
fa0254cb27 Add missing husk and visual improvements
- adjust husk color overlay
- engineer can capture husk
- balance changes to husk HP
2024-05-04 16:23:38 +02:00
Gustas
d91d1f5466 Dispose sound sources 2024-05-04 14:53:38 +02:00
Matthias Mailänder
84e6623773 Use HTTPS 2024-05-04 15:33:45 +03:00
Unrud
0f4f52fa1a AppStream Metadata: Add developer name 2024-05-04 13:48:26 +02:00
JovialFeline
b48a74f065 Add polish to Intervention 2024-05-03 19:05:27 +03:00
dnqbob
64f35feb13 WithIdleOverlay: support animation with facings 2024-05-03 18:36:07 +03:00
JovialFeline
670067ae3a Add Negotiations mission (Red Alert) 2024-05-03 18:24:47 +03:00
dnqbob
c63be29770 TS: simplify Woverine 2024-04-30 21:17:24 +03:00
dnqbob
4f8ae422ec WithAttackOverlay: support decoration animation. 2024-04-30 21:17:24 +03:00
dnqbob
f24a966f53 WithAttackOverlay: support specifc Armament to play animation. 2024-04-30 21:17:24 +03:00
dnqbob
78fdfbfb09 WithAttackOverlay: now support facing correctly with BodyOrientation 2024-04-30 21:17:24 +03:00
dnqbob
bdb0cfe243 WithAttackOverlay: fix ZOffset not related to actor. 2024-04-30 21:17:24 +03:00
RaushanSakhibzadin
106c18480f Fix Scroll bug when chat is opened 2024-04-30 13:19:57 +03:00
Matthias Mailänder
97c61e0068 Extract strings from resource renderer. 2024-04-30 11:27:46 +03:00
JovialFeline
6c9e33b76c Fix subcells imported from Gen1 maps 2024-04-30 11:12:04 +03:00
chacha
e5a7b6e795 Do not keep map pakages loaded on Game start to reduce memory impact 2024-04-30 11:06:17 +03:00
ryanjschneebaum
affc98851c added sorting of names to earnings graph 2024-04-30 10:59:18 +03:00
RoosterDragon
10d7436cf0 CheckTranslationReference lint pass learns additional checks.
- Test all translation languages, not just English.
- Report any fields marked with TranslationReferenceAttribute that the lint pass lacked the knowledge to check.
- Improve context provided by lint messages.

Restructure code for readability.
2024-04-24 15:28:28 +03:00
RoosterDragon
5fcda6a1a4 Fix some bugs in Translation.
- Use error.Message to reports errors, as error.ToString isn't overridden.
- Ensure multiple translation languages are handled correctly, we need to use a list to maintain English being the first one.
2024-04-24 15:28:28 +03:00
RoosterDragon
2c96f6ec8b Inject platform dependency in SpriteFont.
This avoids needing to access the global Game.Renderer.
2024-04-24 15:28:28 +03:00
RoosterDragon
377bb4b6cc Tweak some UI Yaml files
- Increase space of some elements to fit their text.
- Remove editor text that aren't translation keys. The text will be set in code anyway.
- Remove some unused elements.
- Provide a translation key for the port separator, to avoid log warnings.
2024-04-24 15:28:28 +03:00
Gustas
150e28a672 Fix missing map files preventing map saves 2024-04-11 23:02:48 +02:00
Gustas
cf21c8e906 Fix support power name not really being optional 2024-04-10 23:52:43 +02:00
RoosterDragon
7859b913bc Trim memory usage of Png.
For indexed PNGs, we only need to allocate a palette large enough to accommodate the number of indexed colours in the image. For images that don't use all 256 colours, this slightly reduces the memory usage for these images.
2024-04-06 10:47:19 +03:00
RoosterDragon
2481bddf58 Trim memory usage of FileSystem.
When LoadFromManifest is called, trim the various backing collections. These backing collections tend to live a long time without further modifications.
2024-04-06 10:47:19 +03:00
RoosterDragon
c547f3f26d Trim memory usage of SpriteCache.
As the SpriteCache is used as a one-shot operation in practise, holding on to the capacity of backing collections is not required. Memory usage can be reduced by allowing the capacity to be reset after the SpriteCache has resolved items.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

update ToHex(byte[]) to support mono

added punctuations to unit test summary and parameter description

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

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

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

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

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

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

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

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

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

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

The rest of the changeset is dealing with the change in the exposed API surface. Some With* helper methods are introduced to allow creating new YAML from existing YAML. Areas of code that generated small amounts of YAML are able to transition directly to the immutable model without too much ceremony. Some use cases are far less ergonomic even with these helper methods and so a MiniYamlBuilder is introduced to retain mutable creation functionality. This allows those areas to continue to use the old mutable structures. The main users are the update rules and linting capabilities.
2023-08-07 21:57:10 +03:00
Matthias Mailänder
b6a5d19871 Evaluate read only dictionaries. 2023-08-06 17:12:34 +03:00
Gustas
ce002ce8c1 Fix gen1 map importer crashing on invalid tiles 2023-08-06 13:53:22 +02:00
Gustas
9c3e366d03 Fix out of bounds cells not being randomised 2023-08-06 13:53:22 +02:00
Gustas
bb96e22e64 Fix low power notification never triggering 2023-08-05 19:03:15 +02:00
Gustas
a691f2ebac Give husks the ability to crush 2023-08-05 14:27:51 +02:00
Gustas
7638822e49 Disable force start panel start button when unable to start the game 2023-08-05 14:18:15 +02:00
Gustas
a9cf728ee1 Refactor MapPreviewLogic
and add a states for updating map via MapCache.GetUpdatedMap
2023-08-05 14:18:15 +02:00
Gustas
2c4a135c2b Grant condition to units closest to the crate 2023-08-05 13:32:51 +02:00
Gustas
d686634c0b Fix aircraft jittering 2023-08-05 13:27:32 +02:00
Gustas
32b0003a72 Fix misaligned TD combat observer tab 2023-08-05 13:20:33 +02:00
Matthias Mailänder
c234b4c78f Send the join message/ping also in skirmish. 2023-08-04 21:47:28 +03:00
Matthias Mailänder
f2a242b09a Let all lobby sounds be optional. 2023-08-04 21:47:28 +03:00
Matthias Mailänder
a1efb28f0b Add lobby sounds for leave, join and option change 2023-08-04 21:47:28 +03:00
Smittytron
d217ab39c2 Add Soviet13b 2023-08-03 16:22:42 +02:00
Gustas
31840328b7 Exit game save with escape 2023-08-03 15:49:33 +02:00
Gustas
54547a11d0 Trigger a button sound when saving a game with enter 2023-08-03 15:49:33 +02:00
Gustas
f99db8d754 Fix lua sanity check crashing on dedicated servers 2023-08-03 15:34:05 +02:00
Vapre
1ce916182d RingBuffer primitive. 2023-08-02 19:42:31 +03:00
abcdefg30
09ba09f4e3 Fix RA assets installation from the Steam C&C:R version 2023-08-01 22:28:32 +03:00
dnqbob
2ac85ac61d Add InstantlyRepairsProperties 2023-08-01 12:21:19 +02:00
dnqbob
44e024a94e Make InstantRepair public 2023-08-01 12:21:19 +02:00
3084 changed files with 117907 additions and 40007 deletions

File diff suppressed because it is too large Load Diff

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
patreon: orahosting

View File

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

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,25 +2,30 @@ name: Continuous Integration
on:
push:
paths-ignore:
- '*.md'
pull_request:
branches: [ bleed, 'prep-*' ]
paths-ignore:
- '*.md'
- '*.py'
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
linux:
name: Linux (.NET 6.0)
name: Linux (.NET 8.0)
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
- name: Install .NET 8.0
uses: actions/setup-dotnet@v5
with:
dotnet-version: '6.0.x'
dotnet-version: '8.0.x'
- name: Check Code
run: |
@@ -33,36 +38,18 @@ jobs:
make check-scripts
make TREAT_WARNINGS_AS_ERRORS=true test
linux-mono:
name: Linux (mono)
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v3
- name: Check Code
run: |
mono --version
make RUNTIME=mono check
- name: Check Mods
run: |
# check-scripts does not depend on .net/mono, so is not needed here
make RUNTIME=mono TREAT_WARNINGS_AS_ERRORS=true test
windows:
name: Windows (.NET 6.0)
runs-on: windows-2019
name: Windows (.NET 8.0)
runs-on: windows-2022
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
- name: Install .NET 8.0
uses: actions/setup-dotnet@v5
with:
dotnet-version: '6.0.x'
dotnet-version: '8.0.x'
- name: Check Code
shell: powershell
@@ -74,7 +61,7 @@ jobs:
- name: Check Mods
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:TREAT_WARNINGS_AS_ERRORS = "true"
.\make.ps1 check-scripts

View File

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

View File

@@ -15,73 +15,56 @@ jobs:
runs-on: ubuntu-22.04
if: github.repository == 'openra/openra'
steps:
- name: Download Packages
- name: Download Butler
env:
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
run: |
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}-x64.exe"
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}-x64-winportable.zip" -O "OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip"
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}.dmg"
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Dune-2000-x86_64.AppImage"
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Red-Alert-x86_64.AppImage"
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Tiberian-Dawn-x86_64.AppImage"
wget -q "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.event.inputs.tag }}/packaging/.itch.toml"
zip -u "OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip" .itch.toml
wget -cq -O butler-linux-amd64.zip https://broth.itch.ovh/butler/linux-amd64/LATEST/archive/default
unzip butler-linux-amd64.zip
rm butler-linux-amd64.zip
chmod +x butler
./butler -V
./butler login
- name: Publish Windows Installer
uses: josephbmanley/butler-publish-itchio-action@master
env:
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
CHANNEL: win
ITCH_GAME: openra
ITCH_USER: openra
VERSION: ${{ github.event.inputs.tag }}
PACKAGE: OpenRA-${{ github.event.inputs.tag }}-x64.exe
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
run: |
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}-x64.exe"
./butler push "OpenRA-${{ github.event.inputs.tag }}-x64.exe" "openra/openra:win" --userversion ${{ github.event.inputs.tag }}
- name: Publish Windows Itch Bundle
uses: josephbmanley/butler-publish-itchio-action@master
env:
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
CHANNEL: itch
ITCH_GAME: openra
ITCH_USER: openra
VERSION: ${{ github.event.inputs.tag }}
PACKAGE: OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
run: |
wget -q "https://raw.githubusercontent.com/${{ github.repository }}/${{ github.event.inputs.tag }}/packaging/.itch.toml"
zip -u "OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip" .itch.toml
./butler push "OpenRA-${{ github.event.inputs.tag }}-x64-win-itch.zip" "openra/openra:itch" --userversion ${{ github.event.inputs.tag }}
- name: Publish macOS Package
uses: josephbmanley/butler-publish-itchio-action@master
env:
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
CHANNEL: macos
ITCH_GAME: openra
ITCH_USER: openra
VERSION: ${{ github.event.inputs.tag }}
PACKAGE: OpenRA-${{ github.event.inputs.tag }}.dmg
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
run: |
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-${{ github.event.inputs.tag }}.dmg"
./butler push "OpenRA-${{ github.event.inputs.tag }}.dmg" "openra/openra:macos" --userversion ${{ github.event.inputs.tag }}
- name: Publish RA AppImage
uses: josephbmanley/butler-publish-itchio-action@master
env:
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
CHANNEL: linux-ra
ITCH_GAME: openra
ITCH_USER: openra
VERSION: ${{ github.event.inputs.tag }}
PACKAGE: OpenRA-Red-Alert-x86_64.AppImage
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
run: |
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Red-Alert-x86_64.AppImage"
./butler push "OpenRA-Red-Alert-x86_64.AppImage" "openra/openra:linux-ra" --userversion ${{ github.event.inputs.tag }}
- name: Publish TD AppImage
uses: josephbmanley/butler-publish-itchio-action@master
env:
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
CHANNEL: linux-cnc
ITCH_GAME: openra
ITCH_USER: openra
VERSION: ${{ github.event.inputs.tag }}
PACKAGE: OpenRA-Tiberian-Dawn-x86_64.AppImage
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
run: |
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Tiberian-Dawn-x86_64.AppImage"
./butler push "OpenRA-Tiberian-Dawn-x86_64.AppImage" "openra/openra:linux-cnc" --userversion ${{ github.event.inputs.tag }}
- name: Publish D2k AppImage
uses: josephbmanley/butler-publish-itchio-action@master
env:
BUTLER_CREDENTIALS: ${{ secrets.BUTLER_CREDENTIALS }}
CHANNEL: linux-d2k
ITCH_GAME: openra
ITCH_USER: openra
VERSION: ${{ github.event.inputs.tag }}
PACKAGE: OpenRA-Dune-2000-x86_64.AppImage
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
run: |
wget -q "https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.tag }}/OpenRA-Dune-2000-x86_64.AppImage"
./butler push "OpenRA-Dune-2000-x86_64.AppImage" "openra/openra:linux-d2k" --userversion ${{ github.event.inputs.tag }}

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -40,12 +40,12 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
- name: Install .NET 8.0
uses: actions/setup-dotnet@v5
with:
dotnet-version: '6.0.x'
dotnet-version: '8.0.x'
- name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -53,29 +53,27 @@ jobs:
- name: Package AppImages
run: |
mkdir -p build/linux
sudo apt install libfuse2
sudo apt-get install -y desktop-file-utils
./packaging/linux/buildpackage.sh "${GIT_TAG}" "${PWD}/build/linux"
- name: Upload Packages
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
overwrite: true
file_glob: true
file: build/linux/*
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
gh release upload ${{ github.ref_name }} build/linux/*
macos:
name: macOS Disk Image
runs-on: macos-11
runs-on: macos-14
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
- name: Install .NET 8.0
uses: actions/setup-dotnet@v5
with:
dotnet-version: '6.0.x'
dotnet-version: '8.0.x'
- name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -92,25 +90,23 @@ jobs:
./packaging/macos/buildpackage.sh "${GIT_TAG}" "${PWD}/build/macos"
- name: Upload Package
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
overwrite: true
file_glob: true
file: build/macos/*
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
gh release upload ${{ github.ref_name }} build/macos/*
windows:
name: Windows Installers
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
- name: Install .NET 8.0
uses: actions/setup-dotnet@v5
with:
dotnet-version: '6.0.x'
dotnet-version: '8.0.x'
- name: Prepare Environment
run: |
@@ -123,11 +119,37 @@ jobs:
mkdir -p build/windows
./packaging/windows/buildpackage.sh "${GIT_TAG}" "${PWD}/build/windows"
- name: Upload Packages
uses: svenstaro/upload-release-action@v2
- name: Upload Installer
id: unsigned-artifact
if: github.repository == 'openra/openra'
uses: actions/upload-artifact@v6
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
overwrite: true
file_glob: true
file: build/windows/*
path: build/windows/*.exe
- name: Microsoft Authenticode
id: signpath
if: github.repository == 'openra/openra'
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '${{ secrets.SIGNPATH_ORGANISATION_ID }}'
project-slug: 'OpenRA'
signing-policy-slug: 'release-signing'
github-artifact-id: '${{ steps.unsigned-artifact.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'build/windows/signed/'
parameters: |
version: ${{ toJSON(github.ref_name) }}
- name: Upload Packages
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
gh release upload "${{ github.ref_name }}" build/windows/*.zip
if [ "${GITHUB_REPOSITORY}" = "openra/openra" ]; then
gh release upload "${{ github.ref_name }}" build/windows/signed/*.exe
else
gh release upload "${{ github.ref_name }}" build/windows/*.exe
fi

4
.gitignore vendored
View File

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

63
AUTHORS
View File

@@ -1,5 +1,4 @@
OpenRA wouldn't be where it is today without the
hard work of many contributors.
OpenRA wouldn't be where it is today without the hard work of many contributors.
The OpenRA developers are:
* Gustas Kažukauskas (PunkPun)
@@ -37,6 +36,7 @@ Also thanks to:
* Andreas Beck (baxtor)
* Ang Soon Li (asl97)
* Arik Lirette (Angusm3)
* Ashley Newson
* Barnaby Smith (mvi)
* Bellator
* Bernd Stellwag (burned42)
@@ -55,6 +55,7 @@ Also thanks to:
* Constantin Helmig (CH4Code)
* D2k Sardaukar
* D'Arcy Rush (r34ch)
* Dan Stoian (stoiandan)
* Daniel Derejvanik (Harisson)
* Danny Keary (Dan9550)
* David Jiménez (Rydra)
@@ -98,6 +99,7 @@ Also thanks to:
* Kanar
* Kenny Hoxworth (hoxworth)
* Kevin Azzam (ChaoticMind)
* Kevin Streser
* Krishnakanth Mallik
* Kyle Smith (Smitty)
* Kyrre Soerensen (zypres)
@@ -141,6 +143,7 @@ Also thanks to:
* Riderr3
* riiga
* Rikhardur Bjarni Einarsson (WolfGaming)
* Robert Nordan (robpvn)
* Sascha Biedermann (bidifx)
* Sean Hunt (coppro)
* Shawn Collins (UberWaffe)
@@ -150,6 +153,7 @@ Also thanks to:
* Teemu Nieminen (Temeez)
* Thomas Christlieb (ThomasChr)
* Tim Mylemans (gecko)
* Tinix
* Tirili
* Tomas Einarsson (Mesacer)
* Tom van Leth (tovl)
@@ -161,61 +165,42 @@ Also thanks to:
* Wojciech Walaszek (Voidwalker)
* Wuschel
Using GNU FreeFont distributed under the GNU GPL
terms.
Using GNU FreeFont distributed under the GNU GPL terms.
Using Simple DirectMedia Layer distributed under
the terms of the zlib license.
Using Simple DirectMedia Layer distributed under the terms of the zlib license.
Using FreeType distributed under the terms of the
FreeType License.
Using FreeType distributed under the terms of the FreeType License.
Using OpenAL Soft distributed under the GNU LGPL.
Using SDL2-CS and OpenAL-CS created by Ethan
Lee and released under the zlib license.
Using SDL2-CS and OpenAL-CS created by Ethan Lee and released under the zlib license.
Using Eluant created by Chris Howie and released
under the MIT license.
Using Eluant created by Chris Howie and released under the MIT license.
Using FuzzyLogicLibrary (fuzzynet) by Dmitry
Kaluzhny and released under the GNU GPL terms.
Using FuzzyLogicLibrary (fuzzynet) by Dmitry Kaluzhny and released under the GNU GPL terms.
Using Mono.Nat by Alan McGovern, Ben Motmans,
Nicholas Terry distributed under the MIT license.
Using Mono.Nat by Alan McGovern, Ben Motmans, Nicholas Terry distributed under the MIT license.
Using MP3Sharp by Robert Bruke and Zane Wagner
licensed under the GNU LGPL Version 3.
Using MP3Sharp by Robert Bruke and Zane Wagner licensed under the GNU LGPL Version 3.
Using TagLib# by Stephen Shaw licensed under the
GNU LGPL Version 2.1.
Using TagLib# by Stephen Shaw licensed under the GNU LGPL Version 2.1.
Using NVorbis by Andrew Ward distributed under
the MIT license.
Using NVorbis by Andrew Ward distributed under the MIT license.
Using ICSharpCode.SharpZipLib initially by Mike
Krueger and distributed under the GNU GPL terms.
Using ICSharpCode.SharpZipLib initially by Mike Krueger and distributed under the GNU GPL terms.
Using rix0rrr.BeaconLib developed by Rico Huijbers
distributed under MIT License.
Using rix0rrr.BeaconLib developed by Rico Huijbers distributed under MIT License.
Using DiscordRichPresence developed by Lachee
distributed under MIT License.
Using DiscordRichPresence developed by Lachee distributed under MIT License.
Using Json.NET developed by James Newton-King
distributed under MIT License.
Using Json.NET developed by James Newton-King distributed under MIT License.
Using ANGLE distributed under the BS3 3-Clause license.
Using Pfim developed by Nick Babcock
distributed under the MIT license.
Using Pfim developed by Nick Babcock distributed under the MIT license.
Using Linguini by the Space Station 14 team
licensed under Apache and MIT terms.
Using Linguini by the Space Station 14 team licensed under Apache and MIT terms.
This site or product includes IP2Location LITE data
available from http://www.ip2location.com.
This site or product includes IP2Location LITE data available from https://www.ip2location.com.
Finally, special thanks goes to the original teams
at Westwood Studios and EA for creating the classic
games which OpenRA aims to reimagine.
Finally, special thanks goes to the original teams at Westwood Studios and EA for creating the classic games which OpenRA aims to reimagine.

View File

@@ -70,7 +70,7 @@ members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
available at [https://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/4/

View File

@@ -16,7 +16,7 @@ Help us keep OpenRA open and inclusive. Please read and follow our [Code of Cond
* [Coding standard](https://github.com/OpenRA/OpenRA/wiki/Coding-Standard)
* [Branches and Releases](https://github.com/OpenRA/OpenRA/wiki/Branches-and-Releases)
* [Licensing](http://www.gnu.org/licenses/quick-guide-gplv3.html)
* [Licensing](https://www.gnu.org/licenses/quick-guide-gplv3.html)
Please `git rebase` to the latest revision of the bleed branch.
@@ -24,6 +24,6 @@ Don't forget to add yourself to [AUTHORS](https://github.com/OpenRA/OpenRA/blob/
Please propose a [CHANGELOG](https://github.com/OpenRA/OpenRA/wiki/CHANGELOG) entry in the pull-request comments.
While your pull-request is in review it will be helpful if you join [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.

View File

@@ -1,7 +1,7 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@@ -645,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found.
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
@@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
<https://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@@ -5,7 +5,7 @@
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Optimize>true</Optimize>
<LangVersion>9</LangVersion>
<LangVersion>12</LangVersion>
<DebugSymbols>true</DebugSymbols>
<EngineRootPath Condition="'$(EngineRootPath)' == ''">..</EngineRootPath>
<OutputPath>$(EngineRootPath)/bin</OutputPath>
@@ -18,8 +18,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework Condition="'$(MSBuildRuntimeType)'!='Mono'">net6.0</TargetFramework>
<TargetFramework Condition="'$(MSBuildRuntimeType)'=='Mono'">netstandard2.1</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
@@ -33,7 +32,7 @@
<Optimize>false</Optimize>
<!-- Enable only for Debug builds to improve compile-time performance for Release builds -->
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<!-- Enabling GenerateDocumentationFile is required for IDE0005 (Remove unnecessary import)
<!-- Enabling GenerateDocumentationFile is required for IDE0005 (Remove unnecessary import)
rule to run in command line builds. https://github.com/dotnet/roslyn/issues/41640
Enable only for Debug builds to improve compile-time performance for Release builds -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -51,8 +50,10 @@
</ItemGroup>
</Target>
<!-- StyleCop -->
<!-- StyleCop/Roslynator -->
<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" />
<PackageReference Include="Roslynator.Analyzers" Version="4.13.0" PrivateAssets="All" />
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.13.0" PrivateAssets="All" />
</ItemGroup>
</Project>

29
Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0
RUN \
apt-get update; \
apt-get -y upgrade; \
apt-get install -y --no-install-recommends \
curl \
wget \
make \
python3 \
unzip \
mono-complete
RUN useradd -d /home/openra -m -s /sbin/nologin openra
WORKDIR /home/openra
COPY . .
RUN chown -R openra:openra .
USER openra
RUN make
EXPOSE 1234
ENTRYPOINT ["./launch-dedicated.sh"]

View File

@@ -7,8 +7,8 @@ Windows
=======
Compiling OpenRA requires the following dependencies:
* [Windows PowerShell >= 4.0](http://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)
* [Windows PowerShell >= 4.0](https://microsoft.com/powershell) (included by default in recent Windows 10 versions)
* [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) (or via Visual Studio)
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,15 +17,13 @@ Run the game with `launch-game.cmd`. It can be handed arguments that specify the
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 (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.
To compile OpenRA, run `make` from the command line. After this one can run the game with `./launch-game.sh`. It is also possible to specify the mod you wish to run from the command line, e.g. with `./launch-game.sh Game.Mod=ts` if you wish to try the experimental Tiberian Sun mod.
The 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`.
If you choose to use system libraries, or your system is not x86_64, you will need to install [SDL 2](https://www.libsdl.org/download-2.0.php), [FreeType](http://gnuwin32.sourceforge.net/packages/freetype.htm), [OpenAL](https://openal-soft.org/), and [liblua 5.1](http://luabinaries.sourceforge.net/download.html) before compiling OpenRA.
If you choose to use system libraries, or your system is not x86_64, you will need to install [SDL 2](https://www.libsdl.org/download-2.0.php), [FreeType](https://gnuwin32.sourceforge.net/packages/freetype.htm), [OpenAL](https://openal-soft.org/), and [liblua 5.1](https://luabinaries.sourceforge.net/download.html) before compiling OpenRA.
These can be installed using your package manager on various distros:
@@ -78,6 +76,6 @@ Type `sudo make install` for system-wide installation. Run `sudo make install-li
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:
# make
#
# to compile using Mono (version 6.12 or greater) instead of .NET 6, run:
# make RUNTIME=mono
#
# to compile using system libraries for native dependencies, run:
# make [RUNTIME=net6] TARGETPLATFORM=unix-generic
# make TARGETPLATFORM=unix-generic
#
# 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:
# make [RUNTIME=net6] check
# make check
#
# 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
# using system libraries for native dependencies, run:
@@ -48,15 +45,12 @@ gameinstalldir ?= $(libdir)/openra
# Toolchain
CWD = $(shell pwd)
MSBUILD = msbuild -verbosity:m -nologo
DOTNET = dotnet
MONO = mono
RM = rm
RM_R = $(RM) -r
RM_F = $(RM) -f
RM_RF = $(RM) -rf
RUNTIME ?= net6
CONFIGURATION ?= Release
DOTNET_RID = $(shell ${DOTNET} --info | grep RID: | cut -w -f3)
ARCH_X64 = $(shell echo ${DOTNET_RID} | grep x64)
@@ -91,18 +85,12 @@ endif
#
all:
@echo "Compiling in ${CONFIGURATION} mode..."
ifeq ($(RUNTIME), mono)
@command -v $(firstword $(MSBUILD)) >/dev/null || (echo "OpenRA requires the '$(MSBUILD)' tool provided by Mono >= 6.12."; exit 1)
@$(MSBUILD) -t:Build -restore -p:Configuration=${CONFIGURATION} -p:TargetPlatform=$(TARGETPLATFORM)
else
@$(DOTNET) build -c ${CONFIGURATION} -nologo -p:TargetPlatform=$(TARGETPLATFORM)
endif
ifeq ($(TARGETPLATFORM), unix-generic)
@./configure-system-libraries.sh
endif
@./fetch-geoip.sh
# dotnet clean and msbuild -t:Clean leave files that cause problems when switching between mono/dotnet
# Deleting the intermediate / output directories ensures the build directory is actually clean
clean:
@-$(RM_RF) ./bin ./*/obj
@@ -111,12 +99,8 @@ clean:
check:
@echo
@echo "Compiling in Debug mode..."
ifeq ($(RUNTIME), mono)
@$(MSBUILD) -t:clean\;build -restore -p:Configuration=Debug -warnaserror -p:TargetPlatform=$(TARGETPLATFORM)
else
@$(DOTNET) clean -c Debug --nologo --verbosity minimal
@$(DOTNET) build -c Debug -nologo -warnaserror -p:TargetPlatform=$(TARGETPLATFORM)
endif
ifeq ($(TARGETPLATFORM), unix-generic)
@./configure-system-libraries.sh
endif
@@ -135,15 +119,19 @@ check-scripts:
test: all
@echo
@echo "Testing Tiberian Sun mod MiniYAML..."
@./utility.sh ts-content --check-yaml
@./utility.sh ts --check-yaml
@echo
@echo "Testing Dune 2000 mod MiniYAML..."
@./utility.sh d2k-content --check-yaml
@./utility.sh d2k --check-yaml
@echo
@echo "Testing Tiberian Dawn mod MiniYAML..."
@./utility.sh cnc-content --check-yaml
@./utility.sh cnc --check-yaml
@echo
@echo "Testing Red Alert mod MiniYAML..."
@./utility.sh ra-content --check-yaml
@./utility.sh ra --check-yaml
tests:
@@ -153,15 +141,15 @@ tests:
############# LOCAL INSTALLATION AND DOWNSTREAM PACKAGING ##############
#
version: VERSION mods/ra/mod.yaml mods/cnc/mod.yaml mods/d2k/mod.yaml mods/ts/mod.yaml mods/modcontent/mod.yaml mods/all/mod.yaml
version: VERSION mods/*/mod.yaml
ifeq ($(VERSION),)
$(error Unable to determine new version (requires git or override of variable VERSION))
endif
@sh -c '. ./packaging/functions.sh; set_engine_version "$(VERSION)" .'
@sh -c '. ./packaging/functions.sh; set_mod_version "$(VERSION)" mods/ra/mod.yaml mods/cnc/mod.yaml mods/d2k/mod.yaml mods/ts/mod.yaml mods/modcontent/mod.yaml mods/all/mod.yaml'
@sh -c '. ./packaging/functions.sh; set_mod_version "$(VERSION)" mods/*/mod.yaml'
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'
install-linux-shortcuts:
@@ -178,24 +166,21 @@ help:
@echo 'to compile, run:'
@echo ' make'
@echo
@echo 'to compile using Mono (version 6.12 or greater) instead of .NET 6, run:'
@echo ' make RUNTIME=mono'
@echo
@echo 'to compile using system libraries for native dependencies, run:'
@echo ' make [RUNTIME=net6] TARGETPLATFORM=unix-generic'
@echo ' make TARGETPLATFORM=unix-generic'
@echo
@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 'to check the engine and official mod dlls for code style violations, run:'
@echo ' make [RUNTIME=net6] check'
@echo ' make check'
@echo
@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 'to compile and install Red Alert, Tiberian Dawn, and Dune 2000'
@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 'to install FreeDesktop startup scripts, desktop files, icons, and MIME metadata'
@echo ' make install-linux-shortcuts'

View File

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

View File

@@ -71,7 +71,12 @@ namespace OpenRA
public IEffectiveOwner EffectiveOwner { get; }
public IOccupySpace OccupiesSpace { get; }
public ITargetable[] Targetables { get; }
public IEnumerable<ITargetablePositions> EnabledTargetablePositions { get; private set; }
public IEnumerable<ITargetablePositions> EnabledTargetablePositions { get; }
readonly ICrushable[] crushables;
public ICrushable[] Crushables
{
get => crushables ?? throw new InvalidOperationException($"Crushables for {Info.Name} are not initialized.");
}
public bool IsIdle => CurrentActivity == null;
public bool IsDead => Disposed || (health != null && health.IsDead);
@@ -84,21 +89,21 @@ namespace OpenRA
sealed class ConditionState
{
/// <summary>Delegates that have registered to be notified when this condition changes.</summary>
public readonly List<VariableObserverNotifier> Notifiers = new();
public readonly List<VariableObserverNotifier> Notifiers = [];
/// <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>
readonly Dictionary<int, string> conditionTokens = new();
readonly Dictionary<int, string> conditionTokens = [];
int nextConditionToken = 1;
/// <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>
readonly IReadOnlyDictionary<string, int> readOnlyConditionCache;
@@ -118,6 +123,9 @@ namespace OpenRA
readonly IEnumerable<WPos> enabledTargetableWorldPositions;
bool created;
IEnumerable<IRenderable> renderables;
WorldRenderer lastWorldRenderer;
internal Actor(World world, string name, TypeDictionary initDict)
{
var duplicateInit = initDict.WithInterface<ISingleInstanceInit>().GroupBy(i => i.GetType())
@@ -155,6 +163,7 @@ namespace OpenRA
var targetablesList = new List<ITargetable>();
var targetablePositionsList = new List<ITargetablePositions>();
var syncHashesList = new List<SyncHash>();
var crushablesList = new List<ICrushable>();
foreach (var traitInfo in Info.TraitsInConstructOrder())
{
@@ -181,6 +190,7 @@ namespace OpenRA
{ if (trait is ITargetable t) targetablesList.Add(t); }
{ if (trait is ITargetablePositions t) targetablePositionsList.Add(t); }
{ if (trait is ISync t) syncHashesList.Add(new SyncHash(t)); }
{ if (trait is ICrushable t) crushablesList.Add(t); }
}
resolveOrders = resolveOrdersList.ToArray();
@@ -195,6 +205,7 @@ namespace OpenRA
EnabledTargetablePositions = targetablePositions.Where(Exts.IsTraitEnabled);
enabledTargetableWorldPositions = EnabledTargetablePositions.SelectMany(tp => tp.TargetablePositions(this));
SyncHashes = syncHashesList.ToArray();
crushables = crushablesList.ToArray();
}
}
@@ -279,11 +290,18 @@ namespace OpenRA
public IEnumerable<IRenderable> Render(WorldRenderer wr)
{
if (lastWorldRenderer != wr)
{
// PERF: Cache the enumerable to reduce allocations.
lastWorldRenderer = wr;
renderables = Renderables(wr);
}
// PERF: Avoid LINQ.
var renderables = Renderables(wr);
var modifiedRenderables = renderables;
foreach (var modifier in renderModifiers)
renderables = modifier.ModifyRender(this, wr, renderables);
return renderables;
modifiedRenderables = modifier.ModifyRender(this, wr, modifiedRenderables);
return modifiedRenderables;
}
IEnumerable<IRenderable> Renderables(WorldRenderer wr)
@@ -496,7 +514,7 @@ namespace OpenRA
if (!visibilityModifier.IsVisible(this, player))
return false;
return defaultVisibility.IsVisible(this, player);
return defaultVisibility?.IsVisible(this, player) ?? true;
}
public BitSet<TargetableType> GetAllTargetTypes()
@@ -533,7 +551,7 @@ namespace OpenRA
if (EnabledTargetablePositions.Any())
return enabledTargetableWorldPositions;
return new[] { CenterPosition };
return [CenterPosition];
}
#region Conditions

View File

@@ -16,7 +16,8 @@ using OpenRA.Scripting;
namespace OpenRA
{
public readonly struct CPos : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding, IEquatable<CPos>
public readonly struct CPos : IEquatable<CPos>, IScriptBindable,
ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaTableBinding, ILuaToStringBinding
{
// Coordinates are packed in a 32 bit signed int
// X and Y are 12 bits (signed): -2048...2047
@@ -77,17 +78,9 @@ namespace OpenRA
return new MPos(X, Y);
// Convert from RectangularIsometric cell (x, y) position to rectangular map position (u, v)
// - The staggered rows make this fiddly (hint: draw a diagram!)
// (a) Consider the relationships:
// - +1x (even -> odd) adds (0, 1) to (u, v)
// - +1x (odd -> even) adds (1, 1) to (u, v)
// - +1y (even -> odd) adds (-1, 1) to (u, v)
// - +1y (odd -> even) adds (0, 1) to (u, v)
// (b) Therefore:
// - ax + by adds (a - b)/2 to u (only even increments count)
// - ax + by adds a + b to v
var u = (X - Y) / 2;
// The staggered rows make this fiddly.
var v = X + Y;
var u = (v - (v & 1)) / 2 - Y;
return new MPos(u, v);
}
@@ -96,7 +89,8 @@ namespace OpenRA
public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right)
{
if (!left.TryGetClrValue(out CPos a) || !right.TryGetClrValue(out CVec b))
throw new LuaException($"Attempted to call CPos.Add(CPos, CVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
throw new LuaException("Attempted to call CPos.Add(CPos, CVec) with invalid arguments " +
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
return new LuaCustomClrObject(a + b);
}
@@ -105,7 +99,8 @@ namespace OpenRA
{
var rightType = right.WrappedClrType();
if (!left.TryGetClrValue(out CPos a))
throw new LuaException($"Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments ({left.WrappedClrType().Name}, {rightType.Name})");
throw new LuaException("Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments " +
$"({left.WrappedClrType().Name}, {rightType.Name})");
if (rightType == typeof(CPos))
{
@@ -118,7 +113,8 @@ namespace OpenRA
return new LuaCustomClrObject(a - b);
}
throw new LuaException($"Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments ({left.WrappedClrType().Name}, {rightType.Name})");
throw new LuaException("Attempted to call CPos.Subtract(CPos, (CPos|CVec)) with invalid arguments " +
$"({left.WrappedClrType().Name}, {rightType.Name})");
}
public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right)
@@ -145,6 +141,8 @@ namespace OpenRA
set => throw new LuaException("CPos is read-only. Use CPos.New to create a new value");
}
public LuaValue ToString(LuaRuntime runtime) => ToString();
#endregion
}
}

View File

@@ -17,7 +17,9 @@ using OpenRA.Scripting;
namespace OpenRA
{
public readonly struct CVec : IScriptBindable, ILuaAdditionBinding, ILuaSubtractionBinding, ILuaUnaryMinusBinding, ILuaEqualityBinding, ILuaTableBinding, IEquatable<CVec>
public readonly struct CVec : IEquatable<CVec>, IScriptBindable,
ILuaAdditionBinding, ILuaSubtractionBinding, ILuaEqualityBinding, ILuaUnaryMinusBinding,
ILuaMultiplicationBinding, ILuaDivisionBinding, ILuaTableBinding, ILuaToStringBinding
{
public readonly int X, Y;
@@ -60,23 +62,24 @@ namespace OpenRA
public override string ToString() { return X + "," + Y; }
public static readonly CVec[] Directions =
{
new CVec(-1, -1),
new CVec(-1, 0),
new CVec(-1, 1),
new CVec(0, -1),
new CVec(0, 1),
new CVec(1, -1),
new CVec(1, 0),
new CVec(1, 1),
};
[
new(-1, -1),
new(-1, 0),
new(-1, 1),
new(0, -1),
new(0, 1),
new(1, -1),
new(1, 0),
new(1, 1),
];
#region Scripting interface
public LuaValue Add(LuaRuntime runtime, LuaValue left, LuaValue right)
{
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b))
throw new LuaException($"Attempted to call CVec.Add(CVec, CVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
throw new LuaException("Attempted to call CVec.Add(CVec, CVec) with invalid arguments " +
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
return new LuaCustomClrObject(a + b);
}
@@ -84,16 +87,12 @@ namespace OpenRA
public LuaValue Subtract(LuaRuntime runtime, LuaValue left, LuaValue right)
{
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b))
throw new LuaException($"Attempted to call CVec.Subtract(CVec, CVec) with invalid arguments ({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
throw new LuaException("Attempted to call CVec.Subtract(CVec, CVec) with invalid arguments " +
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
return new LuaCustomClrObject(a - b);
}
public LuaValue Minus(LuaRuntime runtime)
{
return new LuaCustomClrObject(-this);
}
public LuaValue Equals(LuaRuntime runtime, LuaValue left, LuaValue right)
{
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out CVec b))
@@ -102,6 +101,29 @@ namespace OpenRA
return a == b;
}
public LuaValue Minus(LuaRuntime runtime)
{
return new LuaCustomClrObject(-this);
}
public LuaValue Multiply(LuaRuntime runtime, LuaValue left, LuaValue right)
{
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out int b))
throw new LuaException("Attempted to call CVec.Multiply(CVec, integer) with invalid arguments " +
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
return new LuaCustomClrObject(a * b);
}
public LuaValue Divide(LuaRuntime runtime, LuaValue left, LuaValue right)
{
if (!left.TryGetClrValue(out CVec a) || !right.TryGetClrValue(out int b))
throw new LuaException("Attempted to call CVec.Multiply(CVec, integer) with invalid arguments " +
$"({left.WrappedClrType().Name}, {right.WrappedClrType().Name})");
return new LuaCustomClrObject(a / b);
}
public LuaValue this[LuaRuntime runtime, LuaValue key]
{
get
@@ -110,6 +132,7 @@ namespace OpenRA
{
case "X": return X;
case "Y": return Y;
case "Length": return Length;
default: throw new LuaException($"CVec does not define a member '{key}'");
}
}
@@ -117,6 +140,8 @@ namespace OpenRA
set => throw new LuaException("CVec is read-only. Use CVec.New to create a new value");
}
public LuaValue ToString(LuaRuntime runtime) => ToString();
#endregion
}
}

View File

@@ -20,7 +20,10 @@ namespace OpenRA
public static class CryptoUtil
{
// Fixed byte pattern for the OID header
static readonly byte[] OIDHeader = { 0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0 };
static readonly byte[] OIDHeader = [0x30, 0xD, 0x6, 0x9, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0];
static readonly char[] HexUpperAlphabet = "0123456789ABCDEF".ToArray();
static readonly char[] HexLowerAlphabet = "0123456789abcdef".ToArray();
public static string PublicKeyFingerprint(RSAParameters parameters)
{
@@ -53,33 +56,33 @@ namespace OpenRA
using (var s = new MemoryStream(data))
{
// SEQUENCE
s.ReadByte();
s.ReadUInt8();
ReadTLVLength(s);
// SEQUENCE -> fixed header junk
s.ReadByte();
s.ReadUInt8();
var headerLength = ReadTLVLength(s);
s.Position += headerLength;
// SEQUENCE -> BIT_STRING
s.ReadByte();
s.ReadUInt8();
ReadTLVLength(s);
s.ReadByte();
s.ReadUInt8();
// SEQUENCE -> BIT_STRING -> SEQUENCE
s.ReadByte();
s.ReadUInt8();
ReadTLVLength(s);
// SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (modulus)
s.ReadByte();
s.ReadUInt8();
var modulusLength = ReadTLVLength(s);
s.ReadByte();
s.ReadUInt8();
var modulus = s.ReadBytes(modulusLength - 1);
// SEQUENCE -> BIT_STRING -> SEQUENCE -> INTEGER (exponent)
s.ReadByte();
s.ReadUInt8();
var exponentLength = ReadTLVLength(s);
s.ReadByte();
s.ReadUInt8();
var exponent = s.ReadBytes(exponentLength - 1);
return new RSAParameters
@@ -158,13 +161,13 @@ namespace OpenRA
static int ReadTLVLength(Stream s)
{
var length = s.ReadByte();
var length = s.ReadUInt8();
if (length < 0x80)
return length;
var data = new byte[4];
s.ReadBytes(data, 0, Math.Min(length & 0x7F, 4));
return BitConverter.ToInt32(data.ToArray(), 0);
Span<byte> data = stackalloc byte[4];
s.ReadBytes(data[..Math.Min(length & 0x7F, 4)]);
return BitConverter.ToInt32(data);
}
static int TripletFullLength(int dataLength)
@@ -207,8 +210,7 @@ namespace OpenRA
using (var rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(parameters);
using (var csp = SHA1.Create())
return Convert.ToBase64String(rsa.SignHash(csp.ComputeHash(data), CryptoConfig.MapNameToOID("SHA1")));
return Convert.ToBase64String(rsa.SignHash(SHA1.HashData(data), CryptoConfig.MapNameToOID("SHA1")));
}
}
catch (Exception e)
@@ -233,8 +235,7 @@ namespace OpenRA
using (var rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(parameters);
using (var csp = SHA1.Create())
return rsa.VerifyHash(csp.ComputeHash(data), CryptoConfig.MapNameToOID("SHA1"), Convert.FromBase64String(signature));
return rsa.VerifyHash(SHA1.HashData(data), CryptoConfig.MapNameToOID("SHA1"), Convert.FromBase64String(signature));
}
}
catch (Exception e)
@@ -249,19 +250,42 @@ namespace OpenRA
public static string SHA1Hash(Stream data)
{
using (var csp = SHA1.Create())
return new string(csp.ComputeHash(data).SelectMany(a => a.ToString("x2")).ToArray());
return ToHex(SHA1.HashData(data), true);
}
public static string SHA1Hash(byte[] data)
{
using (var csp = SHA1.Create())
return new string(csp.ComputeHash(data).SelectMany(a => a.ToString("x2")).ToArray());
return ToHex(SHA1.HashData(data), true);
}
public static string SHA1Hash(string data)
{
return SHA1Hash(Encoding.UTF8.GetBytes(data));
}
public static string ToHex(ReadOnlySpan<byte> source, bool lowerCase = false)
{
if (source.Length == 0)
return string.Empty;
// excessively avoid stack overflow if source is too large (considering that we're allocating a new string)
var buffer = source.Length <= 256 ? stackalloc char[source.Length * 2] : new char[source.Length * 2];
return ToHexInternal(source, buffer, lowerCase);
}
static string ToHexInternal(ReadOnlySpan<byte> source, Span<char> buffer, bool lowerCase)
{
var sourceIndex = 0;
var alphabet = lowerCase ? HexLowerAlphabet : HexUpperAlphabet;
for (var i = 0; i < buffer.Length; i += 2)
{
var b = source[sourceIndex++];
buffer[i] = alphabet[b >> 4];
buffer[i + 1] = alphabet[b & 0xF];
}
return new string(buffer);
}
}
}

View File

@@ -0,0 +1,22 @@
#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;
namespace OpenRA
{
// Mirrors DescriptionAttribute from System.ComponentModel but we don't want to have to use that everywhere.
[AttributeUsage(AttributeTargets.All)]
public sealed class DescAttribute(params string[] lines) : Attribute
{
public readonly string[] Lines = lines;
}
}

View File

@@ -12,6 +12,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using OpenRA.FileFormats;
@@ -27,9 +28,8 @@ namespace OpenRA
{
public readonly string Id;
public readonly string Version;
public readonly string Title;
public readonly string LaunchPath;
public readonly string[] LaunchArgs;
public readonly ImmutableArray<string> LaunchArgs;
public Sprite Icon { get; internal set; }
public Sprite Icon2x { get; internal set; }
public Sprite Icon3x { get; internal set; }
@@ -41,7 +41,7 @@ namespace OpenRA
public class ExternalMods : IReadOnlyDictionary<string, ExternalMod>
{
readonly Dictionary<string, ExternalMod> mods = new();
readonly Dictionary<string, ExternalMod> mods = [];
readonly SheetBuilder sheetBuilder;
Sheet CreateSheet()
@@ -66,6 +66,7 @@ namespace OpenRA
// Several types of support directory types are available, depending on
// how the player has installed and launched the game.
// Read registration metadata from all of them
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
foreach (var source in GetSupportDirs(ModRegistration.User | ModRegistration.System))
{
var metadataPath = Path.Combine(source, "ModMetadata");
@@ -76,7 +77,7 @@ namespace OpenRA
{
try
{
var yaml = MiniYaml.FromStream(File.OpenRead(path), path).First().Value;
var yaml = MiniYaml.FromFile(path, stringPool: stringPool).First().Value;
LoadMod(yaml, path);
}
catch (Exception e)
@@ -94,17 +95,17 @@ namespace OpenRA
if (sheetBuilder != null)
{
var iconNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon");
var iconNode = yaml.NodeWithKeyOrDefault("Icon");
if (iconNode != null && !string.IsNullOrEmpty(iconNode.Value.Value))
using (var stream = new MemoryStream(Convert.FromBase64String(iconNode.Value.Value)))
mod.Icon = sheetBuilder.Add(new Png(stream));
var icon2xNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon2x");
var icon2xNode = yaml.NodeWithKeyOrDefault("Icon2x");
if (icon2xNode != null && !string.IsNullOrEmpty(icon2xNode.Value.Value))
using (var stream = new MemoryStream(Convert.FromBase64String(icon2xNode.Value.Value)))
mod.Icon2x = sheetBuilder.Add(new Png(stream), 1f / 2);
var icon3xNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon3x");
var icon3xNode = yaml.NodeWithKeyOrDefault("Icon3x");
if (icon3xNode != null && !string.IsNullOrEmpty(icon3xNode.Value.Value))
using (var stream = new MemoryStream(Convert.FromBase64String(icon3xNode.Value.Value)))
mod.Icon3x = sheetBuilder.Add(new Png(stream), 1f / 3);
@@ -122,26 +123,29 @@ namespace OpenRA
return;
var key = ExternalMod.MakeKey(mod);
var yaml = new MiniYamlNode("Registration", new MiniYaml("", new List<MiniYamlNode>()
{
var yaml = new MiniYamlNode("Registration", new MiniYaml("",
[
new MiniYamlNode("Id", mod.Id),
new MiniYamlNode("Version", mod.Metadata.Version),
new MiniYamlNode("Title", mod.Metadata.Title),
new MiniYamlNode("LaunchPath", launchPath),
new MiniYamlNode("LaunchArgs", new[] { "Game.Mod=" + mod.Id }.Concat(launchArgs).JoinWith(", "))
}));
]));
var iconNodes = new List<MiniYamlNode>();
using (var stream = mod.Package.GetStream("icon.png"))
if (stream != null)
yaml.Value.Nodes.Add(new MiniYamlNode("Icon", Convert.ToBase64String(stream.ReadAllBytes())));
iconNodes.Add(new MiniYamlNode("Icon", Convert.ToBase64String(stream.ReadAllBytes())));
using (var stream = mod.Package.GetStream("icon-2x.png"))
if (stream != null)
yaml.Value.Nodes.Add(new MiniYamlNode("Icon2x", Convert.ToBase64String(stream.ReadAllBytes())));
iconNodes.Add(new MiniYamlNode("Icon2x", Convert.ToBase64String(stream.ReadAllBytes())));
using (var stream = mod.Package.GetStream("icon-3x.png"))
if (stream != null)
yaml.Value.Nodes.Add(new MiniYamlNode("Icon3x", Convert.ToBase64String(stream.ReadAllBytes())));
iconNodes.Add(new MiniYamlNode("Icon3x", Convert.ToBase64String(stream.ReadAllBytes())));
yaml = yaml.WithValue(yaml.Value.WithNodesAppended(iconNodes));
var sources = new HashSet<string>();
if (registration.HasFlag(ModRegistration.System))
@@ -201,7 +205,7 @@ namespace OpenRA
string modKey = null;
try
{
var yaml = MiniYaml.FromStream(File.OpenRead(path), path).First().Value;
var yaml = MiniYaml.FromFile(path).First().Value;
var m = FieldLoader.Load<ExternalMod>(yaml);
modKey = ExternalMod.MakeKey(m);

View File

@@ -11,9 +11,12 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using OpenRA.Primitives;
using OpenRA.Support;
using OpenRA.Traits;
@@ -22,15 +25,24 @@ namespace OpenRA
{
public static class Exts
{
public static bool IsUppercase(this string str)
/// <summary>Returns <see cref="Color"/> of the <paramref name="actor"/>, taking <see cref="Actor.EffectiveOwner"/> into account.</summary>
public static Color OwnerColor(this Actor actor)
{
return string.Compare(str.ToUpperInvariant(), str, false) == 0;
var effectiveOwner = actor.EffectiveOwner;
if (effectiveOwner != null && effectiveOwner.Disguised && actor.World.RenderPlayer != null)
return effectiveOwner.Owner.Color;
return actor.Owner.Color;
}
public static T WithDefault<T>(T def, Func<T> f)
public static string FormatInvariant(this string format, params object[] args)
{
try { return f(); }
catch { return def; }
return string.Format(CultureInfo.InvariantCulture, format, args);
}
public static string FormatCurrent(this string format, params object[] args)
{
return string.Format(CultureInfo.CurrentCulture, format, args);
}
public static Lazy<T> Lazy<T>(Func<T> p) { return new Lazy<T>(p); }
@@ -67,7 +79,7 @@ namespace OpenRA
return Math.Sign((v1.X - v0.X) * (p.Y - v0.Y) - (p.X - v0.X) * (v1.Y - v0.Y));
}
public static bool PolygonContains(this int2[] polygon, int2 p)
public static bool PolygonContains(this ImmutableArray<int2> polygon, int2 p)
{
var windingNumber = 0;
@@ -109,17 +121,11 @@ namespace OpenRA
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, V v)
{
#if NET5_0_OR_GREATER
// SAFETY: Dictionary cannot be modified whilst the ref is alive.
ref var value = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(d, k, out var exists);
ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(d, k, out var exists);
if (!exists)
value = v;
return value;
#else
if (!d.TryGetValue(k, out var ret))
d.Add(k, ret = v);
return ret;
#endif
}
public static V GetOrAdd<K, V>(this Dictionary<K, V> d, K k, Func<K, V> createFn)
@@ -131,11 +137,35 @@ namespace OpenRA
return ret;
}
public static T GetOrAdd<T>(this HashSet<T> set, T value)
{
if (!set.TryGetValue(value, out var ret))
set.Add(ret = value);
return ret;
}
public static T GetOrAdd<T>(this HashSet<T> set, T value, Func<T, T> createFn)
{
if (!set.TryGetValue(value, out var ret))
set.Add(ret = createFn(value));
return ret;
}
public static int IndexOf<T>(this T[] array, T value)
{
return Array.IndexOf(array, value);
}
public static T FirstOrDefault<T>(this T[] array, Predicate<T> match)
{
return Array.Find(array, match);
}
public static T FirstOrDefault<T>(this List<T> list, Predicate<T> match)
{
return list.Find(match);
}
public static T Random<T>(this IEnumerable<T> ts, MersenneTwister r)
{
return Random(ts, r, true);
@@ -148,7 +178,7 @@ namespace OpenRA
static T Random<T>(IEnumerable<T> ts, MersenneTwister r, bool throws)
{
var xs = ts as ICollection<T>;
var xs = ts as IReadOnlyCollection<T>;
xs ??= ts.ToList();
if (xs.Count == 0)
{
@@ -303,9 +333,9 @@ namespace OpenRA
// Adjust for other rounding modes
if (round == ISqrtRoundMode.Nearest && remainder > root)
root += 1;
root++;
else if (round == ISqrtRoundMode.Ceiling && root * root < number)
root += 1;
root++;
return root;
}
@@ -344,9 +374,9 @@ namespace OpenRA
// Adjust for other rounding modes
if (round == ISqrtRoundMode.Nearest && remainder > root)
root += 1;
root++;
else if (round == ISqrtRoundMode.Ceiling && root * root < number)
root += 1;
root++;
return root;
}
@@ -356,6 +386,11 @@ namespace OpenRA
return number * 46341 / 32768;
}
public static int MultiplyBySqrtTwoOverTwo(int number)
{
return (int)(number * 23170L / 32768L);
}
public static int IntegerDivisionRoundingAwayFromZero(int dividend, int divisor)
{
var quotient = Math.DivRem(dividend, divisor, out var remainder);
@@ -379,11 +414,6 @@ namespace OpenRA
return ts.Except(exclusions);
}
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source)
{
return new HashSet<T>(source);
}
public static Dictionary<TKey, TSource> ToDictionaryWithConflictLog<TSource, TKey>(
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
string debugName, Func<TKey, string> logKey, Func<TSource, string> logValue)
@@ -394,15 +424,26 @@ namespace OpenRA
public static Dictionary<TKey, TElement> ToDictionaryWithConflictLog<TSource, TKey, TElement>(
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector,
string debugName, Func<TKey, string> logKey = null, Func<TElement, string> logValue = null)
{
var output = new Dictionary<TKey, TElement>();
IntoDictionaryWithConflictLog(source, keySelector, elementSelector, debugName, output, logKey, logValue);
return output;
}
public static void IntoDictionaryWithConflictLog<TSource, TKey, TElement>(
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector,
string debugName, Dictionary<TKey, TElement> output,
Func<TKey, string> logKey = null, Func<TElement, string> logValue = null)
{
// Fall back on ToString() if null functions are provided:
logKey ??= s => s.ToString();
logValue ??= s => s.ToString();
// Try to build a dictionary and log all duplicates found (if any):
var dupKeys = new Dictionary<TKey, List<string>>();
Dictionary<TKey, List<string>> dupKeys = null;
var capacity = source is ICollection<TSource> collection ? collection.Count : 0;
var d = new Dictionary<TKey, TElement>(capacity);
output.Clear();
output.EnsureCapacity(capacity);
foreach (var item in source)
{
var key = keySelector(item);
@@ -413,15 +454,16 @@ namespace OpenRA
continue;
// Check for a key conflict:
if (!d.TryAdd(key, element))
if (!output.TryAdd(key, element))
{
dupKeys ??= [];
if (!dupKeys.TryGetValue(key, out var dupKeyMessages))
{
// Log the initial conflicting value already inserted:
dupKeyMessages = new List<string>
{
logValue(d[key])
};
dupKeyMessages =
[
logValue(output[key])
];
dupKeys.Add(key, dupKeyMessages);
}
@@ -431,15 +473,14 @@ namespace OpenRA
}
// If any duplicates were found, throw a descriptive error
if (dupKeys.Count > 0)
if (dupKeys != null)
{
var badKeysFormatted = string.Join(", ", dupKeys.Select(p => $"{logKey(p.Key)}: [{string.Join(",", p.Value)}]"));
var msg = $"{debugName}, duplicate values found for the following keys: {badKeysFormatted}";
throw new ArgumentException(msg);
var badKeysFormatted = new StringBuilder(
$"{debugName}, duplicate values found for the following keys: ");
foreach (var p in dupKeys)
badKeysFormatted.Append(CultureInfo.InvariantCulture, $"{logKey(p.Key)}: [{string.Join(",", p.Value)}]");
throw new ArgumentException(badKeysFormatted.ToString());
}
// Return the dictionary we built:
return d;
}
public static Color ColorLerp(float t, Color c1, Color c2)
@@ -460,50 +501,43 @@ namespace OpenRA
return result;
}
public static T[,] ResizeArray<T>(T[,] ts, T t, int width, int height)
{
var result = new T[width, height];
for (var i = 0; i < width; i++)
{
for (var j = 0; j < height; j++)
{
// Workaround for broken ternary operators in certain versions of mono
// (3.10 and certain versions of the 3.8 series): https://bugzilla.xamarin.com/show_bug.cgi?id=23319
if (i <= ts.GetUpperBound(0) && j <= ts.GetUpperBound(1))
result[i, j] = ts[i, j];
else
result[i, j] = t;
}
}
return result;
}
public static int ToBits(this IEnumerable<bool> bits)
{
var i = 0;
var result = 0;
foreach (var b in bits)
if (b)
result |= 1 << i++;
else
i++;
if (i > 33)
throw new InvalidOperationException("ToBits only accepts up to 32 values.");
return result;
}
public static int ParseIntegerInvariant(string s)
{
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static byte ParseByte(string s)
public static byte ParseByteInvariant(string s)
{
return byte.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static bool TryParseIntegerInvariant(string s, out int i)
public static ushort ParseUshortInvariant(string s)
{
return ushort.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static short ParseInt16Invariant(string s)
{
return short.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static int ParseInt32Invariant(string s)
{
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static float ParseFloatOrPercentInvariant(string s)
{
var f = float.Parse(s.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo);
return f * (s.Contains('%') ? 0.01f : 1f);
}
public static bool TryParseByteInvariant(string s, out byte i)
{
return byte.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
public static bool TryParseUshortInvariant(string s, out ushort i)
{
return ushort.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
public static bool TryParseInt32Invariant(string s, out int i)
{
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
@@ -513,6 +547,47 @@ namespace OpenRA
return long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
public static bool TryParseFloatOrPercentInvariant(string s, out float f)
{
if (float.TryParse(s?.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out f))
{
f *= s.Contains('%') ? 0.01f : 1f;
return true;
}
return false;
}
public static string ToStringInvariant(this byte i)
{
return i.ToString(NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this byte i, string format)
{
return i.ToString(format, NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this int i)
{
return i.ToString(NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this uint i)
{
return i.ToString(NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this float f)
{
return f.ToString(NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this int i, string format)
{
return i.ToString(format, NumberFormatInfo.InvariantInfo);
}
public static bool IsTraitEnabled<T>(this T trait)
{
return trait is not IDisabledTrait disabledTrait || !disabledTrait.IsTraitDisabled;
@@ -576,7 +651,7 @@ namespace OpenRA
Current = default;
}
public LineSplitEnumerator GetEnumerator() => this;
public readonly LineSplitEnumerator GetEnumerator() => this;
public bool MoveNext()
{
@@ -590,7 +665,7 @@ namespace OpenRA
if (index == -1)
{
// The remaining string is an empty string
str = ReadOnlySpan<char>.Empty;
str = [];
Current = span;
return true;
}
@@ -602,27 +677,4 @@ namespace OpenRA
public ReadOnlySpan<char> Current { get; private set; }
}
public static class Enum<T>
{
public static T Parse(string s) { return (T)Enum.Parse(typeof(T), s); }
public static T[] GetValues() { return (T[])Enum.GetValues(typeof(T)); }
public static bool TryParse(string s, bool ignoreCase, out T value)
{
// The string may be a comma delimited list of values
var names = ignoreCase ? Enum.GetNames(typeof(T)).Select(x => x.ToLowerInvariant()) : Enum.GetNames(typeof(T));
var values = ignoreCase ? s.Split(',').Select(x => x.Trim().ToLowerInvariant()) : s.Split(',').Select(x => x.Trim());
if (values.Any(x => !names.Contains(x)))
{
value = default;
return false;
}
value = (T)Enum.Parse(typeof(T), s, ignoreCase);
return true;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,55 +10,57 @@
#endregion
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using OpenRA.Primitives;
namespace OpenRA
{
public static class FieldSaver
{
public static MiniYaml Save(object o, bool includePrivateByDefault = false)
public static MiniYaml Save(object o)
{
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)
{
var key = ((System.Collections.DictionaryEntry)kvp).Key;
var value = ((System.Collections.DictionaryEntry)kvp).Value;
nodes.Add(new MiniYamlNode(FormatValue(key), FormatValue(value)));
dictNodes.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
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())
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));
return new MiniYaml(
null,
fields.Select(info => new MiniYamlNode(info.YamlName, FormatValue(o, info.Field))).ToList());
fields.Select(info => new MiniYamlNode(info.YamlName, FormatValue(o, info.Field))));
}
public static MiniYamlNode SaveField(object o, string field)
@@ -78,13 +80,37 @@ namespace OpenRA
if (t.IsArray && t.GetArrayRank() == 1)
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(", ");
// 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 = "";
var result = new StringBuilder();
var dict = (System.Collections.IDictionary)v;
foreach (var kvp in dict)
{
@@ -94,10 +120,10 @@ namespace OpenRA
var formattedKey = FormatValue(key);
var formattedValue = FormatValue(value);
result += $"{formattedKey}: {formattedValue}{Environment.NewLine}";
result.Append(CultureInfo.InvariantCulture, $"{formattedKey}: {formattedValue}{Environment.NewLine}");
}
return result;
return result.ToString();
}
if (v is DateTime d)

View File

@@ -11,7 +11,7 @@
using System;
namespace OpenRA.Mods.Cnc.FileFormats
namespace OpenRA.FileFormats
{
/// <summary>
/// Static class that uses a lookup table to calculates CRC32
@@ -22,8 +22,8 @@ namespace OpenRA.Mods.Cnc.FileFormats
/// <summary>
/// The CRC32 lookup table.
/// </summary>
static readonly uint[] LookUp = new uint[256]
{
static readonly uint[] LookUp =
[
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
@@ -88,7 +88,7 @@ namespace OpenRA.Mods.Cnc.FileFormats
0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
};
];
/// <summary>
/// A CRC32 implementation that can be used on spans of bytes.
@@ -118,5 +118,16 @@ namespace OpenRA.Mods.Cnc.FileFormats
{
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,28 +10,105 @@
#endregion
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;
using ICSharpCode.SharpZipLib.Checksum;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using OpenRA.Graphics;
using OpenRA.Primitives;
namespace OpenRA.FileFormats
{
/// <summary>
/// Used to connect several IDAT chunks into a continuous stream.
/// </summary>
sealed class PngIdatStream : Stream
{
readonly Stream baseStream;
int remainingInChunk;
bool eof;
public PngIdatStream(Stream baseStream, int initialLen)
{
this.baseStream = baseStream;
remainingInChunk = initialLen;
}
public override int Read(byte[] buffer, int offset, int count) => Read(buffer.AsSpan(offset, count));
public override int Read(Span<byte> buffer)
{
if (eof || buffer.Length == 0)
return 0;
var totalRead = 0;
Span<byte> header = stackalloc byte[8];
while (buffer.Length > 0)
{
if (remainingInChunk == 0)
{
// Skip CRC and read next chunk header.
var r = baseStream.Seek(4, SeekOrigin.Current) + baseStream.Read(header);
if (r < 12)
throw new EndOfStreamException("Invalid PNG file - no end chunk found.");
// The PNG spec states that IDAT chunks must be chained together.
if (BinaryPrimitives.ReadUInt32BigEndian(header[4..]) == Png.ChunkIDAT)
remainingInChunk = BinaryPrimitives.ReadInt32BigEndian(header[..4]);
else
{
// This is not an IDAT chunk. Discontinue reading.
baseStream.Seek(-8, SeekOrigin.Current);
eof = true;
return totalRead;
}
}
var toRead = Math.Min(buffer.Length, remainingInChunk);
var read = baseStream.Read(buffer[..toRead]);
if (read == 0)
throw new EndOfStreamException("Unexpected end of stream in IDAT chunk.");
remainingInChunk -= read;
totalRead += read;
buffer = buffer[read..];
}
return totalRead;
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}
public class Png
{
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 Height { get; }
public Color[] Palette { get; }
public byte[] Data { 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;
@@ -42,164 +119,273 @@ namespace OpenRA.FileFormats
s.Position += 8;
var headerParsed = false;
var data = new List<byte>();
var dataParsed = false;
byte bitDepth = 8;
Type = SpriteFrameType.Rgba32;
// Use a reusable 8-byte buffer for Length + Type.
Span<byte> chunkHeader = stackalloc byte[8];
while (true)
{
var length = IPAddress.NetworkToHostOrder(s.ReadInt32());
var type = Encoding.UTF8.GetString(s.ReadBytes(4));
var content = s.ReadBytes(length);
/*var crc = */s.ReadInt32();
if (s.Read(chunkHeader) < 8)
throw new EndOfStreamException("Invalid PNG file - no end chunk found.");
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.");
using (var ms = new MemoryStream(content))
switch (type)
{
switch (type)
case ChunkIHDR:
{
case "IHDR":
if (headerParsed)
throw new InvalidDataException("Invalid PNG file - duplicate header.");
#pragma warning disable CA2014 // Do not use stackalloc in loops
Span<byte> buffer = stackalloc byte[13];
#pragma warning restore CA2014 // Do not use stackalloc in loops
s.ReadBytes(buffer);
Width = BinaryPrimitives.ReadInt32BigEndian(buffer[..4]);
Height = BinaryPrimitives.ReadInt32BigEndian(buffer[4..8]);
bitDepth = buffer[8];
var colorType = (PngColorType)buffer[9];
if (IsPaletted(bitDepth, colorType))
Type = SpriteFrameType.Indexed8;
else if (colorType == PngColorType.Color)
Type = SpriteFrameType.Rgb24;
var compression = buffer[10];
// filter = buffer[11]
var interlace = buffer[12];
if (compression != 0)
throw new InvalidDataException("Compression method not supported");
if (interlace != 0)
throw new InvalidDataException("Interlacing not supported");
Data = new byte[Width * Height * PixelStride];
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
headerParsed = true;
break;
}
case ChunkPLTE:
{
if (length % 3 != 0)
throw new InvalidDataException("Invalid PLTE chunk length.");
var count = length / 3;
Palette = new Color[count];
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
{
if (headerParsed)
throw new InvalidDataException("Invalid PNG file - duplicate header.");
Width = IPAddress.NetworkToHostOrder(ms.ReadInt32());
Height = IPAddress.NetworkToHostOrder(ms.ReadInt32());
var bitDepth = ms.ReadUInt8();
var colorType = (PngColorType)ms.ReadUInt8();
if (IsPaletted(bitDepth, colorType))
Type = SpriteFrameType.Indexed8;
else if (colorType == PngColorType.Color)
Type = SpriteFrameType.Rgb24;
Data = new byte[Width * Height * PixelStride];
var compression = ms.ReadUInt8();
/*var filter = */ms.ReadUInt8();
var interlace = ms.ReadUInt8();
if (compression != 0)
throw new InvalidDataException("Compression method not supported");
if (interlace != 0)
throw new InvalidDataException("Interlacing not supported");
headerParsed = true;
break;
}
case "PLTE":
{
Palette = new Color[256];
for (var i = 0; i < length / 3; i++)
s.ReadBytes(buffer.AsSpan(0, length));
for (var i = 0; i < count; i++)
{
var r = ms.ReadUInt8(); var g = ms.ReadUInt8(); var b = ms.ReadUInt8();
Palette[i] = Color.FromArgb(r, g, b);
var offset = i * 3;
Palette[i] = Color.FromArgb(buffer[offset], buffer[offset + 1], buffer[offset + 2]);
}
break;
}
case "tRNS":
finally
{
if (Palette == null)
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
for (var i = 0; i < length; i++)
Palette[i] = Color.FromArgb(ms.ReadUInt8(), Palette[i]);
break;
ArrayPool<byte>.Shared.Return(buffer);
}
case "IDAT":
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
break;
}
case ChunkTRNS:
{
if (Palette == null)
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
{
data.AddRange(content);
break;
s.ReadBytes(buffer.AsSpan(0, length));
for (var i = 0; i < length && i < Palette.Length; i++)
Palette[i] = Color.FromArgb(buffer[i], Palette[i]);
}
case "tEXt":
finally
{
var key = ms.ReadASCIIZ();
EmbeddedData.Add(key, ms.ReadASCII(length - key.Length - 1));
break;
ArrayPool<byte>.Shared.Return(buffer);
}
case "IEND":
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
break;
}
case ChunkIDAT:
if (dataParsed)
throw new InvalidDataException("Invalid PNG file - discontinuous IDAT chunks.");
dataParsed = true;
ProcessIDAT(s, length, bitDepth);
break;
case ChunkTEXT:
{
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
{
using (var ns = new MemoryStream(data.ToArray()))
{
using (var ds = new InflaterInputStream(ns))
{
var pxStride = PixelStride;
var rowStride = Width * pxStride;
var span = buffer.AsSpan(0, length);
s.ReadBytes(span);
Span<byte> prevLine = new byte[rowStride];
for (var y = 0; y < Height; y++)
{
var filter = (PngFilter)ds.ReadUInt8();
ds.ReadBytes(Data, y * rowStride, rowStride);
var line = Data.AsSpan(y * rowStride, rowStride);
// Find Null Terminator for ASCIIZ (Keyword).
var nullIndex = span.IndexOf((byte)0);
if (nullIndex == -1)
return;
switch (filter)
{
case PngFilter.None:
break;
case PngFilter.Sub:
for (var i = pxStride; i < rowStride; i++)
line[i] += line[i - pxStride];
break;
case PngFilter.Up:
for (var i = 0; i < rowStride; i++)
line[i] += prevLine[i];
break;
case PngFilter.Average:
for (var i = 0; i < pxStride; i++)
line[i] += Average(0, prevLine[i]);
for (var i = pxStride; i < rowStride; i++)
line[i] += Average(line[i - pxStride], prevLine[i]);
break;
case PngFilter.Paeth:
for (var i = 0; i < pxStride; i++)
line[i] += Paeth(0, prevLine[i], 0);
for (var i = pxStride; i < rowStride; i++)
line[i] += Paeth(line[i - pxStride], prevLine[i], prevLine[i - pxStride]);
break;
default:
throw new InvalidOperationException("Unsupported Filter");
}
var key = Encoding.ASCII.GetString(span[..nullIndex]);
var value = Encoding.ASCII.GetString(span[(nullIndex + 1)..]);
prevLine = line;
}
static byte Average(byte a, byte b) => (byte)((a + b) / 2);
static byte Paeth(byte a, byte b, byte c)
{
var p = a + b - c;
var pa = Math.Abs(p - a);
var pb = Math.Abs(p - b);
var pc = Math.Abs(p - c);
return (pa <= pb && pa <= pc) ? a :
(pb <= pc) ? b : c;
}
}
}
if (Type == SpriteFrameType.Indexed8 && Palette == null)
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
return;
EmbeddedData[key] = value;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
break;
}
case ChunkIEND:
{
if (Type == SpriteFrameType.Indexed8 && Palette == null)
throw new InvalidDataException("Non-Palette indexed PNG are not supported.");
// Skip CRC.
s.Seek(4, SeekOrigin.Current);
return;
}
default:
{
// Skip unknown chunk + CRC.
s.Seek(length + 4, SeekOrigin.Current);
break;
}
}
}
}
void ProcessIDAT(Stream s, int firstChunkLength, byte bitDepth)
{
var pxStride = PixelStride;
var rowStride = Width * pxStride;
var pixelsPerByte = 8 / bitDepth;
var sourceRowStride = Exts.IntegerDivisionRoundingAwayFromZero(rowStride, pixelsPerByte);
// Custom stream that stitches IDAT chunks together without copying.
using var idatStream = new PngIdatStream(s, firstChunkLength);
using var ds = new ZLibStream(idatStream, CompressionMode.Decompress);
// Rent buffer from pool to avoid allocation churn.
var prevLineBuffer = ArrayPool<byte>.Shared.Rent(rowStride);
var prevLine = prevLineBuffer.AsSpan(0, rowStride);
prevLine.Clear();
try
{
for (var y = 0; y < Height; y++)
{
var filterByte = ds.ReadByte();
if (filterByte == -1)
break;
var filter = (PngFilter)filterByte;
var line = Data.AsSpan(y * rowStride, rowStride);
ds.ReadBytes(line[..sourceRowStride]);
// If the source has a bit depth of 1, 2 or 4 it packs multiple pixels per byte.
// Unpack to bit depth of 8, yielding 1 pixel per byte.
// This makes life easier for consumers of palleted data.
if (bitDepth < 8)
{
var mask = (1 << bitDepth) - 1;
for (var i = sourceRowStride - 1; i >= 0; i--)
{
var packed = line[i];
for (var j = 0; j < pixelsPerByte; j++)
{
var dest = i * pixelsPerByte + j;
if (dest < line.Length) // Guard against last byte being only partially packed
line[dest] = (byte)((packed >> (8 - (j + 1) * bitDepth)) & mask);
}
}
}
switch (filter)
{
case PngFilter.None:
break;
case PngFilter.Sub:
for (var i = pxStride; i < line.Length; i++)
line[i] += line[i - pxStride];
break;
case PngFilter.Up:
for (var i = 0; i < line.Length; i++)
line[i] += prevLine[i];
break;
case PngFilter.Average:
for (var i = 0; i < pxStride; i++)
line[i] += Average(0, prevLine[i]);
for (var i = pxStride; i < line.Length; i++)
line[i] += Average(line[i - pxStride], prevLine[i]);
break;
case PngFilter.Paeth:
for (var i = 0; i < pxStride; i++)
line[i] += Paeth(0, prevLine[i], 0);
for (var i = pxStride; i < line.Length; i++)
line[i] += Paeth(line[i - pxStride], prevLine[i], prevLine[i - pxStride]);
break;
default:
throw new InvalidOperationException("Unsupported Filter");
}
prevLine = line;
}
// Drain remaining Zlib footer bytes if necessary.
Span<byte> drain = stackalloc byte[16];
while (ds.Read(drain) > 0) { }
}
finally
{
ArrayPool<byte>.Shared.Return(prevLineBuffer);
}
static byte Average(byte a, byte b) => (byte)((a + b) / 2);
static byte Paeth(byte a, byte b, byte c)
{
var p = a + b - c;
var pa = Math.Abs(p - a);
var pb = Math.Abs(p - b);
var pc = Math.Abs(p - c);
return (pa <= pb && pa <= pc) ? a :
(pb <= pc) ? b : c;
}
}
public Png(byte[] data, SpriteFrameType type, int width, int height, Color[] palette = null,
Dictionary<string, string> embeddedData = null)
{
@@ -234,14 +420,27 @@ namespace OpenRA.FileFormats
// Convert to big endian
Data = new byte[data.Length];
var stride = PixelStride;
for (var i = 0; i < width * height; i++)
var elements = width * height * stride;
var src = data.AsSpan();
var dst = Data.AsSpan();
if (type == SpriteFrameType.Bgra32)
{
Data[stride * i] = data[stride * i + 2];
Data[stride * i + 1] = data[stride * i + 1];
Data[stride * i + 2] = data[stride * i + 0];
if (type == SpriteFrameType.Bgra32)
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;
@@ -258,7 +457,8 @@ namespace OpenRA.FileFormats
public static bool Verify(Stream s)
{
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;
return isPng;
}
@@ -269,7 +469,7 @@ namespace OpenRA.FileFormats
static bool IsPaletted(byte bitDepth, PngColorType colorType)
{
if (bitDepth == 8 && colorType == (PngColorType.Indexed | PngColorType.Color))
if (bitDepth <= 8 && colorType == (PngColorType.Indexed | PngColorType.Color))
return true;
if (bitDepth == 8 && colorType == (PngColorType.Color | PngColorType.Alpha))
@@ -281,28 +481,33 @@ namespace OpenRA.FileFormats
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);
output.Write(IPAddress.HostToNetworkOrder((int)input.Length));
output.WriteArray(typeBytes);
if (!input.TryGetBuffer(out var dataSegment))
dataSegment = new ArraySegment<byte>(input.ToArray());
var data = input.ReadAllBytes();
output.WriteArray(data);
ReadOnlySpan<byte> data = dataSegment.AsSpan(0, (int)input.Length);
var crc32 = new Crc32();
crc32.Update(typeBytes);
crc32.Update(data);
output.Write(IPAddress.NetworkToHostOrder((int)crc32.Value));
output.Write(header);
output.Write(data);
var crc = 0xFFFFFFFF;
crc = CRC32.Update(crc, header[4..]);
crc = CRC32.Update(crc, data);
var finalCrc = CRC32.Finish(crc);
output.Write(IPAddress.NetworkToHostOrder((int)finalCrc));
}
public byte[] Save()
public byte[] Save(CompressionLevel compression = CompressionLevel.SmallestSize)
{
using (var output = new MemoryStream())
{
output.WriteArray(Signature);
output.Write(Signature);
using (var header = new MemoryStream())
{
header.Write(IPAddress.HostToNetworkOrder(Width));
@@ -317,7 +522,7 @@ namespace OpenRA.FileFormats
header.WriteByte(0); // Filter
header.WriteByte(0); // Interlacing
WritePngChunk(output, "IHDR", header);
WritePngChunk(output, ChunkIHDR, header);
}
var alphaPalette = false;
@@ -333,7 +538,7 @@ namespace OpenRA.FileFormats
alphaPalette |= c.A > 0;
}
WritePngChunk(output, "PLTE", palette);
WritePngChunk(output, ChunkPLTE, palette);
}
}
@@ -344,46 +549,44 @@ namespace OpenRA.FileFormats
foreach (var c in Palette)
alpha.WriteByte(c.A);
WritePngChunk(output, "tRNS", alpha);
WritePngChunk(output, ChunkTRNS, alpha);
}
}
using (var data = new MemoryStream())
{
using (var compressed = new DeflaterOutputStream(data))
using (var compressed = new ZLibStream(data, compression, true))
{
var rowStride = Width * PixelStride;
for (var y = 0; y < Height; y++)
{
// Write uncompressed scanlines for simplicity
compressed.WriteByte(0);
// Assuming no filtering for simplicity
const byte FilterType = 0;
compressed.WriteByte(FilterType);
compressed.Write(Data, y * rowStride, rowStride);
}
compressed.Flush();
compressed.Finish();
WritePngChunk(output, "IDAT", data);
}
WritePngChunk(output, ChunkIDAT, data);
}
foreach (var kv in EmbeddedData)
{
using (var text = new MemoryStream())
{
text.WriteArray(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
WritePngChunk(output, "tEXt", text);
text.Write(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
WritePngChunk(output, ChunkTEXT, text);
}
}
WritePngChunk(output, "IEND", new MemoryStream());
WritePngChunk(output, ChunkIEND, new MemoryStream());
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)
{
if (info == null)
throw new ArgumentNullException(nameof(info));
ArgumentNullException.ThrowIfNull(info);
GameInfo = info;
}
@@ -47,8 +46,8 @@ namespace OpenRA.FileFormats
throw new NotSupportedException($"Metadata version {version} is not supported");
// Read game info (max 100K limit as a safeguard against corrupted files)
var data = fs.ReadString(Encoding.UTF8, 1024 * 100);
GameInfo = GameInformation.Deserialize(data);
var data = fs.ReadLengthPrefixedString(Encoding.UTF8, 1024 * 100);
GameInfo = GameInformation.Deserialize(data, path);
}
public void Write(BinaryWriter writer)
@@ -62,7 +61,7 @@ namespace OpenRA.FileFormats
{
// Write lobby info data
writer.Flush();
dataLength += writer.BaseStream.WriteString(Encoding.UTF8, GameInfo.Serialize());
dataLength += writer.BaseStream.WriteLengthPrefixedString(Encoding.UTF8, GameInfo.Serialize());
}
// Write total length & end marker

View File

@@ -23,22 +23,22 @@ namespace OpenRA.FileSystem
bool TryGetPackageContaining(string path, out IReadOnlyPackage package, out string filename);
bool TryOpen(string filename, out Stream s);
bool Exists(string filename);
bool IsExternalModFile(string filename);
bool IsExternalFile(string filename);
}
public class FileSystem : IReadOnlyFileSystem
{
public IEnumerable<IReadOnlyPackage> MountedPackages => mountedPackages.Keys;
readonly Dictionary<IReadOnlyPackage, int> mountedPackages = new();
readonly Dictionary<string, IReadOnlyPackage> explicitMounts = new();
readonly Dictionary<IReadOnlyPackage, int> mountedPackages = [];
readonly Dictionary<string, IReadOnlyPackage> explicitMounts = [];
readonly string modID;
// Mod packages that should not be disposed
readonly List<IReadOnlyPackage> modPackages = new();
readonly List<IReadOnlyPackage> modPackages = [];
readonly IReadOnlyDictionary<string, Manifest> installedMods;
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)
{
@@ -83,14 +83,14 @@ namespace OpenRA.FileSystem
public void Mount(string name, string explicitName = null)
{
var optional = name.StartsWith("~", StringComparison.Ordinal);
var optional = name.StartsWith('~');
if (optional)
name = name[1..];
try
{
IReadOnlyPackage package;
if (name.StartsWith("$", StringComparison.Ordinal))
if (name.StartsWith('$'))
{
name = name[1..];
@@ -109,10 +109,8 @@ namespace OpenRA.FileSystem
Mount(package, explicitName);
}
catch
catch when (optional)
{
if (!optional)
throw;
}
}
@@ -161,9 +159,7 @@ namespace OpenRA.FileSystem
explicitMounts.Remove(key);
// Mod packages aren't owned by us, so we shouldn't dispose them
if (modPackages.Contains(package))
modPackages.Remove(package);
else
if (!modPackages.Remove(package))
package.Dispose();
}
else
@@ -182,14 +178,16 @@ namespace OpenRA.FileSystem
explicitMounts.Clear();
modPackages.Clear();
fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => new List<IReadOnlyPackage>());
fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => []);
}
public void LoadFromManifest(Manifest manifest)
public void TrimExcess()
{
UnmountAll();
foreach (var kv in manifest.Packages)
Mount(kv.Key, kv.Value);
mountedPackages.TrimExcess();
explicitMounts.TrimExcess();
modPackages.TrimExcess();
foreach (var packages in fileIndex.Values)
packages.TrimExcess();
}
Stream GetFromCache(string filename)
@@ -226,14 +224,11 @@ namespace OpenRA.FileSystem
public bool TryOpen(string filename, out Stream s)
{
var explicitSplit = filename.IndexOf('|');
if (explicitSplit > 0)
if (explicitSplit > 0 && explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
{
if (explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
{
s = explicitPackage.GetStream(filename[(explicitSplit + 1)..]);
if (s != null)
return true;
}
s = explicitPackage.GetStream(filename[(explicitSplit + 1)..]);
if (s != null)
return true;
}
s = GetFromCache(filename);
@@ -262,64 +257,20 @@ namespace OpenRA.FileSystem
public bool Exists(string filename)
{
var explicitSplit = filename.IndexOf('|');
if (explicitSplit > 0)
if (explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
if (explicitPackage.Contains(filename[(explicitSplit + 1)..]))
return true;
if (explicitSplit > 0 &&
explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage) &&
explicitPackage.Contains(filename[(explicitSplit + 1)..]))
return true;
return fileIndex.ContainsKey(filename);
}
/// <summary>
/// Returns true if the given filename references an external mod via an explicit mount.
/// Returns true if the given filename references any file outside the mod mount.
/// </summary>
public bool IsExternalModFile(string filename)
public bool IsExternalFile(string filename)
{
var explicitSplit = filename.IndexOf('|');
if (explicitSplit < 0)
return false;
if (!explicitMounts.TryGetValue(filename[..explicitSplit], out var explicitPackage))
return false;
if (installedMods[modID].Package == explicitPackage)
return false;
return modPackages.Contains(explicitPackage);
}
/// <summary>
/// Resolves a filesystem for an assembly, accounting for explicit and mod mounts.
/// Assemblies must exist in the native OS file system (not inside an OpenRA-defined package).
/// </summary>
public static string ResolveAssemblyPath(string path, Manifest manifest, InstalledMods installedMods)
{
var explicitSplit = path.IndexOf('|');
if (explicitSplit > 0 && !path.StartsWith("^"))
{
var parent = path[..explicitSplit];
var filename = path[(explicitSplit + 1)..];
var parentPath = manifest.Packages.FirstOrDefault(kv => kv.Value == parent).Key;
if (parentPath == null)
return null;
if (parentPath.StartsWith("$", StringComparison.Ordinal))
{
if (!installedMods.TryGetValue(parentPath[1..], out var mod))
return null;
if (mod.Package is not Folder)
return null;
path = Path.Combine(mod.Package.Name, filename);
}
else
path = Path.Combine(parentPath, filename);
}
var resolvedPath = Platform.ResolvePath(path);
return File.Exists(resolvedPath) ? resolvedPath : null;
return !filename.StartsWith($"{modID}|", StringComparison.Ordinal);
}
public static string ResolveCaseInsensitivePath(string path)
@@ -335,7 +286,8 @@ namespace OpenRA.FileSystem
if (name == ".")
continue;
resolved = Directory.GetFileSystemEntries(resolved).FirstOrDefault(e => e.Equals(Path.Combine(resolved, name), StringComparison.InvariantCultureIgnoreCase));
resolved = Directory.GetFileSystemEntries(resolved)
.FirstOrDefault(e => e.Equals(Path.Combine(resolved, name), StringComparison.InvariantCultureIgnoreCase));
if (resolved == null)
return null;

View File

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

View File

@@ -21,7 +21,7 @@ namespace OpenRA.FileSystem
{
const uint ZipSignature = 0x04034b50;
class ReadOnlyZipFile : IReadOnlyPackage
public class ReadOnlyZipFile : IReadOnlyPackage
{
public string Name { get; protected set; }
protected ZipFile pkg;
@@ -68,6 +68,7 @@ namespace OpenRA.FileSystem
public void Dispose()
{
pkg?.Close();
GC.SuppressFinalize(this);
}
public IReadOnlyPackage OpenPackage(string filename, FileSystem context)
@@ -93,15 +94,15 @@ namespace OpenRA.FileSystem
}
}
sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage
public sealed class ReadWriteZipFile : ReadOnlyZipFile, IReadWritePackage
{
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
// 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)))
{
@@ -113,14 +114,33 @@ namespace OpenRA.FileSystem
pkgStream.Position = 0;
pkg = new ZipFile(pkgStream);
Name = filename;
// Remove subfields that can break ZIP updating.
foreach (ZipEntry entry in pkg)
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()
{
var pos = pkgStream.Position;
pkgStream.Position = 0;
File.WriteAllBytes(Name, pkgStream.ReadBytes((int)pkgStream.Length));
pkgStream.Position = pos;
if (!string.IsNullOrEmpty(Name))
File.WriteAllBytes(Name, pkgStream.ToArray());
}
public void Update(string filename, byte[] contents)
@@ -147,7 +167,7 @@ namespace OpenRA.FileSystem
public ZipFolder(ReadOnlyZipFile parent, string path)
{
if (path.EndsWith("/", StringComparison.Ordinal))
if (path.EndsWith('/'))
path = path[..^1];
Name = path;

202
OpenRA.Game/FluentBundle.cs Normal file
View File

@@ -0,0 +1,202 @@
#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.Collections.Immutable;
using System.Globalization;
using System.IO;
using Linguini.Bundle;
using Linguini.Bundle.Builder;
using Linguini.Shared.Types.Bundle;
using Linguini.Syntax.Parser;
using Linguini.Syntax.Parser.Error;
using OpenRA.FileSystem;
using OpenRA.Traits;
namespace OpenRA
{
[AttributeUsage(AttributeTargets.Field)]
public sealed class FluentReferenceAttribute : Attribute
{
public readonly bool Optional;
public readonly string[] RequiredVariableNames;
public readonly LintDictionaryReference DictionaryReference;
public FluentReferenceAttribute() { }
public FluentReferenceAttribute(params string[] requiredVariableNames)
{
RequiredVariableNames = requiredVariableNames;
}
public FluentReferenceAttribute(LintDictionaryReference dictionaryReference = LintDictionaryReference.None)
{
DictionaryReference = dictionaryReference;
}
public FluentReferenceAttribute(bool optional)
{
Optional = optional;
}
}
[AttributeUsage(AttributeTargets.Class)]
public sealed class IncludeStaticFluentReferencesAttribute : Attribute
{
public readonly Type[] Types;
public IncludeStaticFluentReferencesAttribute(params Type[] types)
{
Types = types;
}
}
[AttributeUsage(AttributeTargets.Class)]
public sealed class IncludeChromeLogicArgsFluentReferencesAttribute : Attribute
{
public readonly string[] MethodNames;
public IncludeChromeLogicArgsFluentReferencesAttribute(params string[] methodNames)
{
MethodNames = methodNames;
}
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class IncludeFluentReferencesAttribute : Attribute
{
public readonly LintDictionaryReference DictionaryReference;
public IncludeFluentReferencesAttribute() { }
public IncludeFluentReferencesAttribute(LintDictionaryReference dictionaryReference = LintDictionaryReference.None)
{
DictionaryReference = dictionaryReference;
}
}
public class FluentBundle
{
readonly Linguini.Bundle.FluentBundle bundle;
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem)
: this(culture, paths, fileSystem, error => Log.Write("debug", error.Message)) { }
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem, string text)
: this(culture, paths, fileSystem, text, error => Log.Write("debug", error.Message)) { }
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem, Action<ParseError> onError)
: this(culture, paths, fileSystem, null, onError) { }
public FluentBundle(string culture, string text, Action<ParseError> onError)
: this(culture, default, null, text, onError) { }
public FluentBundle(string culture, ImmutableArray<string> paths, IReadOnlyFileSystem fileSystem, string text, Action<ParseError> onError)
{
bundle = LinguiniBuilder.Builder()
.CultureInfo(new CultureInfo(culture))
.SkipResources()
.SetUseIsolating(false)
.UseConcurrent()
.UncheckedBuild();
if (paths != null)
{
foreach (var path in paths)
{
var stream = fileSystem.Open(path);
using (var reader = new StreamReader(stream))
{
var parser = new LinguiniParser(reader);
var resource = parser.Parse();
foreach (var error in resource.Errors)
onError(error);
bundle.AddResourceOverriding(resource);
}
}
}
if (!string.IsNullOrEmpty(text))
{
var parser = new LinguiniParser(text);
var resource = parser.Parse();
foreach (var error in resource.Errors)
onError(error);
bundle.AddResourceOverriding(resource);
}
}
public string GetMessage(string key, object[] args = null)
{
if (!TryGetMessage(key, out var message, args))
message = key;
return message;
}
public bool TryGetMessage(string key, out string value, object[] args = null)
{
ArgumentNullException.ThrowIfNull(key);
try
{
if (!HasMessage(key))
{
value = null;
return false;
}
Dictionary<string, IFluentType> fluentArgs = null;
if (args != null)
{
if (args.Length % 2 != 0)
throw new ArgumentException("Expected a comma separated list of name, value arguments " +
"but the number of arguments is not a multiple of two", nameof(args));
fluentArgs = [];
for (var i = 0; i < args.Length; i += 2)
{
var argKey = args[i] as string;
if (string.IsNullOrEmpty(argKey))
throw new ArgumentException($"Expected the argument at index {i} to be a non-empty string", nameof(args));
var argValue = args[i + 1];
if (argValue == null)
throw new ArgumentNullException(nameof(args), $"Expected the argument at index {i + 1} to be a non-null value");
fluentArgs.Add(argKey, argValue.ToFluentType());
}
}
var result = bundle.TryGetAttrMessage(key, fluentArgs, out var errors, out value);
foreach (var error in errors)
Log.Write("debug", $"FluentBundle of {key}: {error}");
return result;
}
catch (Exception)
{
Log.Write("debug", $"FluentBundle of {key}: threw exception");
value = null;
return false;
}
}
public bool HasMessage(string key)
{
return bundle.HasAttrMessage(key);
}
}
}

View File

@@ -13,7 +13,7 @@ using Linguini.Shared.Types.Bundle;
namespace OpenRA
{
public static class TranslationExts
public static class FluentExts
{
public static IFluentType ToFluentType(this object value)
{

View File

@@ -0,0 +1,94 @@
#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.Immutable;
using System.Text;
using OpenRA.FileSystem;
namespace OpenRA
{
public static class FluentProvider
{
// Ensure thread-safety.
static readonly object SyncObject = new();
static FluentBundle modFluentBundle;
static FluentBundle mapFluentBundle;
public static void Initialize(Manifest manifest, IReadOnlyFileSystem fileSystem)
{
lock (SyncObject)
{
modFluentBundle = new FluentBundle(manifest.FluentCulture, manifest.FluentMessages, fileSystem);
if (fileSystem is Map map && map.FluentMessageDefinitions != null)
{
var files = ImmutableArray<string>.Empty;
if (map.FluentMessageDefinitions.Value != null)
files = FieldLoader.GetValue<ImmutableArray<string>>("value", map.FluentMessageDefinitions.Value);
string text = null;
if (map.FluentMessageDefinitions.Nodes.Length > 0)
{
var builder = new StringBuilder();
foreach (var node in map.FluentMessageDefinitions.Nodes)
if (node.Key == "base64")
builder.Append(Encoding.UTF8.GetString(Convert.FromBase64String(node.Value.Value)));
text = builder.ToString();
}
mapFluentBundle = new FluentBundle(manifest.FluentCulture, files, fileSystem, text);
}
}
}
public static string GetMessage(string key, params object[] args)
{
lock (SyncObject)
{
// By prioritizing mod-level fluent bundles we prevent maps from overwriting string keys. We do not want to
// allow maps to change the UI nor any other strings not exposed to the map.
if (modFluentBundle.TryGetMessage(key, out var message, args))
return message;
if (mapFluentBundle != null)
return mapFluentBundle.GetMessage(key, args);
return key;
}
}
public static bool TryGetMessage(string key, out string message, params object[] args)
{
lock (SyncObject)
{
// By prioritizing mod-level bundle we prevent maps from overwriting string keys. We do not want to
// allow maps to change the UI nor any other strings not exposed to the map.
if (modFluentBundle.TryGetMessage(key, out message, args))
return true;
if (mapFluentBundle != null && mapFluentBundle.TryGetMessage(key, out message, args))
return true;
return false;
}
}
/// <summary>Should only be used by <see cref="MapPreview"/>.</summary>
internal static bool TryGetModMessage(string key, out string message, params object[] args)
{
lock (SyncObject)
{
return modFluentBundle.TryGetMessage(key, out message, args);
}
}
}
}

View File

@@ -9,6 +9,7 @@
*/
#endregion
using System.Collections.Frozen;
using System.Collections.Generic;
namespace OpenRA
@@ -23,15 +24,15 @@ namespace OpenRA
public class Fonts : IGlobalModData
{
[FieldLoader.LoadUsing(nameof(LoadFonts))]
public readonly Dictionary<string, FontData> FontList;
public readonly FrozenDictionary<string, FontData> FontList;
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)
ret.Add(node.Key, FieldLoader.Load<FontData>(node.Value));
return ret;
return ret.ToFrozenDictionary();
}
}
}

View File

@@ -11,6 +11,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@@ -27,9 +28,10 @@ using OpenRA.Widgets;
namespace OpenRA
{
[IncludeStaticFluentReferences(typeof(Server.Server), typeof(Player), typeof(UnitOrders), typeof(OrderManager))]
public static class Game
{
[TranslationReference("filename")]
[FluentReference("filename")]
const string SavedScreenshot = "notification-saved-screenshot";
public const int TimestepJankThreshold = 250; // Don't catch up for delays larger than 250ms
@@ -86,8 +88,9 @@ namespace OpenRA
static void JoinInner(OrderManager om)
{
// Refresh TextNotificationsManager before the game starts.
// Refresh static classes before the game starts.
TextNotificationsManager.Clear();
UnitOrders.Clear();
// HACK: The shellmap World and OrderManager are owned by the main menu's WorldRenderer instead of Game.
// This allows us to switch Game.OrderManager from the shellmap to the new network connection when joining
@@ -165,6 +168,7 @@ namespace OpenRA
{
return ModData.WidgetLoader.LoadWidget(new WidgetArgs(args)
{
{ "modData", ModData },
{ "world", world },
{ "orderManager", OrderManager },
{ "worldRenderer", worldRenderer },
@@ -179,7 +183,17 @@ namespace OpenRA
}
public static event Action BeforeGameStart = () => { };
internal static void StartGame(string mapUID, WorldType type)
public static event Action AfterGameStart = () => { };
internal static void StartGame(string uid, WorldType type)
{
var preview = ModData.MapCache[uid];
if (preview.Status != MapStatus.Available)
throw new InvalidDataException($"Invalid map uid: {uid}");
StartGame(preview.ToMap(), type);
}
internal static void StartGame(Map map, WorldType type)
{
// Dispose of the old world before creating a new one.
worldRenderer?.Dispose();
@@ -188,7 +202,23 @@ namespace OpenRA
BeforeGameStart();
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;
@@ -222,6 +252,12 @@ namespace OpenRA
// Much better to clean up now then to drop frames during gameplay for GC pauses.
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
// PostLoadComplete is designed for anything that should trigger at the very end of loading.
// e.g. audio notifications that the game is starting.
OrderManager.World.PostLoadComplete(worldRenderer);
AfterGameStart();
}
public static void RestartGame()
@@ -336,7 +372,7 @@ namespace OpenRA
var explicitModPaths = Array.Empty<string>();
if (modID != null && (File.Exists(modID) || Directory.Exists(modID)))
{
explicitModPaths = new[] { modID };
explicitModPaths = [modID];
modID = Path.GetFileNameWithoutExtension(modID);
}
@@ -351,6 +387,48 @@ namespace OpenRA
Log.AddChannel("nat", "nat.log");
Log.AddChannel("client", "client.log");
Nat.Initialize();
var modSearchArg = args.GetValue("Engine.ModSearchPaths", null);
var modSearchPaths = modSearchArg != null ?
FieldLoader.GetValue<ImmutableArray<string>>("Engine.ModsPath", modSearchArg) :
[Path.Combine(Platform.EngineDir, "mods")];
Mods = new InstalledMods(modSearchPaths, explicitModPaths);
Console.WriteLine("Internal mods:");
foreach (var mod in Mods)
Console.WriteLine($"\t{mod.Key} ({mod.Value.Metadata.Version})");
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
ExternalMods = new ExternalMods();
if (modID == null)
throw new InvalidOperationException("Game.Mod argument missing.");
if (Mods.TryGetValue(modID, out var manifest))
{
var launchPath = args.GetValue("Engine.LaunchPath", null);
var launchArgs = new List<string>();
// Sanitize input from platform-specific launchers
// Process.Start requires paths to not be quoted, even if they contain spaces
if (launchPath != null && launchPath[0] == '"' && launchPath[^1] == '"')
launchPath = launchPath[1..^1];
// Metadata registration requires an explicit launch path
if (launchPath != null)
ExternalMods.Register(Mods[modID], launchPath, launchArgs, ModRegistration.User);
ExternalMods.ClearInvalidRegistrations(ModRegistration.User);
}
else
throw new InvalidOperationException($"Unknown or invalid mod '{modID}'.");
Console.WriteLine("External mods:");
foreach (var mod in ExternalMods)
Console.WriteLine($"\t{mod.Key} ({mod.Value.Version})");
var platforms = new[] { Settings.Game.Platform, "Default", null };
foreach (var p in platforms)
{
@@ -360,23 +438,8 @@ namespace OpenRA
Settings.Game.Platform = p;
try
{
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + p + ".dll");
#if NET5_0_OR_GREATER
var loader = new AssemblyLoader(rendererPath);
var platformType = loader.LoadDefaultAssembly().GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
#else
// NOTE: This is currently the only use of System.Reflection in this file, so would give an unused using error if we import it above
var assembly = System.Reflection.Assembly.LoadFile(rendererPath);
var platformType = assembly.GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
#endif
if (platformType == null)
throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation.");
var platform = (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null);
Renderer = new Renderer(platform, Settings.Graphics);
var platform = CreatePlatform(p);
Renderer = new Renderer(platform, Settings.Graphics, manifest.RendererConstants.VertexBatchSize);
Sound = new Sound(platform, Settings.Sound);
break;
@@ -392,47 +455,23 @@ namespace OpenRA
}
}
Nat.Initialize();
var modSearchArg = args.GetValue("Engine.ModSearchPaths", null);
var modSearchPaths = modSearchArg != null ?
FieldLoader.GetValue<string[]>("Engine.ModsPath", modSearchArg) :
new[] { Path.Combine(Platform.EngineDir, "mods") };
Mods = new InstalledMods(modSearchPaths, explicitModPaths);
Console.WriteLine("Internal mods:");
foreach (var mod in Mods)
Console.WriteLine($"\t{mod.Key}: {mod.Value.Metadata.Title} ({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.First() == '"' && 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.Title} ({mod.Value.Version})");
InitializeMod(modID, args);
InitializeMod(manifest, args);
}
public static void InitializeMod(string mod, Arguments args)
public static IPlatform CreatePlatform(string platformName)
{
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + platformName + ".dll");
var loader = new AssemblyLoader(rendererPath);
var platformType = loader.LoadDefaultAssembly().GetTypes().SingleOrDefault(t => typeof(IPlatform).IsAssignableFrom(t));
if (platformType == null)
throw new InvalidOperationException("Platform dll must include exactly one IPlatform implementation.");
return (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null);
}
public static void InitializeMod(Manifest manifest, Arguments args)
{
// Clear static state if we have switched mods
LobbyInfoChanged = () => { };
@@ -456,38 +495,29 @@ namespace OpenRA
ModData = null;
if (mod == null)
throw new InvalidOperationException("Game.Mod argument missing.");
if (!Mods.ContainsKey(mod))
throw new InvalidOperationException($"Unknown or invalid mod '{mod}'.");
Console.WriteLine($"Loading mod: {mod}");
Console.WriteLine($"Loading mod: {manifest.Id}");
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;
ModData.InitializeLoaders(ModData.DefaultFileSystem);
Renderer.InitializeFonts(ModData);
using (new PerfTimer("LoadMaps"))
ModData.MapCache.LoadMaps();
var grid = ModData.Manifest.Contains<MapGrid>() ? ModData.Manifest.Get<MapGrid>() : null;
Renderer.InitializeDepthBuffer(grid);
ModData.MapCache.LoadMaps(ModData);
Cursor?.Dispose();
Cursor = new CursorManager(ModData.CursorProvider);
Cursor = new CursorManager(ModData);
var metadata = ModData.Manifest.Metadata;
if (!string.IsNullOrEmpty(metadata.WindowTitle))
Renderer.Window.SetWindowTitle(metadata.WindowTitle);
if (!string.IsNullOrEmpty(metadata.WindowTitleTranslated))
Renderer.Window.SetWindowTitle(metadata.WindowTitleTranslated);
PerfHistory.Items["render"].HasNormalTick = false;
PerfHistory.Items["batches"].HasNormalTick = false;
@@ -501,10 +531,16 @@ namespace OpenRA
ModData.LoadScreen.StartGame(args);
}
public static void LoadEditor(string mapUid)
public static void LoadEditor(string uid)
{
JoinLocal();
StartGame(mapUid, WorldType.Editor);
StartGame(uid, WorldType.Editor);
}
public static void LoadEditor(Map map)
{
JoinLocal();
StartGame(map, WorldType.Editor);
}
public static void LoadShellMap()
@@ -523,10 +559,11 @@ namespace OpenRA
.Where(m => m.Status == MapStatus.Available && m.Visibility.HasFlag(MapVisibility.Shellmap))
.Select(m => m.Uid);
if (!shellmaps.Any())
var shellmap = shellmaps.RandomOrDefault(CosmeticRandom);
if (shellmap == null)
throw new InvalidDataException("No valid shellmaps available");
return shellmaps.Random(CosmeticRandom);
return shellmap;
}
public static void SwitchToExternalMod(ExternalMod mod, string[] launchArguments = null, Action onFailed = null)
@@ -577,11 +614,11 @@ namespace OpenRA
Directory.CreateDirectory(directory);
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);
Renderer.SaveScreenshot(path);
TextNotificationsManager.Debug(TranslationProvider.GetString(SavedScreenshot, Translation.Arguments("filename", filename)));
TextNotificationsManager.Debug(FluentProvider.GetMessage(SavedScreenshot, "filename", filename));
}
}
@@ -600,7 +637,10 @@ namespace OpenRA
if (orderManager.LastTickTime.ShouldAdvance(tick))
{
using (new PerfSample("tick_time"))
if (orderManager.GameStarted && orderManager.LocalFrameNumber == 0)
PerfHistory.Reset(); // Remove history that occurred whilst the new game was loading.
using (var sample = new PerfSample("tick_time"))
{
orderManager.LastTickTime.AdvanceTickTime(tick);
@@ -609,7 +649,11 @@ namespace OpenRA
Sync.RunUnsynced(world, orderManager.TickImmediate);
if (world == null)
{
if (orderManager.GameStarted)
PerfHistory.Reset(); // Remove old history when a new game starts.
return;
}
if (orderManager.TryTick())
{
@@ -617,7 +661,7 @@ namespace OpenRA
world.Tick();
PerfHistory.Tick();
PerfHistory.Tick(!world.Paused);
}
// Wait until we have done our first world Tick before TickRendering
@@ -663,7 +707,7 @@ namespace OpenRA
// Prepare renderables (i.e. render voxels) before calling BeginFrame
using (new PerfSample("render_prepare"))
{
Renderer.WorldModelRenderer.BeginFrame();
worldRenderer?.BeginFrame();
// World rendering is disabled while the loading screen is displayed
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
@@ -673,7 +717,7 @@ namespace OpenRA
}
Ui.PrepareRenderables();
Renderer.WorldModelRenderer.EndFrame();
worldRenderer?.EndFrame();
}
// worldRenderer is null during the initial install/download screen
@@ -681,7 +725,7 @@ namespace OpenRA
// Use worldRenderer.World instead of OrderManager.World to avoid a rendering mismatch while processing orders
if (worldRenderer != null && !worldRenderer.World.IsLoadingGameSave)
{
Renderer.BeginWorld(worldRenderer.Viewport.Rectangle);
Renderer.BeginWorld(worldRenderer.Viewport.CenterLocation, worldRenderer.Viewport.ViewportSize);
Sound.SetListenerPosition(worldRenderer.Viewport.CenterPosition);
using (new PerfSample("render_world"))
worldRenderer.Draw();
@@ -696,15 +740,12 @@ namespace OpenRA
Ui.Draw();
if (ModData != null && ModData.CursorProvider != null)
if (HideCursor)
Cursor?.SetCursor(null);
else
{
if (HideCursor)
Cursor.SetCursor(null);
else
{
Cursor.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default");
Cursor.Render(Renderer);
}
Cursor?.SetCursor(Ui.Root.GetCursorOuter(Viewport.LastMousePos) ?? "default");
Cursor?.Render(Renderer);
}
}
@@ -718,12 +759,13 @@ namespace OpenRA
}
}
PerfHistory.Items["render"].Tick();
PerfHistory.Items["batches"].Tick();
PerfHistory.Items["render_world"].Tick();
PerfHistory.Items["render_widgets"].Tick();
PerfHistory.Items["render_flip"].Tick();
PerfHistory.Items["terrain_lighting"].Tick();
var isActive = !(worldRenderer?.World.Paused ?? true);
PerfHistory.Items["render"].Tick(isActive);
PerfHistory.Items["batches"].Tick(isActive);
PerfHistory.Items["render_world"].Tick(isActive);
PerfHistory.Items["render_widgets"].Tick(isActive);
PerfHistory.Items["render_flip"].Tick(isActive);
PerfHistory.Items["terrain_lighting"].Tick(isActive);
}
static void Loop()
@@ -777,7 +819,7 @@ namespace OpenRA
var logicWorld = worldRenderer?.World;
// ReplayTimestep = 0 means the replay is paused: we need to keep logicInterval as UI.Timestep to avoid breakage
if (logicWorld != null && !(logicWorld.IsReplay && logicWorld.ReplayTimestep == 0))
if (logicWorld != null && (!logicWorld.IsReplay || logicWorld.ReplayTimestep != 0))
logicInterval = logicWorld == OrderManager.World ? OrderManager.SuggestedTimestep : logicWorld.Timestep;
// Ideal time between screen updates
@@ -820,33 +862,45 @@ namespace OpenRA
var haveSomeTimeUntilNextLogic = now < nextLogic;
var isTimeToRender = now >= nextRender;
if (!Renderer.WindowIsSuspended && ((isTimeToRender && haveSomeTimeUntilNextLogic) || forceRender))
if (!Renderer.WindowIsSuspended)
{
nextRender = now + renderInterval;
if (isTimeToRender || forceRender)
{
if (haveSomeTimeUntilNextLogic || forceRender)
RenderTick();
// Pick the minimum allowed FPS (the lower between 'minReplayFPS'
// and the user's max frame rate) and convert it to maximum time
// allowed between screen updates.
// We do this before rendering to include the time rendering takes
// in this interval.
var maxRenderInterval = Math.Max(1000 / MinReplayFps, renderInterval);
forcedNextRender = now + maxRenderInterval;
nextRender = now + renderInterval;
RenderTick();
renderBeforeNextTick = false;
// Pick the minimum allowed FPS (the lower between 'minReplayFPS'
// and the user's max frame rate) and convert it to maximum time
// allowed between screen updates.
// We do this before rendering to include the time rendering takes
// in this interval.
var maxRenderInterval = Math.Max(1000 / MinReplayFps, renderInterval);
forcedNextRender = now + maxRenderInterval;
renderBeforeNextTick = false;
}
}
// Simulate a render tick if it was time to render but we skip actually rendering
if (Renderer.WindowIsSuspended && isTimeToRender)
else
{
// Make sure that nextUpdate is set to a proper minimum interval
nextRender = now + renderInterval;
// Simulate a render tick if it was time to render but we skip actually rendering
if (isTimeToRender || forceRender)
{
// Make sure that nextUpdate is set to a proper minimum interval
nextRender = now + renderInterval;
// Still process SDL events to allow a restore to come through
Renderer.Window.PumpInput(new NullInputHandler());
// Still process SDL events to allow a restore to come through
Renderer.Window.PumpInput(new NullInputHandler());
// Ensure that we still logic tick despite not rendering
renderBeforeNextTick = false;
// Ensure that we still logic tick despite not rendering
renderBeforeNextTick = false;
}
else
{
// Avoid busy wait.
Thread.Sleep((int)(nextRender - now));
}
}
}
else
@@ -912,21 +966,22 @@ namespace OpenRA
{
var endpoints = new List<IPEndPoint>
{
new IPEndPoint(IPAddress.IPv6Any, settings.ListenPort),
new IPEndPoint(IPAddress.Any, settings.ListenPort)
new(IPAddress.IPv6Any, settings.ListenPort),
new(IPAddress.Any, settings.ListenPort)
};
server = new Server.Server(endpoints, settings, ModData, ServerType.Multiplayer);
return server.GetEndpointForLocalConnection();
}
public static ConnectionTarget CreateLocalServer(string map)
public static ConnectionTarget CreateLocalServer(string map, bool isSkirmish = false)
{
var settings = new ServerSettings()
{
Name = "Skirmish Game",
Map = map,
AdvertiseOnline = false
AdvertiseOnline = false,
AdvertiseOnLocalNetwork = !isSkirmish
};
// Always connect to local games using the same loopback connection
@@ -934,9 +989,9 @@ namespace OpenRA
// This would break the Restart button, which relies on the PlayerIndex always being the same for local servers
var endpoints = new List<IPEndPoint>
{
new IPEndPoint(IPAddress.Loopback, 0)
new(IPAddress.Loopback, 0)
};
server = new Server.Server(endpoints, settings, ModData, ServerType.Local);
server = new Server.Server(endpoints, settings, ModData, isSkirmish ? ServerType.Skirmish : ServerType.Local);
return server.GetEndpointForLocalConnection();
}
@@ -964,7 +1019,7 @@ namespace OpenRA
Order.Command($"state {Session.ClientState.Ready}")
};
var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.Package.Name) == launchMap);
var map = ModData.MapCache.SingleOrDefault(m => m.Uid == launchMap || Path.GetFileName(m.Path) == launchMap);
if (map == null)
throw new ArgumentException($"Could not find map '{launchMap}'.");

View File

@@ -10,6 +10,7 @@
#endregion
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Network;
@@ -19,6 +20,9 @@ namespace OpenRA
{
public class GameInformation
{
[FluentReference("name", "number")]
const string EnumeratedBotName = "enumerated-bot-name";
public string Mod;
public string Version;
@@ -36,26 +40,41 @@ namespace OpenRA
public TimeSpan Duration => EndTimeUtc > StartTimeUtc ? EndTimeUtc - StartTimeUtc : TimeSpan.Zero;
public IList<Player> Players { get; }
public HashSet<int> DisabledSpawnPoints = new();
public MapPreview MapPreview => Game.ModData.MapCache[MapUid];
public FrozenSet<int> DisabledSpawnPoints = FrozenSet<int>.Empty;
public IEnumerable<Player> HumanPlayers { get { return Players.Where(p => p.IsHuman); } }
public bool IsSinglePlayer => HumanPlayers.Count() == 1;
[FieldLoader.Ignore]
public MapGenerationArgs MapGenerationArgs;
readonly Dictionary<OpenRA.Player, Player> playersByRuntime;
public GameInformation()
{
Players = new List<Player>();
playersByRuntime = new Dictionary<OpenRA.Player, Player>();
Players = [];
playersByRuntime = [];
}
public static GameInformation Deserialize(string data)
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)
{
try
{
var info = new GameInformation();
var nodes = MiniYaml.FromString(data);
var nodes = MiniYaml.FromString(data, path);
foreach (var node in nodes)
{
var keyParts = node.Key.Split('@');
@@ -69,6 +88,10 @@ namespace OpenRA
case "Player":
info.Players.Add(FieldLoader.Load<Player>(node.Value));
break;
case "MapGenerationArgs":
info.MapGenerationArgs = FieldLoader.Load<MapGenerationArgs>(node.Value);
break;
}
}
@@ -85,23 +108,24 @@ namespace OpenRA
{
var nodes = new List<MiniYamlNode>
{
new MiniYamlNode("Root", FieldSaver.Save(this))
new("Root", FieldSaver.Save(this))
};
for (var i = 0; i < Players.Count; i++)
nodes.Add(new MiniYamlNode($"Player@{i}", FieldSaver.Save(Players[i])));
if (MapGenerationArgs != null)
nodes.Add(new MiniYamlNode("MapGenerationArgs", new MiniYaml("", MapGenerationArgs.Serialize())));
return nodes.WriteToString();
}
/// <summary>Adds the player information at start-up.</summary>
public void AddPlayer(OpenRA.Player runtimePlayer, Session lobbyInfo)
{
if (runtimePlayer == null)
throw new ArgumentNullException(nameof(runtimePlayer));
ArgumentNullException.ThrowIfNull(runtimePlayer);
if (lobbyInfo == null)
throw new ArgumentNullException(nameof(lobbyInfo));
ArgumentNullException.ThrowIfNull(lobbyInfo);
// We don't care about spectators and map players
if (runtimePlayer.NonCombatant || !runtimePlayer.Playable)
@@ -118,11 +142,12 @@ namespace OpenRA
Name = runtimePlayer.PlayerName,
IsHuman = !runtimePlayer.IsBot,
IsBot = runtimePlayer.IsBot,
BotType = runtimePlayer.BotType,
FactionName = runtimePlayer.Faction.Name,
FactionId = runtimePlayer.Faction.InternalName,
DisplayFactionName = runtimePlayer.DisplayFaction.Name,
DisplayFactionId = runtimePlayer.DisplayFaction.InternalName,
Color = runtimePlayer.Color,
Color = OpenRA.Player.GetColor(runtimePlayer),
Team = client.Team,
Handicap = client.Handicap,
SpawnPoint = runtimePlayer.SpawnPoint,
@@ -143,6 +168,19 @@ namespace OpenRA
return player;
}
public string ResolvedPlayerName(Player player)
{
if (player.IsBot)
{
var number = Players.Where(p => p.BotType == player.BotType).ToList().IndexOf(player) + 1;
return FluentProvider.GetMessage(EnumeratedBotName,
"name", FluentProvider.GetMessage(player.Name),
"number", number);
}
return player.Name;
}
public class Player
{
#region Start-up information
@@ -153,6 +191,7 @@ namespace OpenRA
public string Name;
public bool IsHuman;
public bool IsBot;
public string BotType;
/// <summary>The faction's display name.</summary>
public string FactionName;

View File

@@ -22,7 +22,7 @@ namespace OpenRA
/// </summary>
public class ActorInfo
{
public const string AbstractActorPrefix = "^";
public const char AbstractActorPrefix = '^';
public const char TraitInstanceSeparator = '@';
/// <summary>
@@ -32,8 +32,8 @@ namespace OpenRA
/// You can remove inherited traits by adding a - in front of them as in -TraitName: to inherit everything, but this trait.
/// </summary>
public readonly string Name;
readonly TypeDictionary traits = new();
List<TraitInfo> constructOrderCache = null;
readonly TypeDictionary traits = [];
TraitInfo[] constructOrderCache = null;
public ActorInfo(ObjectCreator creator, string name, MiniYaml node)
{
@@ -130,6 +130,7 @@ namespace OpenRA
// Continue resolving traits as long as possible.
// Each time we resolve some traits, this means dependencies for other traits may then be possible to satisfy in the next pass.
#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection
var readyToResolve = more.ToList();
while (readyToResolve.Count != 0)
{
@@ -138,6 +139,7 @@ namespace OpenRA
readyToResolve.Clear();
readyToResolve.AddRange(more);
}
#pragma warning restore CA1851
if (unresolved.Count != 0)
{
@@ -160,7 +162,7 @@ namespace OpenRA
throw new YamlException(exceptionString);
}
constructOrderCache = resolved.Select(r => r.Trait).ToList();
constructOrderCache = resolved.Select(r => r.Trait).ToArray();
return constructOrderCache;
}
@@ -185,7 +187,7 @@ namespace OpenRA
public bool HasTraitInfo<T>() where T : ITraitInfoInterface { return traits.Contains<T>(); }
public T TraitInfo<T>() where T : ITraitInfoInterface { return traits.Get<T>(); }
public T TraitInfoOrDefault<T>() where T : ITraitInfoInterface { return traits.GetOrDefault<T>(); }
public IEnumerable<T> TraitInfos<T>() where T : ITraitInfoInterface { return traits.WithInterface<T>(); }
public IReadOnlyCollection<T> TraitInfos<T>() where T : ITraitInfoInterface { return traits.WithInterface<T>(); }
public BitSet<TargetableType> GetAllTargetTypes()
{

View File

@@ -11,6 +11,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using OpenRA.FileSystem;
@@ -124,7 +125,7 @@ namespace OpenRA
{
var actors = MergeOrDefault("Manifest,Rules", fs, m.Rules, null, null,
k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value),
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal));
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix));
var weapons = MergeOrDefault("Manifest,Weapons", fs, m.Weapons, null, null,
k => new WeaponInfo(k.Value));
@@ -182,7 +183,7 @@ namespace OpenRA
{
var actors = MergeOrDefault("Rules", fileSystem, m.Rules, mapRules, dr.Actors,
k => new ActorInfo(modData.ObjectCreator, k.Key.ToLowerInvariant(), k.Value),
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal));
filterNode: n => n.Key.StartsWith(ActorInfo.AbstractActorPrefix));
var weapons = MergeOrDefault("Weapons", fileSystem, m.Weapons, mapWeapons, dr.Weapons,
k => new WeaponInfo(k.Value));
@@ -226,10 +227,10 @@ namespace OpenRA
static bool AnyCustomYaml(MiniYaml yaml)
{
return yaml != null && (yaml.Value != null || yaml.Nodes.Count > 0);
return yaml != null && (yaml.Value != null || yaml.Nodes.Length > 0);
}
static bool AnyFlaggedTraits(ModData modData, List<MiniYamlNode> actors)
static bool AnyFlaggedTraits(ModData modData, IEnumerable<MiniYamlNode> actors)
{
foreach (var actorNode in actors)
{
@@ -260,18 +261,18 @@ namespace OpenRA
return true;
// Any trait overrides that aren't explicitly whitelisted are flagged
if (mapRules != null)
{
if (AnyFlaggedTraits(modData, mapRules.Nodes))
return true;
if (mapRules == null)
return false;
if (mapRules.Value != null)
{
var mapFiles = FieldLoader.GetValue<string[]>("value", mapRules.Value);
foreach (var f in mapFiles)
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
return true;
}
if (AnyFlaggedTraits(modData, mapRules.Nodes))
return true;
if (mapRules.Value != null)
{
var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", mapRules.Value);
foreach (var f in mapFiles)
if (AnyFlaggedTraits(modData, MiniYaml.FromStream(fileSystem.Open(f), f)))
return true;
}
return false;

View File

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

View File

@@ -11,6 +11,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.Effects;
using OpenRA.Primitives;
@@ -36,7 +37,7 @@ namespace OpenRA.GameRules
public class WarheadArgs
{
public WeaponInfo Weapon;
public int[] DamageModifiers = Array.Empty<int>();
public int[] DamageModifiers = [];
public WPos? Source;
public WRot ImpactOrientation;
public WPos ImpactPosition;
@@ -82,13 +83,13 @@ namespace OpenRA.GameRules
public readonly WVec FollowingBurstTargetOffset = WVec.Zero;
[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.")]
public readonly string[] StartBurstReport = null;
public readonly ImmutableArray<string> StartBurstReport = default;
[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.")]
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.",
"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.")]
public readonly WDist MinRange = WDist.Zero;
@@ -128,7 +129,7 @@ namespace OpenRA.GameRules
public readonly IProjectileInfo Projectile;
[FieldLoader.LoadUsing(nameof(LoadWarheads))]
public readonly List<IWarhead> Warheads = new();
public readonly ImmutableArray<IWarhead> Warheads = [];
/// <summary>
/// This constructor is used solely for documentation generation.
@@ -139,13 +140,14 @@ namespace OpenRA.GameRules
{
// Resolve any weapon-level yaml inheritance or removals
// HACK: The "Defaults" sequence syntax prevents us from doing this generally during yaml parsing
content.Nodes = MiniYaml.Merge(new[] { content.Nodes });
content = content.WithNodes(MiniYaml.Merge([content.Nodes]));
FieldLoader.Load(this, content);
}
static object LoadProjectile(MiniYaml yaml)
{
if (!yaml.ToDictionary().TryGetValue("Projectile", out var proj))
var proj = yaml.NodeWithKeyOrDefault("Projectile")?.Value;
if (proj == null)
return null;
var ret = Game.CreateObject<IProjectileInfo>(proj.Value + "Info");
@@ -159,7 +161,7 @@ namespace OpenRA.GameRules
static object LoadWarheads(MiniYaml yaml)
{
var retList = new List<IWarhead>();
foreach (var node in yaml.Nodes.Where(n => n.Key.StartsWith("Warhead")))
foreach (var node in yaml.Nodes.Where(n => n.Key.StartsWith("Warhead", StringComparison.Ordinal)))
{
var ret = Game.CreateObject<IWarhead>(node.Value.Value + "Warhead");
if (ret == null)
@@ -169,7 +171,7 @@ namespace OpenRA.GameRules
retList.Add(ret);
}
return retList;
return retList.ToImmutableArray();
}
public bool IsValidTarget(BitSet<TargetableType> targetTypes)

View File

@@ -10,13 +10,13 @@
#endregion
using System.Collections.Generic;
using System.Linq;
using OpenRA.Traits;
namespace OpenRA
{
public class GameSpeed
{
[TranslationReference]
[FluentReference]
[FieldLoader.Require]
public readonly string Name;
@@ -32,13 +32,14 @@ namespace OpenRA
[FieldLoader.Require]
public readonly string DefaultSpeed;
[IncludeFluentReferences(LintDictionaryReference.Values)]
[FieldLoader.LoadUsing(nameof(LoadSpeeds))]
public readonly Dictionary<string, GameSpeed> Speeds;
static object LoadSpeeds(MiniYaml y)
{
var ret = new Dictionary<string, GameSpeed>();
var speedsNode = y.Nodes.FirstOrDefault(n => n.Key == "Speeds");
var speedsNode = y.NodeWithKeyOrDefault("Speeds");
if (speedsNode == null)
throw new YamlException("Error parsing GameSpeeds: Missing Speeds node!");

View File

@@ -10,6 +10,7 @@
#endregion
using System;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.Primitives;
using OpenRA.Support;
@@ -22,6 +23,7 @@ namespace OpenRA.Graphics
public string Name { get; private set; }
public bool IsDecoration { get; set; }
readonly Map map;
readonly SequenceSet sequences;
readonly Func<WAngle> facingFunc;
readonly Func<bool> paused;
@@ -43,6 +45,7 @@ namespace OpenRA.Graphics
public Animation(World world, string name, Func<WAngle> facingFunc, Func<bool> paused)
{
map = world.Map;
sequences = world.Map.Sequences;
Name = name.ToLowerInvariant();
this.facingFunc = facingFunc;
@@ -58,18 +61,23 @@ namespace OpenRA.Graphics
var tintModifiers = CurrentSequence.IgnoreWorldTint ? TintModifiers.IgnoreWorldTint : TintModifiers.None;
var alpha = CurrentSequence.GetAlpha(CurrentFrame);
var (image, rotation) = CurrentSequence.GetSpriteWithRotation(CurrentFrame, facingFunc());
var imageRenderable = new SpriteRenderable(image, pos, offset, CurrentSequence.ZOffset + zOffset, palette, CurrentSequence.Scale, alpha, float3.Ones, tintModifiers, IsDecoration,
rotation);
var imageRenderable = new SpriteRenderable(
image, pos, offset, CurrentSequence.ZOffset + zOffset, palette,
CurrentSequence.Scale, alpha, float3.Ones, tintModifiers, IsDecoration, rotation);
var shadow = CurrentSequence.GetShadow(CurrentFrame, facingFunc());
if (shadow != null)
{
var shadowRenderable = new SpriteRenderable(shadow, pos, offset, CurrentSequence.ShadowZOffset + zOffset, palette, CurrentSequence.Scale, 1f, float3.Ones, tintModifiers,
var height = map.DistanceAboveTerrain(pos).Length;
var shadowRenderable = new SpriteRenderable(
shadow, pos, offset - new WVec(0, 0, height), CurrentSequence.ShadowZOffset + zOffset + height, palette,
CurrentSequence.Scale, 1f, float3.Ones, tintModifiers,
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)
@@ -85,10 +93,10 @@ namespace OpenRA.Graphics
{
var shadowPos = pos - new int2((int)(scale * shadow.Size.X / 2), (int)(scale * shadow.Size.Y / 2));
var shadowRenderable = new UISpriteRenderable(shadow, WPos.Zero + offset, shadowPos, CurrentSequence.ShadowZOffset + zOffset, palette, scale, 1f, rotation);
return new IRenderable[] { shadowRenderable, imageRenderable };
return [shadowRenderable, imageRenderable];
}
return new IRenderable[] { imageRenderable };
return [imageRenderable];
}
public Rectangle ScreenBounds(WorldRenderer wr, WPos pos, in WVec offset)
@@ -243,9 +251,9 @@ namespace OpenRA.Graphics
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
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Primitives;
@@ -47,9 +49,9 @@ namespace OpenRA.Graphics
public readonly string Image2x = 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 Dictionary<string, Rectangle> Regions = new();
public readonly FrozenDictionary<string, Rectangle> Regions = FrozenDictionary<string, Rectangle>.Empty;
}
public static IReadOnlyDictionary<string, Collection> Collections => collections;
@@ -71,17 +73,18 @@ namespace OpenRA.Graphics
dpiScale = Game.Renderer.WindowScale;
fileSystem = modData.DefaultFileSystem;
collections = new Dictionary<string, Collection>();
cachedSheets = new Dictionary<string, (Sheet, int)>();
cachedSprites = new Dictionary<string, Dictionary<string, Sprite>>();
cachedPanelSprites = new Dictionary<string, Sprite[]>();
cachedCollectionSheets = new Dictionary<Collection, (Sheet, int)>();
collections = [];
cachedSheets = [];
cachedSprites = [];
cachedPanelSprites = [];
cachedCollectionSheets = [];
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var chrome = MiniYaml.Merge(modData.Manifest.Chrome
.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s)));
.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool)));
foreach (var c in chrome)
if (!c.Key.StartsWith("^", StringComparison.Ordinal))
if (!c.Key.StartsWith('^'))
LoadCollection(c.Key, c.Value);
}
@@ -169,7 +172,7 @@ namespace OpenRA.Graphics
var sheetDensity = SheetForCollection(collection);
if (cachedCollection == null)
{
cachedCollection = new Dictionary<string, Sprite>();
cachedCollection = [];
cachedSprites.Add(collectionName, cachedCollection);
}
@@ -227,19 +230,23 @@ namespace OpenRA.Graphics
(PanelSides.Bottom | PanelSides.Right, new Rectangle(pr[0] + pr[2] + pr[4], pr[1] + pr[3] + pr[5], pr[6], pr[7]))
};
sprites = sides.Select(x => ps.HasSide(x.PanelSides) ? new Sprite(sheetDensity.Sheet, sheetDensity.Density * x.Bounds, TextureChannel.RGBA, 1f / sheetDensity.Density) : null)
sprites = sides
.Select(x =>
ps.HasSide(x.PanelSides)
? new Sprite(sheetDensity.Sheet, sheetDensity.Density * x.Bounds, TextureChannel.RGBA, 1f / sheetDensity.Density)
: null)
.ToArray();
}
else
{
// PERF: We don't need to search for images if there are no definitions.
// PERF: It's more efficient to send an empty array rather than an array of 9 nulls.
if (!collection.Regions.Any())
return Array.Empty<Sprite>();
if (collection.Regions.Count == 0)
return [];
// Support manual definitions for unusual dialog layouts
sprites = new[]
{
sprites =
[
TryGetImage(collectionName, "corner-tl"),
TryGetImage(collectionName, "border-t"),
TryGetImage(collectionName, "corner-tr"),
@@ -249,7 +256,7 @@ namespace OpenRA.Graphics
TryGetImage(collectionName, "corner-bl"),
TryGetImage(collectionName, "border-b"),
TryGetImage(collectionName, "corner-br")
};
];
}
cachedPanelSprites.Add(collectionName, sprites);

View File

@@ -11,11 +11,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Graphics
{
public sealed class CursorManager
public sealed class CursorManager : IDisposable
{
sealed class Cursor
{
@@ -28,8 +30,8 @@ namespace OpenRA.Graphics
public IHardwareCursor[] Cursors;
}
readonly Dictionary<string, Cursor> cursors = new();
readonly SheetBuilder sheetBuilder;
readonly Dictionary<string, Cursor> cursors = [];
public readonly SheetBuilder SheetBuilder;
readonly GraphicSettings graphicSettings;
Cursor cursor;
@@ -38,16 +40,35 @@ namespace OpenRA.Graphics
readonly bool hardwareCursorsDisabled = false;
bool hardwareCursorsDoubled = false;
public CursorManager(CursorProvider cursorProvider)
public CursorManager(ModData modData)
{
hardwareCursorsDisabled = Game.Settings.Graphics.DisableHardwareCursors;
graphicSettings = Game.Settings.Graphics;
sheetBuilder = new SheetBuilder(SheetType.BGRA);
foreach (var kv in cursorProvider.Cursors)
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.
foreach (var kv in modData.Cursors)
{
var frames = kv.Value.Frames;
var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? cursorProvider.Palettes[kv.Value.Palette] : null;
var cursorSprites = frameCache[kv.Value.Src];
var length = kv.Value.Length ?? cursorSprites.Length - kv.Value.Start;
if (kv.Value.Start > cursorSprites.Length)
throw new YamlException($"Cursor {kv.Value.Name}: {nameof(kv.Value.Start)} is greater than the length of the sprite sequence.");
if (kv.Value.Length > cursorSprites.Length)
throw new YamlException($"Cursor {kv.Value.Name}: {nameof(kv.Value.Length)} is greater than the length of the sprite sequence.");
var frames = cursorSprites.Skip(kv.Value.Start).Take(length).ToArray();
var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? paletteCache[kv.Value.Palette] : null;
var c = new Cursor
{
@@ -78,7 +99,7 @@ namespace OpenRA.Graphics
type = SpriteFrameType.Bgra32;
}
c.Sprites[c.Length++] = sheetBuilder.Add(data, type, f.Size, 0, hotspot);
c.Sprites[c.Length++] = SheetBuilder.Add(data, type, f.Size, 0, hotspot);
// Bounds relative to the hotspot
c.Bounds = Rectangle.Union(c.Bounds, new Rectangle(hotspot, f.Size));
@@ -90,12 +111,12 @@ namespace OpenRA.Graphics
cursors.Add(kv.Key, c);
}
CreateOrUpdateHardwareCursors();
foreach (var s in sheetBuilder.AllSheets)
s.ReleaseBuffer();
Update();
// Allow the utility to create a cursor manager.
if (Game.Renderer != null)
{
CreateOrUpdateHardwareCursors();
Update();
}
}
void CreateOrUpdateHardwareCursors()
@@ -128,6 +149,8 @@ namespace OpenRA.Graphics
}
}
SheetBuilder.Current?.ReleaseBuffer();
hardwareCursorsDoubled = graphicSettings.CursorDouble;
}
@@ -229,6 +252,10 @@ namespace OpenRA.Graphics
var width = frame.Size.Width;
var height = frame.Size.Height;
if (width == 0 || height == 0)
return [];
var data = new byte[4 * width * height];
unsafe
{
@@ -273,11 +300,8 @@ namespace OpenRA.Graphics
{
for (var i = 0; i < c.Cursors.Length; i++)
{
if (c.Cursors[i] != null)
{
c.Cursors[i].Dispose();
c.Cursors[i] = null;
}
c.Cursors[i]?.Dispose();
c.Cursors[i] = null;
}
}
}
@@ -287,7 +311,7 @@ namespace OpenRA.Graphics
ClearHardwareCursors();
cursors.Clear();
sheetBuilder.Dispose();
SheetBuilder.Dispose();
}
}
}

View File

@@ -1,66 +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 sequenceYaml = MiniYaml.Merge(modData.Manifest.Cursors.Select(
s => MiniYaml.FromStream(fileSystem.Open(s), s)));
var nodesDict = new MiniYaml(null, sequenceYaml).ToDictionary();
// 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 = nodesDict["Cursors"].Nodes.Select(n => n.Value.Value)
.Where(p => p != null)
.Distinct()
.ToDictionary(p => p, p => pals[p].ReadPalette(modData.DefaultFileSystem));
var frameCache = new FrameCache(fileSystem, modData.SpriteLoaders);
var cursors = new Dictionary<string, CursorSequence>();
foreach (var s in nodesDict["Cursors"].Nodes)
foreach (var 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,60 +9,44 @@
*/
#endregion
using System.Linq;
namespace OpenRA.Graphics
{
public class CursorSequence
{
public readonly string Name;
public readonly string Src;
public readonly int Start;
public readonly int Length;
public readonly int? Length;
public readonly string Palette;
public readonly int2 Hotspot;
public readonly ISpriteFrame[] Frames;
public CursorSequence(FrameCache cache, string name, string cursorSrc, string palette, MiniYaml info)
public CursorSequence(string name, string cursorSrc, string palette, MiniYaml info)
{
var d = info.ToDictionary();
Start = Exts.ParseIntegerInvariant(d["Start"].Value);
Palette = palette;
Name = name;
Src = cursorSrc;
var cursorSprites = cache[cursorSrc];
Frames = cursorSprites.Skip(Start).ToArray();
if ((d.TryGetValue("Length", out var yaml) && yaml.Value == "*") ||
(d.TryGetValue("End", out yaml) && yaml.Value == "*"))
Length = Frames.Length;
else if (d.TryGetValue("Length", out yaml))
Length = Exts.ParseIntegerInvariant(yaml.Value);
else if (d.TryGetValue("End", out yaml))
Length = Exts.ParseIntegerInvariant(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))
if (d.TryGetValue("X", out var yaml))
{
Exts.TryParseIntegerInvariant(yaml.Value, out var x);
Exts.TryParseInt32Invariant(yaml.Value, out var x);
Hotspot = Hotspot.WithX(x);
}
if (d.TryGetValue("Y", out yaml))
{
Exts.TryParseIntegerInvariant(yaml.Value, out var y);
Exts.TryParseInt32Invariant(yaml.Value, out var y);
Hotspot = Hotspot.WithY(y);
}
Start = Exts.ParseInt32Invariant(d["Start"].Value);
if (d.TryGetValue("Length", out yaml))
Length = yaml.Value != "*" ? Exts.ParseInt32Invariant(yaml.Value) : null;
else if (d.TryGetValue("End", out yaml) && yaml.Value == "*")
Length = yaml.Value != "*" ? Exts.ParseInt32Invariant(yaml.Value) - Start : null;
else
Length = 1;
}
}
}

View File

@@ -21,11 +21,11 @@ namespace OpenRA.Graphics
public ITexture ColorShifts { get; }
public int Height { get; private set; }
readonly Dictionary<string, ImmutablePalette> palettes = new();
readonly Dictionary<string, MutablePalette> mutablePalettes = new();
readonly Dictionary<string, int> indices = new();
byte[] buffer = Array.Empty<byte>();
float[] colorShiftBuffer = Array.Empty<float>();
readonly Dictionary<string, ImmutablePalette> palettes = [];
readonly Dictionary<string, MutablePalette> mutablePalettes = [];
readonly Dictionary<string, int> indices = [];
byte[] buffer = [];
float[] colorShiftBuffer = [];
public HardwarePalette()
{
@@ -85,7 +85,10 @@ namespace OpenRA.Graphics
public void ReplacePalette(string name, IPalette p)
{
if (mutablePalettes.ContainsKey(name))
{
palettes[name] = new ImmutablePalette(p);
CopyPaletteToBuffer(indices[name], mutablePalettes[name] = new MutablePalette(p));
}
else if (palettes.ContainsKey(name))
CopyPaletteToBuffer(indices[name], palettes[name] = new ImmutablePalette(p));
else

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using OpenRA.Primitives;
@@ -30,7 +31,7 @@ namespace OpenRA.Graphics
public static Color GetColor(this IPalette palette, int index)
{
return Color.FromArgb((int)palette[index]);
return Color.FromArgb(palette[index]);
}
public static IPalette AsReadOnly(this IPalette palette)
@@ -64,18 +65,18 @@ namespace OpenRA.Graphics
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))
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);
}
void LoadFromStream(Stream s, int[] remapTransparent, int[] remapShadow)
void LoadFromStream(Stream s, ImmutableArray<int> remapTransparent, ImmutableArray<int> remapShadow)
{
using (var reader = new BinaryReader(s))
for (var i = 0; i < Palette.Size; i++)
@@ -103,7 +104,7 @@ namespace OpenRA.Graphics
: this(p)
{
for (var i = 0; i < Palette.Size; i++)
colors[i] = (uint)r.GetRemappedColor(this.GetColor(i), i).ToArgb();
colors[i] = r.GetRemappedColor(this.GetColor(i), i).ToArgb();
}
public ImmutablePalette(IPalette p)
@@ -142,7 +143,7 @@ namespace OpenRA.Graphics
public void SetColor(int index, Color color)
{
colors[index] = (uint)color.ToArgb();
colors[index] = color.ToArgb();
}
public void SetFromPalette(IPalette p)
@@ -153,7 +154,7 @@ namespace OpenRA.Graphics
public void ApplyRemap(IPaletteRemap r)
{
for (var i = 0; i < Palette.Size; i++)
colors[i] = (uint)r.GetRemappedColor(this.GetColor(i), i).ToArgb();
colors[i] = r.GetRemappedColor(this.GetColor(i), i).ToArgb();
}
}
}

View File

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

View File

@@ -20,13 +20,13 @@ namespace OpenRA
Automatic,
ANGLE,
Modern,
Embedded,
Legacy
Embedded
}
public interface IPlatform
{
IPlatformWindow CreateWindow(Size size, WindowMode windowMode, float scaleModifier, int batchSize, int videoDisplay, GLProfile profile, bool enableLegacyGL);
IPlatformWindow CreateWindow(
Size size, WindowMode windowMode, float scaleModifier, int vertexBatchSize, int indexBatchSize, int videoDisplay, GLProfile profile);
ISoundEngine CreateSound(string device);
IFont CreateFont(byte[] data);
}
@@ -66,6 +66,7 @@ namespace OpenRA
void PumpInput(IInputHandler inputHandler);
string GetClipboardText();
bool SetClipboardText(string text);
bool TryOpenUrl(string url);
void GrabWindowMouseFocus();
void ReleaseWindowMouseFocus();
@@ -83,16 +84,19 @@ namespace OpenRA
public interface IGraphicsContext : IDisposable
{
IVertexBuffer<Vertex> CreateVertexBuffer(int size);
Vertex[] CreateVertices(int size);
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;
IIndexBuffer CreateIndexBuffer(uint[] indices);
ITexture CreateTexture();
IFrameBuffer CreateFrameBuffer(Size s);
IFrameBuffer CreateFrameBuffer(Size s, Color clearColor);
IShader CreateShader(string name);
IShader CreateShader(IShaderBindings shaderBindings);
void EnableScissor(int x, int y, int width, int height);
void DisableScissor();
void Present();
void DrawPrimitives(PrimitiveType pt, int firstVertex, int numVertices);
void DrawElements(int numIndices, int offset);
void Clear();
void EnableDepthBuffer();
void DisableDepthBuffer();
@@ -102,7 +106,14 @@ namespace OpenRA
string GLVersion { get; }
}
public interface IVertexBuffer<T> : IDisposable
public interface IRenderer
{
void BeginFrame();
void EndFrame();
void SetPalette(HardwarePalette palette);
}
public interface IVertexBuffer<T> : IDisposable where T : struct
{
void Bind();
void SetData(T[] vertices, int length);
@@ -114,16 +125,32 @@ namespace OpenRA
void SetData(T[] vertices, int offset, int start, int length);
}
public interface IIndexBuffer : IDisposable
{
void Bind();
}
public interface IShader
{
void SetBool(string name, bool value);
void SetVec(string name, float x);
void SetVec(string name, float x, float y);
void SetVec(string name, float x, float y, float z);
void SetVec(string name, float[] vec, int length);
void SetVec(string name, ReadOnlyMemory<float> vec, int length);
void SetTexture(string param, ITexture texture);
void SetMatrix(string param, float[] mtx);
void PrepareRender();
void Bind();
}
public interface IShaderBindings
{
string VertexShaderName { get; }
string VertexShaderCode { get; }
string FragmentShaderName { get; }
string FragmentShaderCode { get; }
int Stride { get; }
ShaderVertexAttribute[] Attributes { get; }
}
public enum TextureScaleFilter { Nearest, Linear }
@@ -132,6 +159,7 @@ namespace OpenRA
{
void SetData(byte[] colors, int width, int height);
void SetFloatData(float[] data, int width, int height);
void SetDataFromReadBuffer(Rectangle rect);
byte[] GetData();
Size Size { get; }
TextureScaleFilter ScaleFilter { get; set; }
@@ -153,12 +181,6 @@ namespace OpenRA
TriangleList,
}
public readonly struct Range<T>
{
public readonly T Start, End;
public Range(T start, T end) { Start = start; End = end; }
}
public enum WindowMode
{
Windowed,

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ namespace OpenRA.Graphics
static readonly float3 Offset = new(0.5f, 0.5f, 0f);
readonly SpriteRenderer parent;
readonly Vertex[] vertices = new Vertex[6];
readonly Vertex[] vertices = new Vertex[4];
public RgbaColorRenderer(SpriteRenderer parent)
{
@@ -45,14 +45,12 @@ namespace OpenRA.Graphics
var eb = endColor.B / 255.0f;
var ea = endColor.A / 255.0f;
vertices[0] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0, 0);
vertices[1] = new Vertex(start + corner + Offset, sr, sg, sb, sa, 0, 0);
vertices[2] = new Vertex(end + corner + Offset, er, eg, eb, ea, 0, 0);
vertices[3] = new Vertex(end + corner + Offset, er, eg, eb, ea, 0, 0);
vertices[4] = new Vertex(end - corner + Offset, er, eg, eb, ea, 0, 0);
vertices[5] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0, 0);
vertices[0] = new Vertex(start - corner + Offset, sr, sg, sb, sa, 0);
vertices[1] = new Vertex(start + corner + Offset, sr, sg, sb, sa, 0);
vertices[2] = new Vertex(end + corner + Offset, er, eg, eb, ea, 0);
vertices[3] = new Vertex(end - corner + Offset, er, eg, eb, ea, 0);
parent.DrawRGBAVertices(vertices, blendMode);
parent.DrawRGBAQuad(vertices, blendMode);
}
public void DrawLine(in float3 start, in float3 end, float width, Color color, BlendMode blendMode = BlendMode.Alpha)
@@ -66,13 +64,11 @@ namespace OpenRA.Graphics
var b = color.B / 255.0f;
var a = color.A / 255.0f;
vertices[0] = new Vertex(start - corner + Offset, r, g, b, a, 0, 0);
vertices[1] = new Vertex(start + corner + Offset, r, g, b, a, 0, 0);
vertices[2] = new Vertex(end + corner + Offset, r, g, b, a, 0, 0);
vertices[3] = new Vertex(end + corner + Offset, r, g, b, a, 0, 0);
vertices[4] = new Vertex(end - corner + Offset, r, g, b, a, 0, 0);
vertices[5] = new Vertex(start - corner + Offset, r, g, b, a, 0, 0);
parent.DrawRGBAVertices(vertices, blendMode);
vertices[0] = new Vertex(start - corner + Offset, r, g, b, a, 0);
vertices[1] = new Vertex(start + corner + Offset, r, g, b, a, 0);
vertices[2] = new Vertex(end + corner + Offset, r, g, b, a, 0);
vertices[3] = new Vertex(end - corner + Offset, r, g, b, a, 0);
parent.DrawRGBAQuad(vertices, blendMode);
}
/// <summary>
@@ -157,13 +153,11 @@ namespace OpenRA.Graphics
var cd = closed || i < limit - 1 ? IntersectionOf(end - corner, dir, end - nextCorner, nextDir) : end - corner;
// Fill segment
vertices[0] = new Vertex(ca + Offset, r, g, b, a, 0, 0);
vertices[1] = new Vertex(cb + Offset, r, g, b, a, 0, 0);
vertices[2] = new Vertex(cc + Offset, r, g, b, a, 0, 0);
vertices[3] = new Vertex(cc + Offset, r, g, b, a, 0, 0);
vertices[4] = new Vertex(cd + Offset, r, g, b, a, 0, 0);
vertices[5] = new Vertex(ca + Offset, r, g, b, a, 0, 0);
parent.DrawRGBAVertices(vertices, blendMode);
vertices[0] = new Vertex(ca + Offset, r, g, b, a, 0);
vertices[1] = new Vertex(cb + Offset, r, g, b, a, 0);
vertices[2] = new Vertex(cc + Offset, r, g, b, a, 0);
vertices[3] = new Vertex(cd + Offset, r, g, b, a, 0);
parent.DrawRGBAQuad(vertices, blendMode);
// Advance line segment
end = next;
@@ -197,21 +191,7 @@ namespace OpenRA.Graphics
{
var tr = new float3(br.X, tl.Y, tl.Z);
var bl = new float3(tl.X, br.Y, br.Z);
DrawPolygon(new[] { tl, tr, br, bl }, width, color, blendMode);
}
public void FillTriangle(in float3 a, in float3 b, in float3 c, Color color, BlendMode blendMode = BlendMode.Alpha)
{
color = Util.PremultiplyAlpha(color);
var cr = color.R / 255.0f;
var cg = color.G / 255.0f;
var cb = color.B / 255.0f;
var ca = color.A / 255.0f;
vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0);
vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0, 0);
vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
parent.DrawRGBAVertices(vertices, blendMode);
DrawPolygon([tl, tr, br, bl], width, color, blendMode);
}
public void FillRect(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha)
@@ -229,25 +209,22 @@ namespace OpenRA.Graphics
var cb = color.B / 255.0f;
var ca = color.A / 255.0f;
vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0);
vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0, 0);
vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
vertices[3] = new Vertex(c + Offset, cr, cg, cb, ca, 0, 0);
vertices[4] = new Vertex(d + Offset, cr, cg, cb, ca, 0, 0);
vertices[5] = new Vertex(a + Offset, cr, cg, cb, ca, 0, 0);
parent.DrawRGBAVertices(vertices, blendMode);
vertices[0] = new Vertex(a + Offset, cr, cg, cb, ca, 0);
vertices[1] = new Vertex(b + Offset, cr, cg, cb, ca, 0);
vertices[2] = new Vertex(c + Offset, cr, cg, cb, ca, 0);
vertices[3] = new Vertex(d + Offset, cr, cg, cb, ca, 0);
parent.DrawRGBAQuad(vertices, blendMode);
}
public void FillRect(in float3 a, in float3 b, in float3 c, in float3 d, Color topLeftColor, Color topRightColor, Color bottomRightColor, Color bottomLeftColor, BlendMode blendMode = BlendMode.Alpha)
public void FillRect(in float3 a, in float3 b, in float3 c, in float3 d,
Color topLeftColor, Color topRightColor, Color bottomRightColor, Color bottomLeftColor, BlendMode blendMode = BlendMode.Alpha)
{
vertices[0] = VertexWithColor(a + Offset, topLeftColor);
vertices[1] = VertexWithColor(b + Offset, topRightColor);
vertices[2] = VertexWithColor(c + Offset, bottomRightColor);
vertices[3] = VertexWithColor(c + Offset, bottomRightColor);
vertices[4] = VertexWithColor(d + Offset, bottomLeftColor);
vertices[5] = VertexWithColor(a + Offset, topLeftColor);
vertices[3] = VertexWithColor(d + Offset, bottomLeftColor);
parent.DrawRGBAVertices(vertices, blendMode);
parent.DrawRGBAQuad(vertices, blendMode);
}
static Vertex VertexWithColor(in float3 xyz, Color color)
@@ -258,7 +235,7 @@ namespace OpenRA.Graphics
var cb = color.B / 255.0f;
var ca = color.A / 255.0f;
return new Vertex(xyz, cr, cg, cb, ca, 0, 0);
return new Vertex(xyz, cr, cg, cb, ca, 0);
}
public void FillEllipse(in float3 tl, in float3 br, Color color, BlendMode blendMode = BlendMode.Alpha)

View File

@@ -37,23 +37,23 @@ namespace OpenRA.Graphics
public interface ISpriteSequenceLoader
{
int BgraSheetSize { get; }
int IndexedSheetSize { get; }
IReadOnlyDictionary<string, ISpriteSequence> ParseSequences(ModData modData, string tileSet, SpriteCache cache, MiniYamlNode node);
}
public sealed class SequenceSet : IDisposable
{
public readonly string TileSet;
readonly ModData modData;
readonly string tileSet;
readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, ISpriteSequence>> images;
public SpriteCache SpriteCache { get; }
public SequenceSet(IReadOnlyFileSystem fileSystem, ModData modData, string tileSet, MiniYaml additionalSequences)
{
this.modData = modData;
this.tileSet = tileSet;
SpriteCache = new SpriteCache(fileSystem, modData.SpriteLoaders, modData.SpriteSequenceLoader.BgraSheetSize, modData.SpriteSequenceLoader.IndexedSheetSize);
TileSet = tileSet;
var rc = modData.Manifest.RendererConstants;
SpriteCache = new SpriteCache(fileSystem, modData.SpriteLoaders, rc.SequenceBgraSheetSize, rc.SequenceIndexedSheetSize);
using (new Support.PerfTimer("LoadSequences"))
images = Load(fileSystem, additionalSequences);
}
@@ -94,10 +94,10 @@ namespace OpenRA.Graphics
foreach (var node in nodes)
{
// Nodes starting with ^ are inheritable but never loaded directly
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix, StringComparison.Ordinal))
if (node.Key.StartsWith(ActorInfo.AbstractActorPrefix))
continue;
images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, tileSet, SpriteCache, node);
images[node.Key] = modData.SpriteSequenceLoader.ParseSequences(modData, TileSet, SpriteCache, node);
}
return images;

View File

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

View File

@@ -132,6 +132,7 @@ namespace OpenRA.Graphics
{
if (!Buffered)
return;
dirty = true;
releaseBufferOnCommit = true;
@@ -140,6 +141,29 @@ namespace OpenRA.Graphics
GetTexture();
}
public bool ReleaseBufferAndTryTransferTo(Sheet destination)
{
if (Size != destination.Size)
throw new ArgumentException("Destination sheet does not have the same size", nameof(destination));
var buffer = data;
ReleaseBuffer();
// We aren't committing data to the GPU, so let's not delete our data.
if (Game.Renderer == null)
return false;
// Only transfer if the destination has no data that would be lost by overwriting.
if (buffer != null && destination.data == null && destination.texture == null)
{
Array.Clear(buffer, 0, buffer.Length);
destination.data = buffer;
return true;
}
return false;
}
public void Dispose()
{
texture?.Dispose();

View File

@@ -16,7 +16,6 @@ using OpenRA.Primitives;
namespace OpenRA.Graphics
{
[Serializable]
public class SheetOverflowException : Exception
{
public SheetOverflowException(string message)
@@ -34,7 +33,7 @@ namespace OpenRA.Graphics
public sealed class SheetBuilder : IDisposable
{
public readonly SheetType Type;
readonly List<Sheet> sheets = new();
readonly List<Sheet> sheets = [];
readonly Func<Sheet> allocateSheet;
readonly int margin;
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)
: this(t, () => AllocateSheet(t, sheetSize), margin) { }
@@ -76,22 +72,26 @@ namespace OpenRA.Graphics
{
CurrentChannel = t == SheetType.Indexed ? TextureChannel.Red : TextureChannel.RGBA;
Type = t;
Current = allocateSheet();
sheets.Add(Current);
this.allocateSheet = allocateSheet;
this.margin = margin;
}
public Sprite Add(ISpriteFrame frame) { return Add(frame.Data, frame.Type, frame.Size, 0, frame.Offset); }
public Sprite Add(byte[] src, SpriteFrameType type, Size size) { return Add(src, type, size, 0, float3.Zero); }
public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset)
public Sprite Add(ISpriteFrame frame, bool premultiplied = false) { return Add(frame.Data, frame.Type, frame.Size, 0, frame.Offset, premultiplied); }
public Sprite Add(byte[] src, SpriteFrameType type, Size size, bool premultiplied = false) { return Add(src, type, size, 0, float3.Zero, premultiplied); }
public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset, bool premultiplied = false)
{
if (Current == null)
{
Current = allocateSheet();
sheets.Add(Current);
}
// Don't bother allocating empty sprites
if (size.Width == 0 || size.Height == 0)
return new Sprite(Current, Rectangle.Empty, 0, spriteOffset, CurrentChannel, BlendMode.Alpha);
var rect = Allocate(size, zRamp, spriteOffset);
Util.FastCopyIntoChannel(rect, src, type);
Util.FastCopyIntoChannel(rect, src, type, premultiplied);
Current.CommitBufferedData();
return rect;
}
@@ -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 zRamp, in float3 spriteOffset, float scale = 1f)
{
if (Current == null)
{
Current = allocateSheet();
sheets.Add(Current);
}
if (imageSize.Width + p.X + margin > Current.Size.Width)
{
p = new int2(0, p.Y + rowHeight + margin);
@@ -130,8 +136,13 @@ namespace OpenRA.Graphics
var next = NextChannel(CurrentChannel);
if (next == null)
{
Current.ReleaseBuffer();
var previous = Current;
Current = allocateSheet();
// Reuse the backing buffer between sheets where possible.
// This avoids allocating additional buffers which the GC must clean up.
previous.ReleaseBufferAndTryTransferTo(Current);
sheets.Add(Current);
CurrentChannel = Type == SheetType.Indexed ? TextureChannel.Red : TextureChannel.RGBA;
}
@@ -142,7 +153,9 @@ namespace OpenRA.Graphics
p = int2.Zero;
}
var rect = new Sprite(Current, new Rectangle(p.X + margin, p.Y + margin, imageSize.Width, imageSize.Height), zRamp, spriteOffset, CurrentChannel, BlendMode.Alpha, scale);
var rect = new Sprite(
Current, new Rectangle(p.X + margin, p.Y + margin, imageSize.Width, imageSize.Height),
zRamp, spriteOffset, CurrentChannel, BlendMode.Alpha, scale);
p += new int2(imageSize.Width + margin, 0);
return rect;

View File

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

View File

@@ -11,31 +11,34 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Primitives;
namespace OpenRA.Graphics
{
public delegate ISpriteFrame AdjustFrame(ISpriteFrame input, int index, int total);
public sealed class SpriteCache : IDisposable
{
public readonly Dictionary<SheetType, SheetBuilder> SheetBuilders;
readonly ISpriteLoader[] loaders;
readonly IReadOnlyFileSystem fileSystem;
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> spriteReservations = new();
readonly Dictionary<int, (int[] Frames, MiniYamlNode.SourceLocation Location)> frameReservations = new();
readonly Dictionary<string, List<int>> reservationsByFilename = new();
readonly Dictionary<
int,
(ImmutableArray<int> Frames, MiniYamlNode.SourceLocation Location, AdjustFrame AdjustFrame, bool Premultiplied)> spriteReservations = [];
readonly Dictionary<string, List<int>> reservationsByFilename = [];
readonly Dictionary<int, ISpriteFrame[]> resolvedFrames = new();
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;
public SpriteCache(IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders, int bgraSheetSize, int indexedSheetSize, int bgraSheetMargin = 1, int indexedSheetMargin = 1)
public SpriteCache(
IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders, int bgraSheetSize, int indexedSheetSize, int bgraSheetMargin = 1, int indexedSheetMargin = 1)
{
SheetBuilders = new Dictionary<SheetType, SheetBuilder>
{
@@ -47,116 +50,122 @@ namespace OpenRA.Graphics
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)
{
var token = nextReservationToken++;
spriteReservations[token] = (frames?.ToArray(), location);
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
spriteReservations[token] = (frames, location, adjustFrame, premultiplied);
reservationsByFilename.GetOrAdd(filename, _ => []).Add(token);
return token;
}
public int ReserveFrames(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
static ISpriteFrame[] GetFrames(IReadOnlyFileSystem fileSystem, string filename, ISpriteLoader[] loaders)
{
var token = nextReservationToken++;
frameReservations[token] = (frames?.ToArray(), location);
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).Add(token);
return token;
}
static ISpriteFrame[] GetFrames(IReadOnlyFileSystem fileSystem, string filename, ISpriteLoader[] loaders, out TypeDictionary metadata)
{
metadata = null;
if (!fileSystem.TryOpen(filename, out var stream))
return null;
using (stream)
{
foreach (var loader in loaders)
if (loader.TryParseSprite(stream, filename, out var frames, out metadata))
if (loader.TryParseSprite(stream, filename, out var frames, out _))
return frames;
return null;
}
}
public ISpriteFrame[] LoadFramesUncached(string filename)
{
return GetFrames(fileSystem, filename, loaders);
}
public void LoadReservations(ModData modData)
{
foreach (var sb in SheetBuilders.Values)
sb.Current.CreateBuffer();
var spriteCache = new Dictionary<int, Sprite>();
var pendingResolve = new List<(
string Filename,
int FrameIndex,
bool Premultiplied,
AdjustFrame AdjustFrame,
ISpriteFrame Frame,
Sprite[] SpritesForToken)>();
foreach (var (filename, tokens) in reservationsByFilename)
{
modData.LoadScreen?.Display();
var loadedFrames = GetFrames(fileSystem, filename, loaders, out _);
var loadedFrames = GetFrames(fileSystem, filename, loaders);
foreach (var token in tokens)
{
if (frameReservations.TryGetValue(token, out var r))
{
if (loadedFrames != null)
{
if (r.Frames != null)
{
var resolved = new ISpriteFrame[loadedFrames.Length];
foreach (var i in r.Frames)
resolved[i] = loadedFrames[i];
resolvedFrames[token] = resolved;
}
else
resolvedFrames[token] = loadedFrames;
}
else
{
resolvedFrames[token] = null;
missingFiles[token] = (filename, r.Location);
}
}
if (spriteReservations.TryGetValue(token, out r))
if (spriteReservations.TryGetValue(token, out var rs))
{
if (loadedFrames != null)
{
var resolved = new Sprite[loadedFrames.Length];
var frames = r.Frames ?? Enumerable.Range(0, loadedFrames.Length);
foreach (var i in frames)
resolved[i] = spriteCache.GetOrAdd(i,
f => SheetBuilders[SheetBuilder.FrameTypeToSheetType(loadedFrames[f].Type)].Add(loadedFrames[f]));
resolvedSprites[token] = resolved;
if (rs.Frames != null && rs.Frames.Any(i => i >= loadedFrames.Length))
throw new InvalidOperationException($"{rs.Location}: {filename} does not contain frames: " +
string.Join(',', rs.Frames.Where(f => f >= loadedFrames.Length)));
var frames = rs.Frames != null ? rs.Frames : Enumerable.Range(0, loadedFrames.Length);
var total = rs.Frames != null ? rs.Frames.Length : loadedFrames.Length;
var j = 0;
foreach (var i in frames)
{
var frame = loadedFrames[i];
if (rs.AdjustFrame != null)
frame = rs.AdjustFrame(frame, j++, total);
pendingResolve.Add((filename, i, rs.Premultiplied, rs.AdjustFrame, frame, resolved));
}
}
else
{
resolvedSprites[token] = null;
missingFiles[token] = (filename, r.Location);
missingFiles[token] = (filename, rs.Location);
}
}
}
spriteCache.Clear();
}
spriteReservations.Clear();
frameReservations.Clear();
spriteReservations.TrimExcess();
reservationsByFilename.Clear();
reservationsByFilename.TrimExcess();
// When the sheet builder is adding sprites, it reserves height for the tallest sprite seen along the row.
// We can achieve better sheet packing by keeping sprites with similar heights together.
var orderedPendingResolve = pendingResolve.OrderBy(x => x.Frame.Size.Height);
var spriteCache = new Dictionary<(
string Filename,
int FrameIndex,
bool Premultiplied,
AdjustFrame AdjustFrame),
Sprite>(pendingResolve.Count);
foreach (var (filename, frameIndex, premultiplied, adjustFrame, frame, spritesForToken) in orderedPendingResolve)
{
// Premultiplied and non-premultiplied sprites must be cached separately
// to cover the case where the same image is requested in both versions.
spritesForToken[frameIndex] = spriteCache.GetOrAdd(
(filename, frameIndex, premultiplied, adjustFrame),
_ =>
{
var sheetBuilder = SheetBuilders[SheetBuilder.FrameTypeToSheetType(frame.Type)];
return sheetBuilder.Add(frame, premultiplied);
});
modData.LoadScreen?.Display();
}
foreach (var sb in SheetBuilders.Values)
sb.Current.ReleaseBuffer();
sb.Current?.ReleaseBuffer();
}
public Sprite[] ResolveSprites(int token)
{
var resolved = resolvedSprites[token];
resolvedSprites.Remove(token);
if (missingFiles.TryGetValue(token, out var r))
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);
if (!resolvedSprites.Remove(token, out var resolved))
throw new InvalidOperationException($"{nameof(token)} {token} has either already been resolved, or was never reserved via {nameof(ReserveSprites)}");
return resolved;
}
resolvedSprites.TrimExcess();
public ISpriteFrame[] ResolveFrames(int token)
{
var resolved = resolvedFrames[token];
resolvedFrames.Remove(token);
if (missingFiles.TryGetValue(token, out var r))
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);

View File

@@ -27,7 +27,7 @@ namespace OpenRA.Graphics
float deviceScale;
public SpriteFont(string name, byte[] data, int size, int ascender, float scale, SheetBuilder builder)
public SpriteFont(IPlatform platform, string name, byte[] data, int size, int ascender, float scale, SheetBuilder builder)
{
if (builder.Type != SheetType.BGRA)
throw new ArgumentException("The sheet builder must create BGRA sheets.", nameof(builder));
@@ -36,7 +36,7 @@ namespace OpenRA.Graphics
this.size = size;
this.builder = builder;
font = Game.Renderer.CreateFont(data);
font = platform.CreateFont(data);
glyphs = new Cache<char, GlyphInfo>(CreateGlyph);
contrastGlyphs = new Cache<(char, int), Sprite>(CreateContrastGlyph);
dilationElements = new Cache<int, float[]>(CreateCircularWeightMap);

View File

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

View File

@@ -9,7 +9,6 @@
*/
#endregion
using System;
using System.Collections.Generic;
using OpenRA.Primitives;
@@ -17,7 +16,7 @@ namespace OpenRA.Graphics
{
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 WPos pos;

View File

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

View File

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

View File

@@ -21,7 +21,8 @@ namespace OpenRA.Graphics
readonly float alpha;
readonly float rotation = 0f;
public UISpriteRenderable(Sprite sprite, WPos effectiveWorldPos, int2 screenPos, int zOffset, PaletteReference palette, float scale = 1f, float alpha = 1f, float rotation = 0f)
public UISpriteRenderable(Sprite sprite, WPos effectiveWorldPos, int2 screenPos, int zOffset, PaletteReference palette,
float scale = 1f, float alpha = 1f, float rotation = 0f)
{
this.sprite = sprite;
Pos = effectiveWorldPos;
@@ -47,7 +48,8 @@ namespace OpenRA.Graphics
public PaletteReference Palette { get; }
public int ZOffset { get; }
public IPalettedRenderable WithPalette(PaletteReference newPalette) { return new UISpriteRenderable(sprite, Pos, screenPos, ZOffset, newPalette, scale, alpha, rotation); }
public IPalettedRenderable WithPalette(PaletteReference newPalette) =>
new UISpriteRenderable(sprite, Pos, screenPos, ZOffset, newPalette, scale, alpha, rotation);
public IRenderable WithZOffset(int newOffset) { return this; }
public IRenderable OffsetBy(in WVec vec) { return this; }
public IRenderable AsDecoration() { return this; }

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using OpenRA.Effects;
using OpenRA.Primitives;
using OpenRA.Traits;
@@ -31,44 +32,61 @@ namespace OpenRA.Graphics
public event Action PaletteInvalidated = null;
readonly HashSet<Actor> onScreenActors = new();
readonly HashSet<Actor> onScreenActors = [];
readonly HardwarePalette palette = new();
readonly Dictionary<string, PaletteReference> palettes = new();
readonly Dictionary<string, PaletteReference> palettes = [];
readonly IRenderTerrain terrainRenderer;
readonly Lazy<DebugVisualizations> debugVis;
readonly Func<string, PaletteReference> createPaletteReference;
readonly bool enableDepthBuffer;
readonly List<IFinalizedRenderable> preparedRenderables = new();
readonly List<IFinalizedRenderable> preparedOverlayRenderables = new();
readonly List<IFinalizedRenderable> preparedAnnotationRenderables = new();
readonly List<IFinalizedRenderable> preparedRenderables = [];
readonly List<IFinalizedRenderable> preparedOverlayRenderables = [];
readonly List<IFinalizedRenderable> preparedAnnotationRenderables = [];
readonly List<IRenderable> renderablesBuffer = new();
readonly List<IRenderable> renderablesBuffer = [];
readonly IRenderer[] renderers;
readonly IRenderPostProcessPass[] postProcessPasses;
long[] renderablesKeysBuffer = [];
internal WorldRenderer(ModData modData, World world)
{
World = world;
TileSize = World.Map.Grid.TileSize;
TileSize = World.Map.Rules.TerrainInfo.TileSize;
TileScale = World.Map.Grid.TileScale;
Viewport = new Viewport(this, world.Map);
createPaletteReference = CreatePaletteReference;
var mapGrid = modData.Manifest.Get<MapGrid>();
var mapGrid = modData.GetOrCreate<MapGrid>();
enableDepthBuffer = mapGrid.EnableDepthBuffer;
foreach (var pal in world.TraitDict.ActorsWithTrait<ILoadsPalettes>())
pal.Trait.LoadPalettes(this);
foreach (var p in world.Players)
UpdatePalettesForPlayer(p.InternalName, p.Color, false);
Player.SetupRelationshipColors(world.Players, world.LocalPlayer, this, true);
palette.Initialize();
TerrainLighting = world.WorldActor.TraitOrDefault<ITerrainLighting>();
renderers = world.WorldActor.TraitsImplementing<IRenderer>().ToArray();
terrainRenderer = world.WorldActor.TraitOrDefault<IRenderTerrain>();
debugVis = Exts.Lazy(() => world.WorldActor.TraitOrDefault<DebugVisualizations>());
debugVis = Exts.Lazy(world.WorldActor.TraitOrDefault<DebugVisualizations>);
postProcessPasses = world.WorldActor.TraitsImplementing<IRenderPostProcessPass>().ToArray();
}
public void BeginFrame()
{
foreach (var r in renderers)
r.BeginFrame();
}
public void EndFrame()
{
foreach (var r in renderers)
r.EndFrame();
}
public void UpdatePalettesForPlayer(string internalName, Color color, bool replaceExisting)
@@ -141,7 +159,14 @@ namespace OpenRA.Graphics
renderablesBuffer.AddRange(e.Render(this));
// 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));
// PERF: Reuse collection to avoid allocations.
@@ -270,6 +295,8 @@ namespace OpenRA.Graphics
if (enableDepthBuffer)
Game.Renderer.ClearDepthBuffer();
ApplyPostProcessing(PostProcessPassType.AfterActors);
World.ApplyToActorsWithTrait<IRenderAboveWorld>((actor, trait) =>
{
if (actor.IsInWorld && !actor.Disposed)
@@ -279,6 +306,8 @@ namespace OpenRA.Graphics
if (enableDepthBuffer)
Game.Renderer.ClearDepthBuffer();
ApplyPostProcessing(PostProcessPassType.AfterWorld);
World.ApplyToActorsWithTrait<IRenderShroud>((actor, trait) => trait.RenderShroud(this));
if (enableDepthBuffer)
@@ -292,9 +321,23 @@ namespace OpenRA.Graphics
foreach (var r in g)
r.Render(this);
ApplyPostProcessing(PostProcessPassType.AfterShroud);
Game.Renderer.Flush();
}
void ApplyPostProcessing(PostProcessPassType type)
{
foreach (var pass in postProcessPasses)
{
if (pass.Type != type || !pass.Enabled)
continue;
Game.Renderer.Flush();
pass.Draw(this);
}
}
public void DrawAnnotations()
{
Game.Renderer.EnableAntialiasingFilter();
@@ -337,6 +380,8 @@ namespace OpenRA.Graphics
}
}
ApplyPostProcessing(PostProcessPassType.AfterAnnotations);
Game.Renderer.Flush();
preparedRenderables.Clear();
@@ -350,12 +395,22 @@ namespace OpenRA.Graphics
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)
{
return new float2((float)TileSize.Width * pos.X / TileScale, (float)TileSize.Height * (pos.Y - pos.Z) / TileScale);
}
/// <summary>
/// Converts a world position to a screen position.
/// </summary>
public float2 ScreenPosition(float2 pos)
{
return new float2(TileSize.Width * pos.X / TileScale, TileSize.Height * pos.Y / TileScale);
}
public float3 Screen3DPosition(WPos pos)
{
// The projection from world coordinates to screen coordinates has
@@ -395,7 +450,7 @@ namespace OpenRA.Graphics
public float[] ScreenVector(in WVec 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)

View File

@@ -9,18 +9,27 @@
*/
#endregion
using System.Collections.Generic;
using System.Collections.Frozen;
using System.Collections.Immutable;
using System.Linq;
namespace OpenRA
{
public sealed class HotkeyDefinition
{
public const string ContextFluentPrefix = "hotkey-context";
public readonly string Name;
public readonly Hotkey Default = Hotkey.Invalid;
[FluentReference]
public readonly string Description = "";
public readonly HashSet<string> Types = new();
public readonly HashSet<string> Contexts = new();
public readonly FrozenSet<string> Types = FrozenSet<string>.Empty;
[FluentReference]
public readonly FrozenSet<string> Contexts = FrozenSet<string>.Empty;
public readonly bool Readonly = false;
public bool HasDuplicates { get; internal set; }
@@ -31,29 +40,27 @@ namespace OpenRA
if (!string.IsNullOrEmpty(node.Value))
Default = FieldLoader.GetValue<Hotkey>("value", node.Value);
var descriptionNode = node.Nodes.FirstOrDefault(n => n.Key == "Description");
if (descriptionNode != null)
Description = descriptionNode.Value.Value;
var nodeDict = node.ToDictionary();
var typesNode = node.Nodes.FirstOrDefault(n => n.Key == "Types");
if (typesNode != null)
Types = FieldLoader.GetValue<HashSet<string>>("Types", typesNode.Value.Value);
if (nodeDict.TryGetValue("Description", out var descriptionYaml))
Description = descriptionYaml.Value;
var contextsNode = node.Nodes.FirstOrDefault(n => n.Key == "Contexts");
if (contextsNode != null)
Contexts = FieldLoader.GetValue<HashSet<string>>("Contexts", contextsNode.Value.Value);
if (nodeDict.TryGetValue("Types", out var typesYaml))
Types = FieldLoader.GetValue<FrozenSet<string>>("Types", typesYaml.Value);
var platformNode = node.Nodes.FirstOrDefault(n => n.Key == "Platform");
if (platformNode != null)
if (nodeDict.TryGetValue("Contexts", out var contextYaml))
Contexts = FieldLoader.GetValue<ImmutableArray<string>>("Contexts", contextYaml.Value)
.Select(c => ContextFluentPrefix + "." + c).ToFrozenSet();
if (nodeDict.TryGetValue("Platform", out var platformYaml))
{
var platformOverride = platformNode.Value.Nodes.FirstOrDefault(n => n.Key == Platform.CurrentPlatform.ToString());
var platformOverride = platformYaml.NodeWithKeyOrDefault(Platform.CurrentPlatform.ToString());
if (platformOverride != null)
Default = FieldLoader.GetValue<Hotkey>("value", platformOverride.Value.Value);
}
var readonlyNode = node.Nodes.FirstOrDefault(n => n.Key == "Readonly");
if (readonlyNode != null)
Readonly = FieldLoader.GetValue<bool>("Readonly", readonlyNode.Value.Value);
if (nodeDict.TryGetValue("Readonly", out var readonlyYaml))
Readonly = FieldLoader.GetValue<bool>("Readonly", readonlyYaml.Value);
}
}
}

View File

@@ -17,13 +17,16 @@ namespace OpenRA
{
public sealed class HotkeyManager
{
readonly Dictionary<string, Hotkey> settings;
readonly Dictionary<string, HotkeyDefinition> definitions = new();
readonly Dictionary<string, Hotkey> keys = new();
[YamlNode("Keys", shared: true)]
sealed class HotkeySettings : SettingsModule { }
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);
foreach (var kd in keyDefinitions)
@@ -33,11 +36,9 @@ namespace OpenRA
keys[kd.Key] = definition.Default;
}
foreach (var kv in settings)
{
if (definitions.TryGetValue(kv.Key, out var definition) && !definition.Readonly)
keys[kv.Key] = kv.Value;
}
foreach (var node in hotkeySettings.Yaml.Nodes)
if (definitions.TryGetValue(node.Key, out var definition) && !definition.Readonly)
keys[node.Key] = FieldLoader.GetValue<Hotkey>(node.Key, node.Value.Value);
foreach (var hd in definitions)
hd.Value.HasDuplicates = GetFirstDuplicate(hd.Value, this[hd.Value.Name].GetValue()) != null;
@@ -68,10 +69,9 @@ namespace OpenRA
return;
keys[name] = value;
hotkeySettings.Yaml.Nodes.RemoveAll(n => n.Key == name);
if (value != definition.Default)
settings[name] = value;
else
settings.Remove(name);
hotkeySettings.Yaml.Nodes.Add(new MiniYamlNodeBuilder(name, FieldSaver.FormatValue(value)));
var hadDuplicates = definition.HasDuplicates;
definition.HasDuplicates = GetFirstDuplicate(definition, this[definition.Name].GetValue()) != null;
@@ -108,5 +108,10 @@ namespace OpenRA
public HotkeyReference this[string name] => new(GetHotkeyReference(name));
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 canReportProgress = total > 0;
#if NET5_0_OR_GREATER
using (var contentStream = await response.Content.ReadAsStreamAsync(token))
#else
using (var contentStream = await response.Content.ReadAsStreamAsync())
#endif
await using (var contentStream = await response.Content.ReadAsStreamAsync(token))
{
var totalRead = 0L;
var buffer = new byte[8192];

View File

@@ -27,25 +27,21 @@ namespace OpenRA
public static bool TryParse(string s, out Hotkey result)
{
result = Invalid;
if (string.IsNullOrWhiteSpace(s))
if (s == null)
return false;
var parts = s.Split(' ');
Span<Range> ranges = stackalloc Range[2];
var span = s.AsSpan();
var count = span.Split(ranges, ' ');
if (count == 0)
return false;
if (!Enum<Keycode>.TryParse(parts[0], true, out var key))
{
if (!int.TryParse(parts[0], out var c))
return false;
key = (Keycode)c;
}
if (!Enum.TryParse(span[ranges[0]], true, out Keycode key))
return false;
var mods = Modifiers.None;
if (parts.Length >= 2)
{
var modString = s[s.IndexOf(' ')..];
if (!Enum<Modifiers>.TryParse(modString, true, out mods))
return false;
}
if (count == 2 && !Enum.TryParse(span[ranges[1]], true, out mods))
return false;
result = new Hotkey(key, mods);
return true;
@@ -91,16 +87,16 @@ namespace OpenRA
var ret = KeycodeExts.DisplayString(Key);
if (Modifiers.HasModifier(Modifiers.Shift))
ret = "Shift + " + ret;
ret = $"{ModifiersExts.DisplayString(Modifiers.Shift)} + {ret}";
if (Modifiers.HasModifier(Modifiers.Alt))
ret = "Alt + " + ret;
ret = $"{ModifiersExts.DisplayString(Modifiers.Alt)} + {ret}";
if (Modifiers.HasModifier(Modifiers.Ctrl))
ret = "Ctrl + " + ret;
ret = $"{ModifiersExts.DisplayString(Modifiers.Ctrl)} + {ret}";
if (Modifiers.HasModifier(Modifiers.Meta))
ret = (Platform.CurrentPlatform == PlatformType.OSX ? "Cmd + " : "Meta + ") + ret;
ret = $"{ModifiersExts.DisplayString(Modifiers.Meta)} + {ret}";
return ret;
}

View File

@@ -10,6 +10,7 @@
#endregion
using System;
using System.Collections.Generic;
namespace OpenRA
{
@@ -22,25 +23,7 @@ namespace OpenRA
}
public enum MouseInputEvent { Down, Move, Up, Scroll }
public struct MouseInput
{
public MouseInputEvent Event;
public MouseButton Button;
public int2 Location;
public int2 Delta;
public Modifiers Modifiers;
public int MultiTapCount;
public MouseInput(MouseInputEvent ev, MouseButton button, int2 location, int2 delta, Modifiers mods, int multiTapCount)
{
Event = ev;
Button = button;
Location = location;
Delta = delta;
Modifiers = mods;
MultiTapCount = multiTapCount;
}
}
public record struct MouseInput(MouseInputEvent Event, MouseButton Button, int2 Location, int2 Delta, Modifiers Modifiers, int MultiTapCount);
[Flags]
public enum MouseButton
@@ -61,6 +44,33 @@ namespace OpenRA
Meta = 8,
}
public static class ModifiersExts
{
[FluentReference]
const string Cmd = "keycode-modifier.cmd";
[FluentReference(Traits.LintDictionaryReference.Values)]
public static readonly IReadOnlyDictionary<Modifiers, string> ModifierFluentKeys = new Dictionary<Modifiers, string>()
{
{ Modifiers.None, "keycode-modifier.none" },
{ Modifiers.Shift, "keycode-modifier.shift" },
{ Modifiers.Alt, "keycode-modifier.alt" },
{ Modifiers.Ctrl, "keycode-modifier.ctrl" },
{ Modifiers.Meta, "keycode-modifier.meta" },
};
public static string DisplayString(Modifiers m)
{
if (m == Modifiers.Meta && Platform.CurrentPlatform == PlatformType.OSX)
return FluentProvider.GetMessage(Cmd);
if (!ModifierFluentKeys.TryGetValue(m, out var fluentKey))
return m.ToString();
return FluentProvider.GetMessage(fluentKey);
}
}
public enum KeyInputEvent { Down, Up }
public struct KeyInput
{

View File

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

View File

@@ -259,254 +259,255 @@ namespace OpenRA
public static class KeycodeExts
{
static readonly Dictionary<Keycode, string> KeyNames = new()
[FluentReference(Traits.LintDictionaryReference.Values)]
public static readonly IReadOnlyDictionary<Keycode, string> KeycodeFluentKeys = new Dictionary<Keycode, string>()
{
{ Keycode.UNKNOWN, "Undefined" },
{ Keycode.RETURN, "Return" },
{ Keycode.ESCAPE, "Escape" },
{ Keycode.BACKSPACE, "Backspace" },
{ Keycode.TAB, "Tab" },
{ Keycode.SPACE, "Space" },
{ Keycode.EXCLAIM, "!" },
{ Keycode.QUOTEDBL, "\"" },
{ Keycode.HASH, "#" },
{ Keycode.PERCENT, "%" },
{ Keycode.DOLLAR, "$" },
{ Keycode.AMPERSAND, "&" },
{ Keycode.QUOTE, "'" },
{ Keycode.LEFTPAREN, "(" },
{ Keycode.RIGHTPAREN, ")" },
{ Keycode.ASTERISK, "*" },
{ Keycode.PLUS, "+" },
{ Keycode.COMMA, "," },
{ Keycode.MINUS, "-" },
{ Keycode.PERIOD, "." },
{ Keycode.SLASH, "/" },
{ Keycode.NUMBER_0, "0" },
{ Keycode.NUMBER_1, "1" },
{ Keycode.NUMBER_2, "2" },
{ Keycode.NUMBER_3, "3" },
{ Keycode.NUMBER_4, "4" },
{ Keycode.NUMBER_5, "5" },
{ Keycode.NUMBER_6, "6" },
{ Keycode.NUMBER_7, "7" },
{ Keycode.NUMBER_8, "8" },
{ Keycode.NUMBER_9, "9" },
{ Keycode.COLON, ":" },
{ Keycode.SEMICOLON, ";" },
{ Keycode.LESS, "<" },
{ Keycode.EQUALS, "=" },
{ Keycode.GREATER, ">" },
{ Keycode.QUESTION, "?" },
{ Keycode.AT, "@" },
{ Keycode.LEFTBRACKET, "[" },
{ Keycode.BACKSLASH, "\\" },
{ Keycode.RIGHTBRACKET, "]" },
{ Keycode.CARET, "^" },
{ Keycode.UNDERSCORE, "_" },
{ Keycode.BACKQUOTE, "`" },
{ Keycode.A, "A" },
{ Keycode.B, "B" },
{ Keycode.C, "C" },
{ Keycode.D, "D" },
{ Keycode.E, "E" },
{ Keycode.F, "F" },
{ Keycode.G, "G" },
{ Keycode.H, "H" },
{ Keycode.I, "I" },
{ Keycode.J, "J" },
{ Keycode.K, "K" },
{ Keycode.L, "L" },
{ Keycode.M, "M" },
{ Keycode.N, "N" },
{ Keycode.O, "O" },
{ Keycode.P, "P" },
{ Keycode.Q, "Q" },
{ Keycode.R, "R" },
{ Keycode.S, "S" },
{ Keycode.T, "T" },
{ Keycode.U, "U" },
{ Keycode.V, "V" },
{ Keycode.W, "W" },
{ Keycode.X, "X" },
{ Keycode.Y, "Y" },
{ Keycode.Z, "Z" },
{ Keycode.CAPSLOCK, "CapsLock" },
{ Keycode.F1, "F1" },
{ Keycode.F2, "F2" },
{ Keycode.F3, "F3" },
{ Keycode.F4, "F4" },
{ Keycode.F5, "F5" },
{ Keycode.F6, "F6" },
{ Keycode.F7, "F7" },
{ Keycode.F8, "F8" },
{ Keycode.F9, "F9" },
{ Keycode.F10, "F10" },
{ Keycode.F11, "F11" },
{ Keycode.F12, "F12" },
{ Keycode.PRINTSCREEN, "PrintScreen" },
{ Keycode.SCROLLLOCK, "ScrollLock" },
{ Keycode.PAUSE, "Pause" },
{ Keycode.INSERT, "Insert" },
{ Keycode.HOME, "Home" },
{ Keycode.PAGEUP, "PageUp" },
{ Keycode.DELETE, "Delete" },
{ Keycode.END, "End" },
{ Keycode.PAGEDOWN, "PageDown" },
{ Keycode.RIGHT, "Right" },
{ Keycode.LEFT, "Left" },
{ Keycode.DOWN, "Down" },
{ Keycode.UP, "Up" },
{ Keycode.NUMLOCKCLEAR, "Numlock" },
{ Keycode.KP_DIVIDE, "Keypad /" },
{ Keycode.KP_MULTIPLY, "Keypad *" },
{ Keycode.KP_MINUS, "Keypad -" },
{ Keycode.KP_PLUS, "Keypad +" },
{ Keycode.KP_ENTER, "Keypad Enter" },
{ Keycode.KP_1, "Keypad 1" },
{ Keycode.KP_2, "Keypad 2" },
{ Keycode.KP_3, "Keypad 3" },
{ Keycode.KP_4, "Keypad 4" },
{ Keycode.KP_5, "Keypad 5" },
{ Keycode.KP_6, "Keypad 6" },
{ Keycode.KP_7, "Keypad 7" },
{ Keycode.KP_8, "Keypad 8" },
{ Keycode.KP_9, "Keypad 9" },
{ Keycode.KP_0, "Keypad 0" },
{ Keycode.KP_PERIOD, "Keypad ." },
{ Keycode.APPLICATION, "Application" },
{ Keycode.POWER, "Power" },
{ Keycode.KP_EQUALS, "Keypad =" },
{ Keycode.F13, "F13" },
{ Keycode.F14, "F14" },
{ Keycode.F15, "F15" },
{ Keycode.F16, "F16" },
{ Keycode.F17, "F17" },
{ Keycode.F18, "F18" },
{ Keycode.F19, "F19" },
{ Keycode.F20, "F20" },
{ Keycode.F21, "F21" },
{ Keycode.F22, "F22" },
{ Keycode.F23, "F23" },
{ Keycode.F24, "F24" },
{ Keycode.EXECUTE, "Execute" },
{ Keycode.HELP, "Help" },
{ Keycode.MENU, "Menu" },
{ Keycode.SELECT, "Select" },
{ Keycode.STOP, "Stop" },
{ Keycode.AGAIN, "Again" },
{ Keycode.UNDO, "Undo" },
{ Keycode.CUT, "Cut" },
{ Keycode.COPY, "Copy" },
{ Keycode.PASTE, "Paste" },
{ Keycode.FIND, "Find" },
{ Keycode.MUTE, "Mute" },
{ Keycode.VOLUMEUP, "VolumeUp" },
{ Keycode.VOLUMEDOWN, "VolumeDown" },
{ Keycode.KP_COMMA, "Keypad }," },
{ Keycode.KP_EQUALSAS400, "Keypad, (AS400)" },
{ Keycode.ALTERASE, "AltErase" },
{ Keycode.SYSREQ, "SysReq" },
{ Keycode.CANCEL, "Cancel" },
{ Keycode.CLEAR, "Clear" },
{ Keycode.PRIOR, "Prior" },
{ Keycode.RETURN2, "Return" },
{ Keycode.SEPARATOR, "Separator" },
{ Keycode.OUT, "Out" },
{ Keycode.OPER, "Oper" },
{ Keycode.CLEARAGAIN, "Clear / Again" },
{ Keycode.CRSEL, "CrSel" },
{ Keycode.EXSEL, "ExSel" },
{ Keycode.KP_00, "Keypad 00" },
{ Keycode.KP_000, "Keypad 000" },
{ Keycode.THOUSANDSSEPARATOR, "ThousandsSeparator" },
{ Keycode.DECIMALSEPARATOR, "DecimalSeparator" },
{ Keycode.CURRENCYUNIT, "CurrencyUnit" },
{ Keycode.CURRENCYSUBUNIT, "CurrencySubUnit" },
{ Keycode.KP_LEFTPAREN, "Keypad (" },
{ Keycode.KP_RIGHTPAREN, "Keypad )" },
{ Keycode.KP_LEFTBRACE, "Keypad {" },
{ Keycode.KP_RIGHTBRACE, "Keypad }" },
{ Keycode.KP_TAB, "Keypad Tab" },
{ Keycode.KP_BACKSPACE, "Keypad Backspace" },
{ Keycode.KP_A, "Keypad A" },
{ Keycode.KP_B, "Keypad B" },
{ Keycode.KP_C, "Keypad C" },
{ Keycode.KP_D, "Keypad D" },
{ Keycode.KP_E, "Keypad E" },
{ Keycode.KP_F, "Keypad F" },
{ Keycode.KP_XOR, "Keypad XOR" },
{ Keycode.KP_POWER, "Keypad ^" },
{ Keycode.KP_PERCENT, "Keypad %" },
{ Keycode.KP_LESS, "Keypad <" },
{ Keycode.KP_GREATER, "Keypad >" },
{ Keycode.KP_AMPERSAND, "Keypad &" },
{ Keycode.KP_DBLAMPERSAND, "Keypad &&" },
{ Keycode.KP_VERTICALBAR, "Keypad |" },
{ Keycode.KP_DBLVERTICALBAR, "Keypad ||" },
{ Keycode.KP_COLON, "Keypad :" },
{ Keycode.KP_HASH, "Keypad #" },
{ Keycode.KP_SPACE, "Keypad Space" },
{ Keycode.KP_AT, "Keypad @" },
{ Keycode.KP_EXCLAM, "Keypad !" },
{ Keycode.KP_MEMSTORE, "Keypad MemStore" },
{ Keycode.KP_MEMRECALL, "Keypad MemRecall" },
{ Keycode.KP_MEMCLEAR, "Keypad MemClear" },
{ Keycode.KP_MEMADD, "Keypad MemAdd" },
{ Keycode.KP_MEMSUBTRACT, "Keypad MemSubtract" },
{ Keycode.KP_MEMMULTIPLY, "Keypad MemMultiply" },
{ Keycode.KP_MEMDIVIDE, "Keypad MemDivide" },
{ Keycode.KP_PLUSMINUS, "Keypad +/-" },
{ Keycode.KP_CLEAR, "Keypad Clear" },
{ Keycode.KP_CLEARENTRY, "Keypad ClearEntry" },
{ Keycode.KP_BINARY, "Keypad Binary" },
{ Keycode.KP_OCTAL, "Keypad Octal" },
{ Keycode.KP_DECIMAL, "Keypad Decimal" },
{ Keycode.KP_HEXADECIMAL, "Keypad Hexadecimal" },
{ Keycode.LCTRL, "Left Ctrl" },
{ Keycode.LSHIFT, "Left Shift" },
{ Keycode.LALT, "Left Alt" },
{ Keycode.LGUI, "Left GUI" },
{ Keycode.RCTRL, "Right Ctrl" },
{ Keycode.RSHIFT, "Right Shift" },
{ Keycode.RALT, "Right Alt" },
{ Keycode.RGUI, "Right GUI" },
{ Keycode.MODE, "ModeSwitch" },
{ Keycode.AUDIONEXT, "AudioNext" },
{ Keycode.AUDIOPREV, "AudioPrev" },
{ Keycode.AUDIOSTOP, "AudioStop" },
{ Keycode.AUDIOPLAY, "AudioPlay" },
{ Keycode.AUDIOMUTE, "AudioMute" },
{ Keycode.MEDIASELECT, "MediaSelect" },
{ Keycode.WWW, "WWW" },
{ Keycode.MAIL, "Mail" },
{ Keycode.CALCULATOR, "Calculator" },
{ Keycode.COMPUTER, "Computer" },
{ Keycode.AC_SEARCH, "AC Search" },
{ Keycode.AC_HOME, "AC Home" },
{ Keycode.AC_BACK, "AC Back" },
{ Keycode.AC_FORWARD, "AC Forward" },
{ Keycode.AC_STOP, "AC Stop" },
{ Keycode.AC_REFRESH, "AC Refresh" },
{ Keycode.AC_BOOKMARKS, "AC Bookmarks" },
{ Keycode.BRIGHTNESSDOWN, "BrightnessDown" },
{ Keycode.BRIGHTNESSUP, "BrightnessUp" },
{ Keycode.DISPLAYSWITCH, "DisplaySwitch" },
{ Keycode.KBDILLUMTOGGLE, "KBDIllumToggle" },
{ Keycode.KBDILLUMDOWN, "KBDIllumDown" },
{ Keycode.KBDILLUMUP, "KBDIllumUp" },
{ Keycode.EJECT, "Eject" },
{ Keycode.SLEEP, "Sleep" },
{ Keycode.MOUSE4, "Mouse 4" },
{ Keycode.MOUSE5, "Mouse 5" },
{ Keycode.UNKNOWN, "keycode.unknown" },
{ Keycode.RETURN, "keycode.return" },
{ Keycode.ESCAPE, "keycode.escape" },
{ Keycode.BACKSPACE, "keycode.backspace" },
{ Keycode.TAB, "keycode.tab" },
{ Keycode.SPACE, "keycode.space" },
{ Keycode.EXCLAIM, "keycode.exclaim" },
{ Keycode.QUOTEDBL, "keycode.quotedbl" },
{ Keycode.HASH, "keycode.hash" },
{ Keycode.PERCENT, "keycode.percent" },
{ Keycode.DOLLAR, "keycode.dollar" },
{ Keycode.AMPERSAND, "keycode.ampersand" },
{ Keycode.QUOTE, "keycode.quote" },
{ Keycode.LEFTPAREN, "keycode.leftparen" },
{ Keycode.RIGHTPAREN, "keycode.rightparen" },
{ Keycode.ASTERISK, "keycode.asterisk" },
{ Keycode.PLUS, "keycode.plus" },
{ Keycode.COMMA, "keycode.comma" },
{ Keycode.MINUS, "keycode.minus" },
{ Keycode.PERIOD, "keycode.period" },
{ Keycode.SLASH, "keycode.slash" },
{ Keycode.NUMBER_0, "keycode.number_0" },
{ Keycode.NUMBER_1, "keycode.number_1" },
{ Keycode.NUMBER_2, "keycode.number_2" },
{ Keycode.NUMBER_3, "keycode.number_3" },
{ Keycode.NUMBER_4, "keycode.number_4" },
{ Keycode.NUMBER_5, "keycode.number_5" },
{ Keycode.NUMBER_6, "keycode.number_6" },
{ Keycode.NUMBER_7, "keycode.number_7" },
{ Keycode.NUMBER_8, "keycode.number_8" },
{ Keycode.NUMBER_9, "keycode.number_9" },
{ Keycode.COLON, "keycode.colon" },
{ Keycode.SEMICOLON, "keycode.semicolon" },
{ Keycode.LESS, "keycode.less" },
{ Keycode.EQUALS, "keycode.equals" },
{ Keycode.GREATER, "keycode.greater" },
{ Keycode.QUESTION, "keycode.question" },
{ Keycode.AT, "keycode.at" },
{ Keycode.LEFTBRACKET, "keycode.leftbracket" },
{ Keycode.BACKSLASH, "keycode.backslash" },
{ Keycode.RIGHTBRACKET, "keycode.rightbracket" },
{ Keycode.CARET, "keycode.caret" },
{ Keycode.UNDERSCORE, "keycode.underscore" },
{ Keycode.BACKQUOTE, "keycode.backquote" },
{ Keycode.A, "keycode.a" },
{ Keycode.B, "keycode.b" },
{ Keycode.C, "keycode.c" },
{ Keycode.D, "keycode.d" },
{ Keycode.E, "keycode.e" },
{ Keycode.F, "keycode.f" },
{ Keycode.G, "keycode.g" },
{ Keycode.H, "keycode.h" },
{ Keycode.I, "keycode.i" },
{ Keycode.J, "keycode.j" },
{ Keycode.K, "keycode.k" },
{ Keycode.L, "keycode.l" },
{ Keycode.M, "keycode.m" },
{ Keycode.N, "keycode.n" },
{ Keycode.O, "keycode.o" },
{ Keycode.P, "keycode.p" },
{ Keycode.Q, "keycode.q" },
{ Keycode.R, "keycode.r" },
{ Keycode.S, "keycode.s" },
{ Keycode.T, "keycode.t" },
{ Keycode.U, "keycode.u" },
{ Keycode.V, "keycode.v" },
{ Keycode.W, "keycode.w" },
{ Keycode.X, "keycode.x" },
{ Keycode.Y, "keycode.y" },
{ Keycode.Z, "keycode.z" },
{ Keycode.CAPSLOCK, "keycode.capslock" },
{ Keycode.F1, "keycode.f1" },
{ Keycode.F2, "keycode.f2" },
{ Keycode.F3, "keycode.f3" },
{ Keycode.F4, "keycode.f4" },
{ Keycode.F5, "keycode.f5" },
{ Keycode.F6, "keycode.f6" },
{ Keycode.F7, "keycode.f7" },
{ Keycode.F8, "keycode.f8" },
{ Keycode.F9, "keycode.f9" },
{ Keycode.F10, "keycode.f10" },
{ Keycode.F11, "keycode.f11" },
{ Keycode.F12, "keycode.f12" },
{ Keycode.PRINTSCREEN, "keycode.printscreen" },
{ Keycode.SCROLLLOCK, "keycode.scrolllock" },
{ Keycode.PAUSE, "keycode.pause" },
{ Keycode.INSERT, "keycode.insert" },
{ Keycode.HOME, "keycode.home" },
{ Keycode.PAGEUP, "keycode.pageup" },
{ Keycode.DELETE, "keycode.delete" },
{ Keycode.END, "keycode.end" },
{ Keycode.PAGEDOWN, "keycode.pagedown" },
{ Keycode.RIGHT, "keycode.right" },
{ Keycode.LEFT, "keycode.left" },
{ Keycode.DOWN, "keycode.down" },
{ Keycode.UP, "keycode.up" },
{ Keycode.NUMLOCKCLEAR, "keycode.numlockclear" },
{ Keycode.KP_DIVIDE, "keycode.kp_divide" },
{ Keycode.KP_MULTIPLY, "keycode.kp_multiply" },
{ Keycode.KP_MINUS, "keycode.kp_minus" },
{ Keycode.KP_PLUS, "keycode.kp_plus" },
{ Keycode.KP_ENTER, "keycode.kp_enter" },
{ Keycode.KP_1, "keycode.kp_1" },
{ Keycode.KP_2, "keycode.kp_2" },
{ Keycode.KP_3, "keycode.kp_3" },
{ Keycode.KP_4, "keycode.kp_4" },
{ Keycode.KP_5, "keycode.kp_5" },
{ Keycode.KP_6, "keycode.kp_6" },
{ Keycode.KP_7, "keycode.kp_7" },
{ Keycode.KP_8, "keycode.kp_8" },
{ Keycode.KP_9, "keycode.kp_9" },
{ Keycode.KP_0, "keycode.kp_0" },
{ Keycode.KP_PERIOD, "keycode.kp_period" },
{ Keycode.APPLICATION, "keycode.application" },
{ Keycode.POWER, "keycode.power" },
{ Keycode.KP_EQUALS, "keycode.kp_equals" },
{ Keycode.F13, "keycode.f13" },
{ Keycode.F14, "keycode.f14" },
{ Keycode.F15, "keycode.f15" },
{ Keycode.F16, "keycode.f16" },
{ Keycode.F17, "keycode.f17" },
{ Keycode.F18, "keycode.f18" },
{ Keycode.F19, "keycode.f19" },
{ Keycode.F20, "keycode.f20" },
{ Keycode.F21, "keycode.f21" },
{ Keycode.F22, "keycode.f22" },
{ Keycode.F23, "keycode.f23" },
{ Keycode.F24, "keycode.f24" },
{ Keycode.EXECUTE, "keycode.execute" },
{ Keycode.HELP, "keycode.help" },
{ Keycode.MENU, "keycode.menu" },
{ Keycode.SELECT, "keycode.select" },
{ Keycode.STOP, "keycode.stop" },
{ Keycode.AGAIN, "keycode.again" },
{ Keycode.UNDO, "keycode.undo" },
{ Keycode.CUT, "keycode.cut" },
{ Keycode.COPY, "keycode.copy" },
{ Keycode.PASTE, "keycode.paste" },
{ Keycode.FIND, "keycode.find" },
{ Keycode.MUTE, "keycode.mute" },
{ Keycode.VOLUMEUP, "keycode.volumeup" },
{ Keycode.VOLUMEDOWN, "keycode.volumedown" },
{ Keycode.KP_COMMA, "keycode.kp_comma" },
{ Keycode.KP_EQUALSAS400, "keycode.kp_equalsas400" },
{ Keycode.ALTERASE, "keycode.alterase" },
{ Keycode.SYSREQ, "keycode.sysreq" },
{ Keycode.CANCEL, "keycode.cancel" },
{ Keycode.CLEAR, "keycode.clear" },
{ Keycode.PRIOR, "keycode.prior" },
{ Keycode.RETURN2, "keycode.return2" },
{ Keycode.SEPARATOR, "keycode.separator" },
{ Keycode.OUT, "keycode.out" },
{ Keycode.OPER, "keycode.oper" },
{ Keycode.CLEARAGAIN, "keycode.clearagain" },
{ Keycode.CRSEL, "keycode.crsel" },
{ Keycode.EXSEL, "keycode.exsel" },
{ Keycode.KP_00, "keycode.kp_00" },
{ Keycode.KP_000, "keycode.kp_000" },
{ Keycode.THOUSANDSSEPARATOR, "keycode.thousandsseparator" },
{ Keycode.DECIMALSEPARATOR, "keycode.decimalseparator" },
{ Keycode.CURRENCYUNIT, "keycode.currencyunit" },
{ Keycode.CURRENCYSUBUNIT, "keycode.currencysubunit" },
{ Keycode.KP_LEFTPAREN, "keycode.kp_leftparen" },
{ Keycode.KP_RIGHTPAREN, "keycode.kp_rightparen" },
{ Keycode.KP_LEFTBRACE, "keycode.kp_leftbrace" },
{ Keycode.KP_RIGHTBRACE, "keycode.kp_rightbrace" },
{ Keycode.KP_TAB, "keycode.kp_tab" },
{ Keycode.KP_BACKSPACE, "keycode.kp_backspace" },
{ Keycode.KP_A, "keycode.kp_a" },
{ Keycode.KP_B, "keycode.kp_b" },
{ Keycode.KP_C, "keycode.kp_c" },
{ Keycode.KP_D, "keycode.kp_d" },
{ Keycode.KP_E, "keycode.kp_e" },
{ Keycode.KP_F, "keycode.kp_f" },
{ Keycode.KP_XOR, "keycode.kp_xor" },
{ Keycode.KP_POWER, "keycode.kp_power" },
{ Keycode.KP_PERCENT, "keycode.kp_percent" },
{ Keycode.KP_LESS, "keycode.kp_less" },
{ Keycode.KP_GREATER, "keycode.kp_greater" },
{ Keycode.KP_AMPERSAND, "keycode.kp_ampersand" },
{ Keycode.KP_DBLAMPERSAND, "keycode.kp_dblampersand" },
{ Keycode.KP_VERTICALBAR, "keycode.kp_verticalbar" },
{ Keycode.KP_DBLVERTICALBAR, "keycode.kp_dblverticalbar" },
{ Keycode.KP_COLON, "keycode.kp_colon" },
{ Keycode.KP_HASH, "keycode.kp_hash" },
{ Keycode.KP_SPACE, "keycode.kp_space" },
{ Keycode.KP_AT, "keycode.kp_at" },
{ Keycode.KP_EXCLAM, "keycode.kp_exclam" },
{ Keycode.KP_MEMSTORE, "keycode.kp_memstore" },
{ Keycode.KP_MEMRECALL, "keycode.kp_memrecall" },
{ Keycode.KP_MEMCLEAR, "keycode.kp_memclear" },
{ Keycode.KP_MEMADD, "keycode.kp_memadd" },
{ Keycode.KP_MEMSUBTRACT, "keycode.kp_memsubtract" },
{ Keycode.KP_MEMMULTIPLY, "keycode.kp_memmultiply" },
{ Keycode.KP_MEMDIVIDE, "keycode.kp_memdivide" },
{ Keycode.KP_PLUSMINUS, "keycode.kp_plusminus" },
{ Keycode.KP_CLEAR, "keycode.kp_clear" },
{ Keycode.KP_CLEARENTRY, "keycode.kp_clearentry" },
{ Keycode.KP_BINARY, "keycode.kp_binary" },
{ Keycode.KP_OCTAL, "keycode.kp_octal" },
{ Keycode.KP_DECIMAL, "keycode.kp_decimal" },
{ Keycode.KP_HEXADECIMAL, "keycode.kp_hexadecimal" },
{ Keycode.LCTRL, "keycode.lctrl" },
{ Keycode.LSHIFT, "keycode.lshift" },
{ Keycode.LALT, "keycode.lalt" },
{ Keycode.LGUI, "keycode.lgui" },
{ Keycode.RCTRL, "keycode.rctrl" },
{ Keycode.RSHIFT, "keycode.rshift" },
{ Keycode.RALT, "keycode.ralt" },
{ Keycode.RGUI, "keycode.rgui" },
{ Keycode.MODE, "keycode.mode" },
{ Keycode.AUDIONEXT, "keycode.audionext" },
{ Keycode.AUDIOPREV, "keycode.audioprev" },
{ Keycode.AUDIOSTOP, "keycode.audiostop" },
{ Keycode.AUDIOPLAY, "keycode.audioplay" },
{ Keycode.AUDIOMUTE, "keycode.audiomute" },
{ Keycode.MEDIASELECT, "keycode.mediaselect" },
{ Keycode.WWW, "keycode.www" },
{ Keycode.MAIL, "keycode.mail" },
{ Keycode.CALCULATOR, "keycode.calculator" },
{ Keycode.COMPUTER, "keycode.computer" },
{ Keycode.AC_SEARCH, "keycode.ac_search" },
{ Keycode.AC_HOME, "keycode.ac_home" },
{ Keycode.AC_BACK, "keycode.ac_back" },
{ Keycode.AC_FORWARD, "keycode.ac_forward" },
{ Keycode.AC_STOP, "keycode.ac_stop" },
{ Keycode.AC_REFRESH, "keycode.ac_refresh" },
{ Keycode.AC_BOOKMARKS, "keycode.ac_bookmarks" },
{ Keycode.BRIGHTNESSDOWN, "keycode.brightnessdown" },
{ Keycode.BRIGHTNESSUP, "keycode.brightnessup" },
{ Keycode.DISPLAYSWITCH, "keycode.displayswitch" },
{ Keycode.KBDILLUMTOGGLE, "keycode.kbdillumtoggle" },
{ Keycode.KBDILLUMDOWN, "keycode.kbdillumdown" },
{ Keycode.KBDILLUMUP, "keycode.kbdillumup" },
{ Keycode.EJECT, "keycode.eject" },
{ Keycode.SLEEP, "keycode.sleep" },
{ Keycode.MOUSE4, "keycode.mouse4" },
{ Keycode.MOUSE5, "keycode.mouse5" },
};
public static string DisplayString(Keycode k)
{
if (!KeyNames.TryGetValue(k, out var ret))
if (!KeycodeFluentKeys.TryGetValue(k, out var fluentKey))
return k.ToString();
return ret;
return FluentProvider.GetMessage(fluentKey);
}
}
}

View File

@@ -84,10 +84,11 @@ namespace OpenRA
{
var client = HttpClientFactory.Create();
var httpResponseMessage = await client.GetAsync(playerDatabase.Profile + Fingerprint);
var url = playerDatabase.Profile + Fingerprint;
var httpResponseMessage = await client.GetAsync(url);
var result = await httpResponseMessage.Content.ReadAsStreamAsync();
var yaml = MiniYaml.FromStream(result).First();
var yaml = MiniYaml.FromStream(result, url).First();
if (yaml.Key == "Player")
{
innerData = FieldLoader.Load<PlayerProfile>(yaml.Value);

View File

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

View File

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

View File

@@ -154,7 +154,7 @@ namespace OpenRA
public virtual void Initialize(MiniYaml yaml)
{
Initialize((T)FieldLoader.GetValue(nameof(value), typeof(T), yaml.Value));
Initialize(FieldLoader.GetValue<T>(nameof(value), yaml.Value));
}
public virtual void Initialize(T value)
@@ -175,8 +175,7 @@ namespace OpenRA
protected CompositeActorInit(TraitInfo info)
: base(info.InstanceName) { }
protected CompositeActorInit()
: base() { }
protected CompositeActorInit() { }
public virtual void Initialize(MiniYaml yaml)
{
@@ -217,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

View File

@@ -14,7 +14,7 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.CompilerServices;
using OpenRA.Primitives;
using OpenRA.Traits;
@@ -30,15 +30,15 @@ namespace OpenRA
internal TypeDictionary InitDict => initDict.Value;
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;
initDict = Exts.Lazy(() =>
{
var dict = new TypeDictionary();
foreach (var i in inits)
foreach (var i in inits.Nodes)
{
var init = LoadInit(i.Key, i.Value);
if (init is ISingleInstanceInit && dict.Contains(init.GetType()))
@@ -54,13 +54,8 @@ namespace OpenRA
public ActorReference(string type, TypeDictionary inits)
{
Type = type;
initDict = new Lazy<TypeDictionary>(() =>
{
var dict = new TypeDictionary();
foreach (var i in inits)
dict.Add(i);
return dict;
});
var initsClone = new TypeDictionary(inits);
initDict = Exts.Lazy(() => initsClone);
}
static ActorInit LoadInit(string initName, MiniYaml initYaml)
@@ -70,21 +65,21 @@ namespace OpenRA
if (type == null)
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)
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)
throw new InvalidDataException($"{initInstance[0]}Init does not define a yaml-assignable type.");
loader.Invoke(init, new[] { initYaml });
loader.Invoke(init, [initYaml]);
return init;
}
public MiniYaml Save(Func<ActorInit, bool> initFilter = null)
{
var ret = new MiniYaml(Type);
var nodes = new List<MiniYamlNode>();
foreach (var o in initDict.Value)
{
if (o is not ActorInit init || o is ISuppressInitExport)
@@ -98,10 +93,10 @@ namespace OpenRA
if (!string.IsNullOrEmpty(init.InstanceName))
initName += ActorInfo.TraitInstanceSeparator + init.InstanceName;
ret.Nodes.Add(new MiniYamlNode(initName, init.Save()));
nodes.Add(new MiniYamlNode(initName, init.Save()));
}
return ret;
return new MiniYaml(Type, nodes);
}
public IEnumerator<object> GetEnumerator() { return initDict.Value.GetEnumerator(); }
@@ -110,11 +105,7 @@ namespace OpenRA
public ActorReference Clone()
{
var clone = new ActorReference(Type);
foreach (var init in initDict.Value)
clone.initDict.Value.Add(init);
return clone;
return new ActorReference(Type, initDict.Value);
}
public void Add(ActorInit init)
@@ -125,6 +116,15 @@ namespace OpenRA
InitDict.Add(init);
}
public void Replace<T>(T init) where T : ActorInit, ISingleInstanceInit
{
var original = GetOrDefault<T>();
if (original != null)
Remove(original);
Add(init);
}
public void Remove(ActorInit o) { initDict.Value.Remove(o); }
public int RemoveAll<T>() where T : ActorInit
@@ -139,7 +139,7 @@ namespace OpenRA
return removed;
}
public IEnumerable<T> GetAll<T>() where T : ActorInit
public IReadOnlyCollection<T> GetAll<T>() where T : ActorInit
{
return initDict.Value.WithInterface<T>();
}
@@ -152,8 +152,9 @@ namespace OpenRA
// If a more specific init is not available, fall back to an unnamed init.
// If duplicate inits are defined, take the last to match standard yaml override expectations
if (info != null && !string.IsNullOrEmpty(info.InstanceName))
return inits.LastOrDefault(i => i.InstanceName == info.InstanceName) ??
inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName));
return
inits.LastOrDefault(i => i.InstanceName == info.InstanceName) ??
inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName));
// Untagged traits will only use untagged inits
return inits.LastOrDefault(i => string.IsNullOrEmpty(i.InstanceName));

View File

@@ -0,0 +1,121 @@
#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;
using System.Collections.Generic;
namespace OpenRA
{
public readonly struct CellCoordsRegion : IEnumerable<CPos>
{
public struct CellCoordsEnumerator : IEnumerator<CPos>
{
readonly CellCoordsRegion r;
public CellCoordsEnumerator(CellCoordsRegion region)
: this()
{
r = region;
Reset();
}
public bool MoveNext()
{
var x = Current.X + 1;
var y = Current.Y;
// Check for column overflow.
if (x > r.BottomRight.X)
{
y++;
x = r.TopLeft.X;
// Check for row overflow.
if (y > r.BottomRight.Y)
return false;
}
Current = new CPos(x, y);
return true;
}
public void Reset()
{
Current = new CPos(r.TopLeft.X - 1, r.TopLeft.Y);
}
public CPos Current { get; private set; }
readonly object IEnumerator.Current => Current;
public readonly void Dispose() { }
}
public CellCoordsRegion(CPos topLeft, CPos bottomRight)
{
TopLeft = topLeft;
BottomRight = bottomRight;
}
public bool Contains(CPos cell)
{
return cell.X >= TopLeft.X && cell.X <= BottomRight.X && cell.Y >= TopLeft.Y && cell.Y <= BottomRight.Y;
}
public override string ToString()
{
return $"{TopLeft}->{BottomRight}";
}
public CellCoordsEnumerator GetEnumerator()
{
return new CellCoordsEnumerator(this);
}
IEnumerator<CPos> IEnumerable<CPos>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <summary>Returns the minimal region that covers at least the specified cells.</summary>
public static CellCoordsRegion BoundingRegion(IReadOnlyCollection<CPos> cells)
{
if (cells == null || cells.Count == 0)
throw new ArgumentException("cells must not be null or empty.", nameof(cells));
var minX = int.MaxValue;
var minY = int.MaxValue;
var maxX = int.MinValue;
var maxY = int.MinValue;
foreach (var cell in cells)
{
if (minX > cell.X)
minX = cell.X;
if (maxX < cell.X)
maxX = cell.X;
if (minY > cell.Y)
minY = cell.Y;
if (maxY < cell.Y)
maxY = cell.Y;
}
return new CellCoordsRegion(new CPos(minX, minY), new CPos(maxX, maxY));
}
public CPos TopLeft { get; }
public CPos BottomRight { get; }
}
}

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -32,12 +33,12 @@ namespace OpenRA
public readonly uint HeightsOffset;
public readonly uint ResourcesOffset;
public BinaryDataHeader(Stream s, int2 expectedSize)
public BinaryDataHeader(Stream s, Size expectedSize)
{
Format = s.ReadUInt8();
var width = 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");
if (Format == 1)
@@ -90,13 +91,13 @@ namespace OpenRA
throw new InvalidOperationException("Map does not have a field/property " + fieldName);
var t = field != null ? field.FieldType : property.PropertyType;
type = t == typeof(List<MiniYamlNode>) ? Type.NodeList :
type = t == typeof(IReadOnlyCollection<MiniYamlNode>) ? Type.NodeList :
t == typeof(MiniYaml) ? Type.MiniYaml : Type.Normal;
}
public void Deserialize(Map map, List<MiniYamlNode> nodes)
public void Deserialize(Map map, MiniYaml yaml)
{
var node = nodes.FirstOrDefault(n => n.Key == key);
var node = yaml.NodeWithKeyOrDefault(key);
if (node == null)
{
if (required)
@@ -111,7 +112,7 @@ namespace OpenRA
else if (type == Type.MiniYaml)
field.SetValue(map, node.Value);
else
FieldLoader.LoadField(map, fieldName, node.Value.Value);
FieldLoader.LoadFieldOrProperty(map, fieldName, node.Value.Value);
}
if (property != null)
@@ -121,7 +122,7 @@ namespace OpenRA
else if (type == Type.MiniYaml)
property.SetValue(map, node.Value, null);
else
FieldLoader.LoadField(map, fieldName, node.Value.Value);
FieldLoader.LoadFieldOrProperty(map, fieldName, node.Value.Value);
}
}
@@ -130,14 +131,14 @@ namespace OpenRA
var value = field != null ? field.GetValue(map) : property.GetValue(map, null);
if (type == Type.NodeList)
{
var listValue = (List<MiniYamlNode>)value;
var listValue = (IReadOnlyCollection<MiniYamlNode>)value;
if (required || listValue.Count > 0)
nodes.Add(new MiniYamlNode(key, null, listValue));
}
else if (type == Type.MiniYaml)
{
var yamlValue = (MiniYaml)value;
if (required || (yamlValue != null && (yamlValue.Value != null || yamlValue.Nodes.Count > 0)))
if (required || (yamlValue != null && (yamlValue.Value != null || yamlValue.Nodes.Length > 0)))
nodes.Add(new MiniYamlNode(key, yamlValue));
}
else
@@ -157,28 +158,28 @@ namespace OpenRA
/// <summary>Defines the order of the fields in map.yaml.</summary>
static readonly MapField[] YamlFields =
{
new MapField("MapFormat"),
new MapField("RequiresMod"),
new MapField("Title"),
new MapField("Author"),
new MapField("Tileset"),
new MapField("MapSize"),
new MapField("Bounds"),
new MapField("Visibility"),
new MapField("Categories"),
new MapField("LockPreview", required: false, ignoreIfValue: "False"),
new MapField("Players", "PlayerDefinitions"),
new MapField("Actors", "ActorDefinitions"),
new MapField("Rules", "RuleDefinitions", required: false),
new MapField("Translations", "TranslationDefinitions", required: false),
new MapField("Sequences", "SequenceDefinitions", required: false),
new MapField("ModelSequences", "ModelSequenceDefinitions", required: false),
new MapField("Weapons", "WeaponDefinitions", required: false),
new MapField("Voices", "VoiceDefinitions", required: false),
new MapField("Music", "MusicDefinitions", required: false),
new MapField("Notifications", "NotificationDefinitions", required: false),
};
[
new("MapFormat"),
new("RequiresMod"),
new("Title"),
new("Author"),
new("Tileset"),
new("MapSize"),
new("Bounds"),
new("Visibility"),
new("Categories"),
new("LockPreview", required: false, ignoreIfValue: "False"),
new("Players", nameof(PlayerDefinitions)),
new("Actors", nameof(ActorDefinitions)),
new("Rules", nameof(RuleDefinitions), required: false),
new("FluentMessages", nameof(FluentMessageDefinitions), required: false),
new("Sequences", nameof(SequenceDefinitions), required: false),
new("ModelSequences", nameof(ModelSequenceDefinitions), required: false),
new("Weapons", nameof(WeaponDefinitions), required: false),
new("Voices", nameof(VoiceDefinitions), required: false),
new("Music", nameof(MusicDefinitions), required: false),
new("Notifications", nameof(NotificationDefinitions), required: false),
];
// Format versions
public int MapFormat { get; private set; }
@@ -192,25 +193,25 @@ namespace OpenRA
public bool LockPreview;
public Rectangle Bounds;
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.
public List<MiniYamlNode> PlayerDefinitions = new();
public List<MiniYamlNode> ActorDefinitions = new();
public IReadOnlyCollection<MiniYamlNode> PlayerDefinitions = [];
public IReadOnlyCollection<MiniYamlNode> ActorDefinitions = [];
// Custom map yaml. Public for access by the map importers and lint checks
public readonly MiniYaml RuleDefinitions;
public readonly MiniYaml TranslationDefinitions;
public readonly MiniYaml SequenceDefinitions;
public readonly MiniYaml ModelSequenceDefinitions;
public readonly MiniYaml WeaponDefinitions;
public readonly MiniYaml VoiceDefinitions;
public readonly MiniYaml MusicDefinitions;
public readonly MiniYaml NotificationDefinitions;
public MiniYaml RuleDefinitions;
public MiniYaml FluentMessageDefinitions;
public MiniYaml SequenceDefinitions;
public MiniYaml ModelSequenceDefinitions;
public MiniYaml WeaponDefinitions;
public MiniYaml VoiceDefinitions;
public MiniYaml MusicDefinitions;
public MiniYaml NotificationDefinitions;
public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = new();
public readonly Dictionary<CPos, TerrainTile> ReplacedInvalidTerrainTiles = [];
// Generated data
public readonly MapGrid Grid;
@@ -274,12 +275,15 @@ namespace OpenRA
try
{
foreach (var filename in contents)
if (filename.EndsWith(".yaml") || filename.EndsWith(".bin") || filename.EndsWith(".lua") || (format >= 12 && filename == "map.png"))
if (filename.EndsWith(".yaml", StringComparison.Ordinal) ||
filename.EndsWith(".bin", StringComparison.Ordinal) ||
filename.EndsWith(".lua", StringComparison.Ordinal) ||
(format >= 12 && filename == "map.png"))
streams.Add(package.GetStream(filename));
// Take the SHA1
if (streams.Count == 0)
return CryptoUtil.SHA1Hash(Array.Empty<byte>());
return CryptoUtil.SHA1Hash([]);
var merged = streams[0];
for (var i = 1; i < streams.Count; i++)
@@ -317,26 +321,25 @@ namespace OpenRA
/// Initializes a new map created by the editor or importer.
/// The map will not receive a valid UID until after it has been saved and reloaded.
/// </summary>
public Map(ModData modData, ITerrainInfo terrainInfo, int width, int height)
public Map(ModData modData, ITerrainInfo terrainInfo, Size size)
{
this.modData = modData;
var size = new Size(width, height);
Grid = modData.Manifest.Get<MapGrid>();
MapSize = size;
Grid = modData.GetOrCreate<MapGrid>();
Title = "Name your map here";
Author = "Your name here";
MapSize = new int2(size);
Tileset = terrainInfo.Id;
// Empty rules that can be added to by the importers.
// Will be dropped on save if nothing is added to it
RuleDefinitions = new MiniYaml("");
Tiles = new CellLayer<TerrainTile>(Grid.Type, size);
Resources = new CellLayer<ResourceTile>(Grid.Type, size);
Height = new CellLayer<byte>(Grid.Type, size);
Ramp = new CellLayer<byte>(Grid.Type, size);
Tiles = new CellLayer<TerrainTile>(Grid.Type, MapSize);
Resources = new CellLayer<ResourceTile>(Grid.Type, MapSize);
Height = new CellLayer<byte>(Grid.Type, MapSize);
Ramp = new CellLayer<byte>(Grid.Type, MapSize);
Tiles.Clear(terrainInfo.DefaultTerrainTile);
if (Grid.MaximumTerrainHeight > 0)
@@ -357,23 +360,22 @@ namespace OpenRA
if (!Package.Contains("map.yaml") || !Package.Contains("map.bin"))
throw new InvalidDataException($"Not a valid map\n File: {package.Name}");
var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), package.Name));
var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), $"{package.Name}:map.yaml"));
foreach (var field in YamlFields)
field.Deserialize(this, yaml.Nodes);
field.Deserialize(this, yaml);
if (MapFormat < SupportedMapFormat)
throw new InvalidDataException($"Map format {MapFormat} is not supported.\n File: {package.Name}");
PlayerDefinitions = MiniYaml.NodesOrEmpty(yaml, "Players");
ActorDefinitions = MiniYaml.NodesOrEmpty(yaml, "Actors");
PlayerDefinitions = yaml.NodeWithKeyOrDefault("Players")?.Value.Nodes ?? [];
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, size);
Resources = new CellLayer<ResourceTile>(Grid.Type, size);
Height = new CellLayer<byte>(Grid.Type, size);
Ramp = new CellLayer<byte>(Grid.Type, size);
Tiles = new CellLayer<TerrainTile>(Grid.Type, MapSize);
Resources = new CellLayer<ResourceTile>(Grid.Type, MapSize);
Height = new CellLayer<byte>(Grid.Type, MapSize);
Ramp = new CellLayer<byte>(Grid.Type, MapSize);
using (var s = Package.GetStream("map.bin"))
{
@@ -381,9 +383,9 @@ namespace OpenRA
if (header.TilesOffset > 0)
{
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 index = s.ReadUInt8();
@@ -400,9 +402,9 @@ namespace OpenRA
if (header.ResourcesOffset > 0)
{
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 density = s.ReadUInt8();
@@ -414,8 +416,8 @@ namespace OpenRA
if (header.HeightsOffset > 0)
{
s.Position = header.HeightsOffset;
for (var i = 0; i < MapSize.X; i++)
for (var j = 0; j < MapSize.Y; j++)
for (var i = 0; i < MapSize.Width; i++)
for (var j = 0; j < MapSize.Height; j++)
Height[new MPos(i, j)] = s.ReadUInt8().Clamp((byte)0, Grid.MaximumTerrainHeight);
}
}
@@ -451,7 +453,7 @@ namespace OpenRA
Sequences = new SequenceSet(this, modData, Tileset, SequenceDefinitions);
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);
var btl = new PPos(Bounds.Left, Bounds.Top);
@@ -512,7 +514,7 @@ namespace OpenRA
foreach (var cell in AllCells)
{
var uv = cell.ToMPos(Grid.Type);
cellProjection[uv] = Array.Empty<PPos>();
cellProjection[uv] = [];
inverseCellProjection[uv] = new List<MPos>(1);
}
@@ -528,7 +530,7 @@ namespace OpenRA
if (Grid.MaximumTerrainHeight == 0)
{
uv = cell.ToMPos(Grid.Type);
cellProjection[cell] = new[] { (PPos)uv };
cellProjection[cell] = [(PPos)uv];
var inverse = inverseCellProjection[uv];
inverse.Clear();
inverse.Add(uv);
@@ -603,11 +605,11 @@ namespace OpenRA
// Any changes to this function should be reflected when setting projectionSafeBounds.
var height = mapHeight[uv];
if (height == 0)
return new[] { (PPos)uv };
return [(PPos)uv];
// Odd-height ramps get bumped up a level to the next even height layer
if ((height & 1) == 1 && Ramp[uv] != 0)
height += 1;
height++;
var candidates = new List<PPos>();
@@ -646,21 +648,24 @@ namespace OpenRA
foreach (var file in Package.Contents)
toPackage.Update(file, Package.GetStream(file).ReadAllBytes());
if (!LockPreview)
void UpdatePackage(string filename, byte[] data)
{
var previewData = SavePreview();
if (Package != toPackage || !Enumerable.SequenceEqual(previewData, Package.GetStream("map.png").ReadAllBytes()))
toPackage.Update("map.png", previewData);
if (Package != toPackage)
toPackage.Update(filename, data);
else
{
var stream = Package.GetStream(filename);
if (stream == null || !Enumerable.SequenceEqual(data, stream.ReadAllBytes()))
toPackage.Update(filename, data);
}
}
// Update the package with the new map data
var textData = Encoding.UTF8.GetBytes(root.WriteToString());
if (Package != toPackage || !Enumerable.SequenceEqual(textData, Package.GetStream("map.yaml").ReadAllBytes()))
toPackage.Update("map.yaml", textData);
if (!LockPreview)
UpdatePackage("map.png", SavePreview());
var binaryData = SaveBinaryData();
if (Package != toPackage || !Enumerable.SequenceEqual(binaryData, Package.GetStream("map.bin").ReadAllBytes()))
toPackage.Update("map.bin", binaryData);
// Update the package with the new map data
UpdatePackage("map.yaml", Encoding.UTF8.GetBytes(root.WriteToString()));
UpdatePackage("map.bin", SaveBinaryData());
Package = toPackage;
@@ -677,24 +682,24 @@ namespace OpenRA
writer.Write(TileFormat);
// Size
writer.Write((ushort)MapSize.X);
writer.Write((ushort)MapSize.Y);
writer.Write((ushort)MapSize.Width);
writer.Write((ushort)MapSize.Height);
// Data offsets
var tilesOffset = 17;
var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.X * MapSize.Y + 17 : 0;
var resourcesOffset = (Grid.MaximumTerrainHeight > 0 ? 4 : 3) * MapSize.X * MapSize.Y + 17;
const int TilesOffset = 17;
var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.Width * MapSize.Height + 17 : 0;
var resourcesOffset = (Grid.MaximumTerrainHeight > 0 ? 4 : 3) * MapSize.Width * MapSize.Height + 17;
writer.Write((uint)tilesOffset);
writer.Write((uint)TilesOffset);
writer.Write((uint)heightsOffset);
writer.Write((uint)resourcesOffset);
// Tile data
if (tilesOffset != 0)
if (TilesOffset != 0)
{
for (var i = 0; i < MapSize.X; i++)
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)];
writer.Write(tile.Type);
@@ -705,16 +710,16 @@ namespace OpenRA
// Height data
if (heightsOffset != 0)
for (var i = 0; i < MapSize.X; i++)
for (var j = 0; j < MapSize.Y; j++)
for (var i = 0; i < MapSize.Width; i++)
for (var j = 0; j < MapSize.Height; j++)
writer.Write(Height[new MPos(i, j)]);
// Resource data
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)];
writer.Write(tile.Type);
@@ -751,7 +756,7 @@ namespace OpenRA
var positions = new List<(MPos Uv, Color Color)>();
foreach (var actor in actors)
{
var s = new ActorReference(actor.Value.Value, actor.Value.ToDictionary());
var s = new ActorReference(actor.Value.Value, actor.Value);
var ai = Rules.Actors[actor.Value.Value];
var impsis = ai.TraitInfos<IMapPreviewSignatureInfo>();
@@ -772,19 +777,10 @@ namespace OpenRA
if (Grid.MaximumTerrainHeight > 0)
{
// The minimap is drawn in cell space, so we need to
// unproject the PPos bounds to find the MPos boundaries.
// This matches the calculation in RadarWidget that is used ingame
for (var x = Bounds.Left; x < Bounds.Right; x++)
{
var allTop = Unproject(new PPos(x, Bounds.Top));
var allBottom = Unproject(new PPos(x, Bounds.Bottom));
if (allTop.Count > 0)
top = Math.Min(top, allTop.MinBy(uv => uv.V).V);
(top, bottom) = GetCellSpaceBounds();
if (allBottom.Count > 0)
bottom = Math.Max(bottom, allBottom.MaxBy(uv => uv.V).V);
}
if (top == int.MaxValue || bottom == int.MinValue)
throw new InvalidDataException("The map has invalid boundaries");
}
else
{
@@ -801,7 +797,7 @@ namespace OpenRA
bitmapWidth = 2 * bitmapWidth - 1;
var stride = bitmapWidth * 4;
var pxStride = 4;
const int PxStride = 4;
var minimapData = new byte[stride * height];
(Color Left, Color Right) terrainColor = default;
@@ -823,10 +819,10 @@ namespace OpenRA
{
// Odd rows are shifted right by 1px
var dx = uv.V & 1;
var xOffset = pxStride * (2 * x + dx);
var xOffset = PxStride * (2 * x + dx);
if (x + dx > 0)
{
var z = y * stride + xOffset - pxStride;
var z = y * stride + xOffset - PxStride;
var c = actorColor.A == 0 ? terrainColor.Left : actorColor;
minimapData[z++] = c.R;
minimapData[z++] = c.G;
@@ -846,7 +842,7 @@ namespace OpenRA
}
else
{
var z = y * stride + pxStride * x;
var z = y * stride + PxStride * x;
var c = actorColor.A == 0 ? terrainColor.Left : actorColor;
minimapData[z++] = c.R;
minimapData[z++] = c.G;
@@ -860,6 +856,28 @@ namespace OpenRA
return png.Save();
}
public (int Top, int Bottom) GetCellSpaceBounds()
{
var top = int.MaxValue;
var bottom = int.MinValue;
// The minimap is drawn in cell space, so we need to
// unproject the PPos bounds to find the MPos boundaries.
// This matches the calculation in RadarWidget that is used ingame
for (var x = Bounds.Left; x < Bounds.Right; x++)
{
var allTop = Unproject(new PPos(x, Bounds.Top));
var allBottom = Unproject(new PPos(x, Bounds.Bottom));
if (allTop.Count > 0)
top = Math.Min(top, allTop.MinBy(uv => uv.V).V);
if (allBottom.Count > 0)
bottom = Math.Max(bottom, allBottom.MaxBy(uv => uv.V).V);
}
return (top, bottom);
}
public bool Contains(CPos cell)
{
if (Grid.Type == MapGridType.RectangularIsometric)
@@ -1024,7 +1042,7 @@ namespace OpenRA
return (PPos)CellContaining(projectedPos).ToMPos(Grid.Type);
}
static readonly PPos[] NoProjectedCells = Array.Empty<PPos>();
static readonly PPos[] NoProjectedCells = [];
public PPos[] ProjectedCellsCovering(MPos uv)
{
if (!initializedCellProjection)
@@ -1044,7 +1062,7 @@ namespace OpenRA
InitializeCellProjection();
if (!inverseCellProjection.Contains(uv))
return new List<MPos>();
return [];
return inverseCellProjection[uv];
}
@@ -1069,16 +1087,15 @@ namespace OpenRA
var oldMapResources = Resources;
var oldMapHeight = Height;
var oldMapRamp = Ramp;
var newSize = new Size(width, height);
Tiles = CellLayer.Resize(oldMapTiles, newSize, oldMapTiles[MPos.Zero]);
Resources = CellLayer.Resize(oldMapResources, newSize, oldMapResources[MPos.Zero]);
Height = CellLayer.Resize(oldMapHeight, newSize, oldMapHeight[MPos.Zero]);
Ramp = CellLayer.Resize(oldMapRamp, newSize, oldMapRamp[MPos.Zero]);
MapSize = new int2(newSize);
MapSize = new Size(width, height);
Tiles = CellLayer.Resize(oldMapTiles, MapSize, oldMapTiles[MPos.Zero]);
Resources = CellLayer.Resize(oldMapResources, MapSize, oldMapResources[MPos.Zero]);
Height = CellLayer.Resize(oldMapHeight, MapSize, oldMapHeight[MPos.Zero]);
Ramp = CellLayer.Resize(oldMapRamp, MapSize, oldMapRamp[MPos.Zero]);
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));
SetBounds(new PPos(tl.U + 1, tl.V + 1), new PPos(br.U - 1, br.V - 1));
}
@@ -1125,6 +1142,11 @@ namespace OpenRA
}
public byte GetTerrainIndex(CPos cell)
{
return GetTerrainIndex(cell.ToMPos(this));
}
public byte GetTerrainIndex(MPos uv)
{
// Lazily initialize a cache for terrain indexes.
if (cachedTerrainIndexes == null)
@@ -1133,7 +1155,6 @@ namespace OpenRA
cachedTerrainIndexes.Clear(InvalidCachedTerrainIndex);
}
var uv = cell.ToMPos(this);
var terrainIndex = cachedTerrainIndexes[uv];
// PERF: Cache terrain indexes per cell on demand.
@@ -1148,7 +1169,12 @@ namespace OpenRA
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)
@@ -1180,7 +1206,7 @@ namespace OpenRA
// Project this guessed cell and take the first available cell
// If it is projected outside the layer, then make another guess.
var allProjected = ProjectedCellsCovering(uv);
var projected = allProjected.Length > 0 ? allProjected.First()
var projected = allProjected.Length > 0 ? allProjected[0]
: new PPos(uv.U, uv.V.Clamp(Bounds.Top, Bounds.Bottom));
// Clamp the projected cell to the map area
@@ -1249,7 +1275,7 @@ namespace OpenRA
PPos edge;
if (allProjected.Length > 0)
{
var puv = allProjected.First();
var puv = allProjected[0];
var horizontalBound = (puv.U - Bounds.Left < Bounds.Width / 2) ? Bounds.Left : Bounds.Right;
var verticalBound = (puv.V - Bounds.Top < Bounds.Height / 2) ? Bounds.Top : Bounds.Bottom;
@@ -1349,13 +1375,18 @@ namespace OpenRA
throw new ArgumentOutOfRangeException(nameof(maxRange),
$"The requested range ({maxRange}) cannot exceed the value of MaximumTileSearchRange ({Grid.MaximumTileSearchRange})");
for (var i = minRange; i <= maxRange; i++)
return FindTilesInAnnulus();
IEnumerable<CPos> FindTilesInAnnulus()
{
foreach (var offset in Grid.TilesByDistance[i])
for (var i = minRange; i <= maxRange; i++)
{
var t = offset + center;
if (allowOutsideBounds ? Tiles.Contains(t) : Contains(t))
yield return t;
foreach (var offset in Grid.TilesByDistance[i])
{
var t = offset + center;
if (allowOutsideBounds ? Tiles.Contains(t) : Contains(t))
yield return t;
}
}
}
}
@@ -1402,11 +1433,11 @@ namespace OpenRA
return modData.DefaultFileSystem.Exists(filename);
}
public bool IsExternalModFile(string filename)
public bool IsExternalFile(string filename)
{
// Explicit package paths never refer to a map
if (filename.Contains('|'))
return modData.DefaultFileSystem.IsExternalModFile(filename);
return modData.DefaultFileSystem.IsExternalFile(filename);
return false;
}

View File

@@ -20,6 +20,7 @@ using OpenRA.FileSystem;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Support;
using FS = OpenRA.FileSystem.FileSystem;
namespace OpenRA
{
@@ -27,26 +28,27 @@ namespace OpenRA
{
public static readonly MapPreview UnknownMap = new(null, null, MapGridType.Rectangular, null);
public IReadOnlyDictionary<IReadOnlyPackage, MapClassification> MapLocations => mapLocations;
readonly Dictionary<IReadOnlyPackage, MapClassification> mapLocations = new();
readonly Dictionary<IReadOnlyPackage, MapClassification> mapLocations = [];
public bool LoadPreviewImages = true;
readonly Cache<string, MapPreview> previews;
readonly ModData modData;
readonly Manifest manifest;
readonly FS modFiles;
Cache<string, MapPreview> previews;
readonly SheetBuilder sheetBuilder;
Thread previewLoaderThread;
bool previewLoaderThreadShutDown = true;
readonly object syncRoot = new();
readonly Queue<MapPreview> generateMinimap = new();
readonly Queue<MapPreview> generateMinimap = [];
public Dictionary<string, string> StringPool { get; } = new Dictionary<string, string>();
public HashSet<string> StringPool { get; } = [];
readonly List<MapDirectoryTracker> mapDirectoryTrackers = new();
readonly List<MapDirectoryTracker> mapDirectoryTrackers = [];
/// <summary>
/// The most recently modified or loaded map at runtime.
/// </summary>
public string LastModifiedMap { get; private set; } = null;
readonly Dictionary<string, string> mapUpdates = new();
readonly Dictionary<string, string> mapUpdates = [];
string lastLoadedLastModifiedMap;
@@ -66,13 +68,11 @@ namespace OpenRA
return null;
}
public MapCache(ModData modData)
public MapCache(Manifest manifest, FS modFiles)
{
this.modData = modData;
var gridType = Exts.Lazy(() => modData.Manifest.Get<MapGrid>().Type);
previews = new Cache<string, MapPreview>(uid => new MapPreview(modData, uid, gridType.Value, this));
sheetBuilder = new SheetBuilder(SheetType.BGRA);
this.manifest = manifest;
this.modFiles = modFiles;
sheetBuilder = new SheetBuilder(SheetType.BGRA, manifest.RendererConstants.MapPreviewSheetSize);
}
public void UpdateMaps()
@@ -81,23 +81,24 @@ namespace OpenRA
tracker.UpdateMaps(this);
}
public void LoadMaps()
public void LoadMaps(ModData modData)
{
// Utility mod that does not support maps
if (!modData.Manifest.Contains<MapGrid>())
if (manifest.MapFolders.Count == 0)
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
foreach (var kv in modData.Manifest.MapFolders)
foreach (var kv in manifest.MapFolders)
{
var name = kv.Key;
var classification = string.IsNullOrEmpty(kv.Value)
? MapClassification.Unknown : Enum<MapClassification>.Parse(kv.Value);
? MapClassification.Unknown : Enum.Parse<MapClassification>(kv.Value);
IReadOnlyPackage package;
var optional = name.StartsWith("~", StringComparison.Ordinal);
var optional = name.StartsWith('~');
if (optional)
name = name[1..];
@@ -106,10 +107,10 @@ namespace OpenRA
// HACK: If the path is inside the support directory then we may need to create it
// Assume that the path is a directory if there is not an existing file with the same name
var resolved = Platform.ResolvePath(name);
if (resolved.StartsWith(Platform.SupportDir) && !File.Exists(resolved))
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && !File.Exists(resolved))
Directory.CreateDirectory(resolved);
package = modData.ModFiles.OpenPackage(name);
package = modFiles.OpenPackage(name);
}
catch
{
@@ -120,38 +121,38 @@ namespace OpenRA
}
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.
var modDataRules = modData.GetRulesYaml();
foreach (var kv in MapLocations)
{
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
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, IEnumerable<List<MiniYamlNode>> modDataRules)
void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, string oldMap,
MapGridType? gridType = null, MiniYamlNode[][] modDataRules = null)
{
IReadOnlyPackage mapPackage = null;
try
{
using (new PerfTimer(map))
{
mapPackage = package.OpenPackage(map, modData.ModFiles);
mapPackage = package.OpenPackage(map, modFiles);
if (mapPackage != null)
{
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();
if (oldMap != uid)
{
@@ -176,12 +177,8 @@ namespace OpenRA
public IEnumerable<IReadWritePackage> EnumerateMapDirPackages(MapClassification classification = MapClassification.System)
{
// Utility mod that does not support maps
if (!modData.Manifest.Contains<MapGrid>())
yield break;
// Enumerate map directories
foreach (var kv in modData.Manifest.MapFolders)
foreach (var kv in manifest.MapFolders)
{
if (!Enum.TryParse(kv.Value, out MapClassification packageClassification))
continue;
@@ -190,16 +187,16 @@ namespace OpenRA
continue;
var name = kv.Key;
var optional = name.StartsWith("~", StringComparison.Ordinal);
var optional = name.StartsWith('~');
if (optional)
name = name[1..];
// Don't try to open the map directory in the support directory if it doesn't exist
var resolved = Platform.ResolvePath(name);
if (resolved.StartsWith(Platform.SupportDir) && (!Directory.Exists(resolved) || !File.Exists(resolved)))
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && (!Directory.Exists(resolved) || !File.Exists(resolved)))
continue;
using (var package = (IReadWritePackage)modData.ModFiles.OpenPackage(name))
using (var package = (IReadWritePackage)modFiles.OpenPackage(name))
yield return package;
}
}
@@ -219,11 +216,12 @@ namespace OpenRA
foreach (var mapDirPackage in mapDirPackages)
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;
}
public void QueryRemoteMapDetails(string repositoryUrl, IEnumerable<string> uids, Action<MapPreview> mapDetailsReceived = null, Action<MapPreview> mapQueryFailed = null)
public void QueryRemoteMapDetails(string repositoryUrl, IEnumerable<string> uids,
Action<MapPreview> mapDetailsReceived = null, Action<MapPreview> mapQueryFailed = null)
{
var queryUids = uids.Distinct()
.Where(uid => uid != null)
@@ -233,44 +231,37 @@ namespace OpenRA
.ToList();
foreach (var uid in queryUids)
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null);
previews[uid].BeginRemoteSearch();
Task.Run(async () =>
{
var client = HttpClientFactory.Create();
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
// Limit each query to 50 maps at a time to avoid request size limits
for (var i = 0; i < queryUids.Count; i += 50)
foreach (var batchUids in queryUids.Chunk(50))
{
var batchUids = queryUids.Skip(i).Take(50).ToList();
var url = repositoryUrl + "hash/" + string.Join(",", batchUids) + "/yaml";
try
using (new PerfTimer("RemoteMapDetails"))
{
var httpResponseMessage = await client.GetAsync(url);
var result = await httpResponseMessage.Content.ReadAsStreamAsync();
var yaml = MiniYaml.FromStream(result);
foreach (var kv in yaml)
previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, mapDetailsReceived);
foreach (var uid in batchUids)
try
{
var p = previews[uid];
if (p.Status != MapStatus.DownloadAvailable)
p.UpdateRemoteSearch(MapStatus.Unavailable, null);
var result = await client.GetStreamAsync(url);
foreach (var kv in MiniYaml.FromStream(result, url, stringPool: stringPool))
previews[kv.Key].CompleteRemoteSearch(kv.Value, mapDetailsReceived);
}
catch (Exception e)
{
Log.Write("debug", "Remote map query failed with error:");
Log.Write("debug", e);
Log.Write("debug", $"URL was: {url}");
}
}
catch (Exception e)
{
Log.Write("debug", "Remote map query failed with error:");
Log.Write("debug", e);
Log.Write("debug", $"URL was: {url}");
foreach (var uid in batchUids)
{
var p = previews[uid];
p.UpdateRemoteSearch(MapStatus.Unavailable, null);
mapQueryFailed?.Invoke(p);
if (p.Status == MapStatus.Searching)
p.CompleteRemoteSearch(null, mapQueryFailed);
}
}
}
@@ -282,11 +273,11 @@ namespace OpenRA
Log.Write("debug", "MapCache.LoadAsyncInternal started");
// Milliseconds to wait on one loop when nothing to do
var emptyDelay = 50;
const int EmptyDelay = 50;
// Keep the thread alive for at least 5 seconds after the last minimap generation
var maxKeepAlive = 5000 / emptyDelay;
var keepAlive = maxKeepAlive;
const int MaxKeepAlive = 5000 / EmptyDelay;
var keepAlive = MaxKeepAlive;
while (true)
{
@@ -306,11 +297,11 @@ namespace OpenRA
if (todo.Count == 0)
{
Thread.Sleep(emptyDelay);
Thread.Sleep(EmptyDelay);
continue;
}
else
keepAlive = maxKeepAlive;
keepAlive = MaxKeepAlive;
// Render the minimap into the shared sheet
foreach (var p in todo)
@@ -337,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.
Game.RunAfterTick(sheetBuilder.Current.ReleaseBuffer);
if (sheetBuilder.Current != null)
Game.RunAfterTick(sheetBuilder.Current.ReleaseBuffer);
Log.Write("debug", "MapCache.LoadAsyncInternal ended");
}
@@ -348,8 +341,8 @@ namespace OpenRA
while (this[uid].Status != MapStatus.Available)
{
if (mapUpdates.ContainsKey(uid))
uid = mapUpdates[uid];
if (mapUpdates.TryGetValue(uid, out var newUid))
uid = newUid;
else
return null;
}
@@ -406,10 +399,16 @@ namespace OpenRA
{
UpdateMaps();
var map = string.IsNullOrEmpty(initialUid) ? null : previews[initialUid];
if (map == null || map.Status != MapStatus.Available || !map.Visibility.HasFlag(MapVisibility.Lobby) || (map.Class != MapClassification.System && map.Class != MapClassification.User))
if (map == null ||
map.Status != MapStatus.Available ||
!map.Visibility.HasFlag(MapVisibility.Lobby) ||
(map.Class != MapClassification.System && map.Class != MapClassification.User))
{
var selected = previews.Values.Where(IsSuitableInitialMap).RandomOrDefault(random) ??
previews.Values.FirstOrDefault(m => m.Status == MapStatus.Available && m.Visibility.HasFlag(MapVisibility.Lobby) && (m.Class == MapClassification.System || m.Class == MapClassification.User));
previews.Values.FirstOrDefault(m =>
m.Status == MapStatus.Available &&
m.Visibility.HasFlag(MapVisibility.Lobby) &&
(m.Class == MapClassification.System || m.Class == MapClassification.User));
return selected == null ? string.Empty : selected.Uid;
}

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
@@ -23,10 +24,11 @@ using OpenRA.FileSystem;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Support;
using OpenRA.Traits;
namespace OpenRA
{
public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError }
public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError, Generatable, Generating }
// Used for grouping maps in the UI
[Flags]
@@ -35,7 +37,8 @@ namespace OpenRA
Unknown = 0,
System = 1,
User = 2,
Remote = 4
Remote = 4,
Generated = 8
}
[SuppressMessage("StyleCop.CSharp.NamingRules",
@@ -48,10 +51,10 @@ namespace OpenRA
{
public readonly string title;
public readonly string author;
public readonly string[] categories;
public readonly ImmutableArray<string> categories;
public readonly int players;
public readonly Rectangle bounds;
public readonly short[] spawnpoints = Array.Empty<short>();
public readonly ImmutableArray<short> spawnpoints = [];
public readonly MapGridType map_grid_type;
public readonly string minimap;
public readonly bool downloading;
@@ -59,6 +62,7 @@ namespace OpenRA
public readonly string rules;
public readonly string players_block;
public readonly int mapformat;
public readonly string game_mod;
}
public sealed class MapPreview : IDisposable, IReadOnlyFileSystem
@@ -68,12 +72,12 @@ namespace OpenRA
{
public int MapFormat;
public string Title;
public string[] Categories;
public ImmutableArray<string> Categories;
public string Author;
public string TileSet;
public MapPlayers Players;
public int PlayerCount;
public CPos[] SpawnPoints;
public ImmutableArray<CPos> SpawnPoints;
public MapGridType GridType;
public Rectangle Bounds;
public Png Preview;
@@ -81,6 +85,7 @@ namespace OpenRA
public MapClassification Class;
public MapVisibility Visibility;
public DateTime ModifiedDate;
public MapGenerationArgs GenerationArgs;
public MiniYaml RuleDefinitions;
public MiniYaml WeaponDefinitions;
@@ -89,8 +94,9 @@ namespace OpenRA
public MiniYaml NotificationDefinitions;
public MiniYaml SequenceDefinitions;
public MiniYaml ModelSequenceDefinitions;
public MiniYaml FluentMessageDefinitions;
public Translation Translation { get; private set; }
public FluentBundle FluentBundle { get; private set; }
public ActorInfo WorldActorInfo { get; private set; }
public ActorInfo PlayerActorInfo { get; private set; }
@@ -111,7 +117,7 @@ namespace OpenRA
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");
WeaponDefinitions = LoadRuleSection(yaml, "Weapons");
@@ -120,13 +126,32 @@ namespace OpenRA
NotificationDefinitions = LoadRuleSection(yaml, "Notifications");
SequenceDefinitions = LoadRuleSection(yaml, "Sequences");
ModelSequenceDefinitions = LoadRuleSection(yaml, "ModelSequences");
Translation = yaml.TryGetValue("Translations", out var node) && node != null
? new Translation(Game.Settings.Player.Language, FieldLoader.GetValue<string[]>("value", node.Value), fileSystem)
: null;
FluentMessageDefinitions = LoadRuleSection(yaml, "FluentMessages");
try
{
if (FluentMessageDefinitions != null)
{
var files = ImmutableArray<string>.Empty;
if (FluentMessageDefinitions.Value != null)
files = FieldLoader.GetValue<ImmutableArray<string>>("value", FluentMessageDefinitions.Value);
string text = null;
if (FluentMessageDefinitions.Nodes.Length > 0)
{
var builder = new StringBuilder();
foreach (var node in FluentMessageDefinitions.Nodes)
if (node.Key == "base64")
builder.Append(Encoding.UTF8.GetString(Convert.FromBase64String(node.Value.Value)));
text = builder.ToString();
}
FluentBundle = new FluentBundle(modData.Manifest.FluentCulture, files, fileSystem, text);
}
else
FluentBundle = null;
// PERF: Implement a minimal custom loader for custom world and player actors to minimize loading time
// This assumes/enforces that these actor types can only inherit abstract definitions (starting with ^)
if (RuleDefinitions != null)
@@ -135,19 +160,26 @@ namespace OpenRA
var files = Enumerable.Empty<string>();
if (RuleDefinitions.Value != null)
{
var mapFiles = FieldLoader.GetValue<string[]>("value", RuleDefinitions.Value);
files = files.Append(mapFiles);
var mapFiles = FieldLoader.GetValue<ImmutableArray<string>>("value", RuleDefinitions.Value);
files = files.Concat(mapFiles);
}
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var sources =
modDataRules.Select(x => x.Where(IsLoadableRuleDefinition).ToList())
.Concat(files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s).Where(IsLoadableRuleDefinition).ToList()));
if (RuleDefinitions.Nodes.Count > 0)
.Concat(files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool).Where(IsLoadableRuleDefinition).ToList()));
if (RuleDefinitions.Nodes.Length > 0)
sources = sources.Append(RuleDefinitions.Nodes.Where(IsLoadableRuleDefinition).ToList());
var yamlNodes = MiniYaml.Merge(sources);
WorldActorInfo = new ActorInfo(modData.ObjectCreator, "world", yamlNodes.First(n => string.Equals(n.Key, "world", StringComparison.InvariantCultureIgnoreCase)).Value);
PlayerActorInfo = new ActorInfo(modData.ObjectCreator, "player", yamlNodes.First(n => string.Equals(n.Key, "player", StringComparison.InvariantCultureIgnoreCase)).Value);
WorldActorInfo = new ActorInfo(
modData.ObjectCreator,
"world",
yamlNodes.First(n => string.Equals(n.Key, "world", StringComparison.InvariantCultureIgnoreCase)).Value);
PlayerActorInfo = new ActorInfo(
modData.ObjectCreator,
"player",
yamlNodes.First(n => string.Equals(n.Key, "player", StringComparison.InvariantCultureIgnoreCase)).Value);
return;
}
}
@@ -167,24 +199,40 @@ namespace OpenRA
}
}
static readonly CPos[] NoSpawns = Array.Empty<CPos>();
readonly object syncRoot = new();
readonly MapCache cache;
readonly ModData modData;
IReadOnlyPackage package;
public readonly string Uid;
public IReadOnlyPackage Package { get; private set; }
public string Path { get; private set; }
void LoadPackage()
{
if (package == null && parentPackage != null)
package = parentPackage.OpenPackage(Path, modData.ModFiles);
}
public Map ToMap()
{
LoadPackage();
using (new PerfTimer("Map"))
return new Map(modData, package);
}
IReadOnlyPackage parentPackage;
volatile InnerData innerData;
public int MapFormat => innerData.MapFormat;
public string Title => innerData.Title;
public string[] Categories => innerData.Categories;
public ImmutableArray<string> Categories => innerData.Categories;
public string Author => innerData.Author;
public string TileSet => innerData.TileSet;
public MapPlayers Players => innerData.Players;
public int PlayerCount => innerData.PlayerCount;
public CPos[] SpawnPoints => innerData.SpawnPoints;
public ImmutableArray<CPos> SpawnPoints => innerData.SpawnPoints;
public MapGridType GridType => innerData.GridType;
public Rectangle Bounds => innerData.Bounds;
public Png Preview => innerData.Preview;
@@ -200,20 +248,37 @@ namespace OpenRA
public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo;
public DateTime ModifiedDate => innerData.ModifiedDate;
public MapGenerationArgs GenerationArgs => innerData.GenerationArgs;
public long DownloadBytes { get; private set; }
public int DownloadPercentage { get; private set; }
/// <summary>
/// Functionality mirrors <see cref="TranslationProvider.GetString"/>, except instead of using
/// loaded <see cref="Map"/>'s translations as backup, we use this <see cref="MapPreview"/>'s.
/// Functionality mirrors <see cref="FluentProvider.GetMessage"/>, except instead of using
/// loaded <see cref="Map"/>'s fluent bundle as backup, we use this <see cref="MapPreview"/>'s.
/// </summary>
public string GetLocalisedString(string key, IDictionary<string, object> args = null)
public string GetMessage(string key, object[] args = null)
{
// PERF: instead of loading mod level Translation per each MapPreview, reuse the already loaded one in TranslationProvider.
if (TranslationProvider.TryGetModString(key, out var message, args))
if (TryGetMessage(key, out var message, args))
return message;
return innerData.Translation?.GetString(key, args) ?? key;
return key;
}
/// <summary>
/// Functionality mirrors <see cref="FluentProvider.TryGetMessage"/>, except instead of using
/// loaded <see cref="Map"/>'s fluent bundle as backup, we use this <see cref="MapPreview"/>'s.
/// </summary>
public bool TryGetMessage(string key, out string message, object[] args = null)
{
// PERF: instead of loading mod level strings per each MapPreview, reuse the already loaded one in FluentProvider.
if (FluentProvider.TryGetModMessage(key, out message, args))
return true;
if (innerData.FluentBundle == null)
return false;
return innerData.FluentBundle.TryGetMessage(key, out message, args);
}
Sprite minimap;
@@ -262,12 +327,12 @@ namespace OpenRA
{
MapFormat = 0,
Title = "Unknown Map",
Categories = new[] { "Unknown" },
Categories = ["Unknown"],
Author = "Unknown Author",
TileSet = "unknown",
Players = null,
PlayerCount = 0,
SpawnPoints = NoSpawns,
SpawnPoints = [],
GridType = gridType,
Bounds = Rectangle.Empty,
Preview = null,
@@ -277,71 +342,40 @@ namespace OpenRA
};
}
// For linting purposes only!
public MapPreview(Map map, ModData modData)
/// <summary>
/// Updates internal state from a map without taking ownership of its package.
/// A new copy of the map package will be opened lazily when needed.
/// </summary>
public void UpdateFromMapWithoutOwningPackage(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification,
MapGridType? gridType = null, MiniYamlNode[][] modDataRules = null)
{
this.modData = modData;
cache = modData.MapCache;
Uid = map.Uid;
Package = map.Package;
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
{
MapFormat = map.MapFormat,
Title = map.Title,
Categories = map.Categories,
Author = map.Author,
TileSet = map.Tileset,
Players = mapPlayers,
PlayerCount = mapPlayers.Players.Count(x => x.Value.Playable),
SpawnPoints = spawns.ToArray(),
GridType = map.Grid.Type,
Bounds = map.Bounds,
Preview = null,
Status = MapStatus.Available,
Class = MapClassification.Unknown,
Visibility = map.Visibility,
};
innerData.SetCustomRules(modData, this, new Dictionary<string, MiniYaml>()
{
{ "Rules", map.RuleDefinitions },
{ "Translations", map.TranslationDefinitions },
{ "Weapons", map.WeaponDefinitions },
{ "Voices", map.VoiceDefinitions },
{ "Music", map.MusicDefinitions },
{ "Notifications", map.NotificationDefinitions },
{ "Sequences", map.SequenceDefinitions },
{ "ModelSequences", map.ModelSequenceDefinitions }
}, null);
UpdateFromMap(p, classification, gridType, modDataRules);
parentPackage = parent;
package = null;
}
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType, IEnumerable<List<MiniYamlNode>> modDataRules)
/// <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)
{
Path = p.Name;
package = p;
Dictionary<string, MiniYaml> yaml;
using (var yamlStream = p.GetStream("map.yaml"))
{
if (yamlStream == null)
throw new FileNotFoundException("Required file map.yaml not present in this map");
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, "map.yaml", stringPool: cache.StringPool)).ToDictionary();
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, $"{p.Name}:map.yaml", stringPool: cache.StringPool)).ToDictionary();
}
Package = p;
parentPackage = parent;
var newData = innerData.Clone();
newData.GridType = gridType;
newData.Class = classification;
newData.GridType = gridType ?? modData.GetOrCreate<MapGrid>().Type;
if (yaml.TryGetValue("MapFormat", out var temp))
{
@@ -354,7 +388,7 @@ namespace OpenRA
newData.Title = temp.Value;
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))
newData.TileSet = temp.Value;
@@ -375,7 +409,7 @@ namespace OpenRA
if (yaml.TryGetValue("MapFormat", out temp))
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;
try
@@ -386,18 +420,18 @@ namespace OpenRA
var spawns = new List<CPos>();
foreach (var kv in actorDefinitions.Nodes.Where(d => d.Value.Value == "mpspawn"))
{
var s = new ActorReference(kv.Value.Value, kv.Value.ToDictionary());
var s = new ActorReference(kv.Value.Value, kv.Value);
spawns.Add(s.Get<LocationInit>().Value);
}
newData.SpawnPoints = spawns.ToArray();
newData.SpawnPoints = spawns.ToImmutableArray();
}
else
newData.SpawnPoints = Array.Empty<CPos>();
newData.SpawnPoints = [];
}
catch (Exception)
{
newData.SpawnPoints = Array.Empty<CPos>();
newData.SpawnPoints = [];
newData.Status = MapStatus.Unavailable;
}
@@ -421,31 +455,122 @@ namespace OpenRA
using (var dataStream = p.GetStream("map.png"))
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
innerData = newData;
// Local maps have higher precedence than remote/generated maps,
// so should always replace their metadata
lock (syncRoot)
innerData = newData;
}
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action<MapPreview> parseMetadata = null)
public void UpdateFromGenerationArgs(MapGenerationArgs args)
{
var newData = innerData.Clone();
newData.Status = status;
newData.Class = MapClassification.Remote;
newData.Class = MapClassification.Generated;
newData.Status = MapStatus.Generatable;
newData.Title = args.Title;
newData.Author = args.Author;
newData.TileSet = args.Tileset;
newData.GenerationArgs = args;
newData.MapFormat = Map.CurrentMapFormat;
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
{
var r = FieldLoader.Load<RemoteMapData>(yaml);
// Map download has been disabled server side
if (!r.downloading)
{
newData.Status = MapStatus.Unavailable;
return;
}
newData.Status = r.downloading ? MapStatus.DownloadAvailable : MapStatus.Unavailable;
newData.Title = r.title;
newData.Categories = r.categories;
newData.Author = r.author;
@@ -457,7 +582,7 @@ namespace OpenRA
var spawns = new CPos[r.spawnpoints.Length / 2];
for (var j = 0; j < r.spawnpoints.Length; j += 2)
spawns[j / 2] = new CPos(r.spawnpoints[j], r.spawnpoints[j + 1]);
newData.SpawnPoints = spawns;
newData.SpawnPoints = spawns.ToImmutableArray();
newData.GridType = r.map_grid_type;
if (cache.LoadPreviewImages)
{
@@ -474,32 +599,48 @@ namespace OpenRA
}
var playersString = Encoding.UTF8.GetString(Convert.FromBase64String(r.players_block));
newData.Players = new MapPlayers(MiniYaml.FromString(playersString));
newData.Players = new MapPlayers(MiniYaml.FromString(playersString,
$"{yaml.NodeWithKey(nameof(r.players_block)).Location.Name}:{nameof(r.players_block)}"));
var rulesString = Encoding.UTF8.GetString(Convert.FromBase64String(r.rules));
var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString)).ToDictionary();
var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString,
$"{yaml.NodeWithKey(nameof(r.rules)).Location.Name}:{nameof(r.rules)}")).ToDictionary();
newData.SetCustomRules(modData, this, rulesYaml, null);
// Map is for a different mod: update its information so it can be displayed
// in the cross-mod server browser UI, but mark it as unavailable so it can't
// be selected in a server for the current mod.
if (!modData.Manifest.MapCompatibility.Contains(r.game_mod))
newData.Status = MapStatus.Unavailable;
}
catch (Exception e)
{
Log.Write("debug", "Failed parsing mapserver response:");
Log.Write("debug", e);
newData.Status = MapStatus.Unavailable;
}
}
// Commit updated data before running the callbacks
innerData = newData;
// 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;
}
if (mapClassification == MapClassification.Remote)
{
if (innerData.Preview != null)
cache.CacheMinimap(this);
parseMetadata?.Invoke(this);
}
// Update the status and class unconditionally
innerData = newData;
}
public void Install(string mapRepositoryUrl, Action onSuccess)
public void Install(string mapRepositoryUrl)
{
if ((Status != MapStatus.DownloadError && Status != MapStatus.DownloadAvailable) || !Game.Settings.Game.AllowDownloading)
return;
@@ -552,14 +693,11 @@ namespace OpenRA
mapInstallPackage.Update(mapFilename, fileStream.ToArray());
Log.Write("debug", $"Downloaded map to '{mapFilename}'");
var package = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles);
if (package == null)
var p = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles);
if (p == null)
innerData.Status = MapStatus.DownloadError;
else
{
UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType, null);
Game.RunAfterTick(onSuccess);
}
UpdateFromMapWithoutOwningPackage(p, mapInstallPackage, MapClassification.User, GridType);
}
catch (Exception e)
{
@@ -572,29 +710,31 @@ namespace OpenRA
public void Invalidate()
{
innerData.Status = MapStatus.Unavailable;
lock (syncRoot)
{
innerData.Class = MapClassification.Unknown;
innerData.Status = MapStatus.Unavailable;
}
}
public void Dispose()
{
if (Package != null)
{
Package.Dispose();
Package = null;
}
package?.Dispose();
package = null;
}
public void Delete()
{
Invalidate();
(parentPackage as IReadWritePackage)?.Delete(Package.Name);
(parentPackage as IReadWritePackage)?.Delete(Path);
}
Stream IReadOnlyFileSystem.Open(string filename)
{
// Explicit package paths never refer to a map
if (!filename.Contains('|') && Package.Contains(filename))
return Package.GetStream(filename);
LoadPackage();
if (!filename.Contains('|') && package.Contains(filename))
return package.GetStream(filename);
return modData.DefaultFileSystem.Open(filename);
}
@@ -610,7 +750,8 @@ namespace OpenRA
// Explicit package paths never refer to a map
if (!filename.Contains('|'))
{
s = Package.GetStream(filename);
LoadPackage();
s = package.GetStream(filename);
if (s != null)
return true;
}
@@ -621,17 +762,18 @@ namespace OpenRA
bool IReadOnlyFileSystem.Exists(string filename)
{
// Explicit package paths never refer to a map
if (!filename.Contains('|') && Package.Contains(filename))
LoadPackage();
if (!filename.Contains('|') && package.Contains(filename))
return true;
return modData.DefaultFileSystem.Exists(filename);
}
bool IReadOnlyFileSystem.IsExternalModFile(string filename)
bool IReadOnlyFileSystem.IsExternalFile(string filename)
{
// Explicit package paths never refer to a map
if (filename.Contains('|'))
return modData.DefaultFileSystem.IsExternalModFile(filename);
return modData.DefaultFileSystem.IsExternalFile(filename);
return false;
}

View File

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

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