Compare commits

...

772 Commits

Author SHA1 Message Date
hacker
ef0c9533a4 Updated version
Some checks failed
Continuous Integration / Linux (.NET 6.0) (push) Has been cancelled
Continuous Integration / Linux (mono) (push) Has been cancelled
Continuous Integration / Windows (.NET 6.0) (push) Has been cancelled
2025-11-02 02:58:11 +00:00
hacker
3aa84277a3 Added Dockerfile 2025-11-02 02:57:32 +00:00
hacker
f9bca71b15 Log chat messages on the server 2025-11-02 02:57:32 +00:00
Pavel Penev
0bbe6e58f6 Added a new helper method - a temporary fix
(cherry picked from commit d83e579dfe)
2025-11-02 02:57:04 +00:00
Gustas
ee4403f923 Add vote kick
(cherry picked from commit 144e716cdf)
2025-11-02 02:57:04 +00:00
Gustas
aacf0e2b8d Add backup ExplicitSequenceFilenames to update rules
(cherry picked from commit 29eaab59be)
2025-11-02 02:55:35 +00:00
penev92
499a40e32d Bumped Eluant NuGet version
The new version fixes the windows 32-bit build not working.
2025-11-02 02:55:04 +00:00
dnqbob
68fd987856 Autocarryall put down unit if destination is cancelled when picking up 2025-11-02 02:54:39 +00:00
Gustas
b4f3d8ae02 Increase perf for parsing remote maps 2025-03-30 18:21:18 +01:00
Gustas
d70c30763b Add a backup for unloaded bots 2025-03-30 18:17:26 +01:00
Gustas
27297a1ba9 Make lobby names dynamic, incase players don't have maps 2025-03-30 18:17:19 +01:00
Gustas
2890ccf2b1 Fix game browser tooltips being untranslated 2025-03-30 18:17:14 +01:00
Gustas
f9f1889112 Fix custom bot names being untranslated in lobby 2025-03-30 10:21:09 +01:00
Gustas
d9c5821329 Fix resource area calculation 2025-03-29 16:54:56 +02:00
Gustas
7fc415de19 Fix rallypoints not showing full target lines 2025-03-29 16:42:13 +02:00
Gustas
ec01f63317 Add missing dispose calls 2025-03-29 13:58:24 +00:00
Gustas
c9ac702217 Fix camera being in an incorrect position of first frame 2025-03-28 21:18:38 +00:00
Gustas
1913c608a6 Fix telemetry data description 2025-03-28 21:13:40 +00:00
Gustas
94ac6cc400 Fix alignment regressions 2025-03-28 21:13:32 +00:00
JovialFeline
97d9520bf0 Get Fluent bot names in server list 2025-03-27 23:05:59 +02:00
N.N
e7300cc430 Fix wrong Spread in Warheads
(cherry picked from commit e78ab81b26ba7da566dae7caea81f40fbd296c6d)
2025-03-23 16:11:40 +02:00
Paul Chote
677d7e47a4 Work around Raspberry Pi GLSL bug.
(cherry picked from commit e8391de36a342fe3612031575193956d14160777)
2025-03-23 15:46:01 +02:00
Matthias Mailänder
5f287d38ab Avoid a thirdparty action. 2025-03-18 14:58:23 +02:00
N.N
3a00d5a679 Fix crush anim 2025-03-18 14:27:00 +02:00
Matthias Mailänder
d5d43ee97c Avoid thirdparty action. 2025-03-18 14:18:39 +02:00
Paul Chote
fd12ef1535 Fix FlashPostProcessEffect channel order. 2025-03-18 14:11:29 +02:00
RoosterDragon
de8317d9b8 Harvesters prefer ore near to the refinery.
When searching for ore, harvesters will prefer ore close to their refinery, the dockPos. However this only works when this is set, and is was not being set when expected. Fixing this prevents harvesters moving in straight lines, particularly when the ore is to the left of the refinery, which is the default path search direction.

Fixes a regression from d0974cfdd2.
2025-03-16 10:39:47 +00:00
Gustas
b953f4a40d Fix a potential crash when cancelling capture 2025-03-16 10:29:48 +00:00
Gustas
b565f0903a Fix tree husks on jungle tileset 2025-03-16 10:26:29 +00:00
N.N
da71a8271d Fix frame on 1141 tile 2025-03-12 16:48:39 +02:00
Gustas
54558169e1 Additional punctuation fixup 2025-03-03 21:02:01 +00:00
test1232156
bcb953309a Remove unnecessary punctuation from descriptions 2025-03-02 18:58:41 +02:00
Paul Chote
a371826613 Fix .NET 6 CI false positives. 2025-02-28 21:00:02 +01:00
JovialFeline
82ee3d779c Fix soviet-06 harvester production crash 2025-02-26 23:40:44 +02:00
Paul Chote
4ea0e70589 Sanitize saved skirmish factions. 2025-02-22 15:32:06 +02:00
Gustas
3f4e4db836 vs seems more standard 2025-02-19 21:01:44 +00:00
Gustas
85020bc2af Adjust to a 4 space standard 2025-02-19 20:55:30 +00:00
Gustas
70bb01a2c7 Tooltips should not have punctuation 2025-02-16 14:12:44 +02:00
Gustas
38d4719073 Re-add a mini lobby options menu to missionbrowser 2025-02-16 12:41:29 +01:00
RoosterDragon
aebb97967d Fix a bad comparison against Target.Invalid.
Target.Invalid acts like a NaN, and will not compare equal with itself. Compare against the TargetType instead, which performs the intended comparison.
2025-02-15 13:22:35 +00:00
Gustas
97f81d0aa4 Update SDL to 2.32.0 2025-02-15 13:19:59 +00:00
JovialFeline
2030c17a8b Add Lua documentation details 2025-02-08 14:02:27 +02:00
Gustas
b6dcd8d9af Fix bot harvesters crashing 2025-02-01 20:36:33 +00:00
Gustas
9a78f22e86 Match the check inside .CenterPosition 2025-02-01 20:33:23 +00:00
Gustas
19ca3d610b Allow tileset to be entered in lower case 2025-02-01 20:23:04 +00:00
Gustas
dd74d3572a Fix utility commands losing data 2025-02-01 20:23:00 +00:00
JovialFeline
ad7e5d62cd Fix unrepairable desert bridge 2025-01-30 14:10:15 +02:00
JovialFeline
ece7547695 D2k shellmap: Hide pickup indicators 2025-01-28 19:04:14 +02:00
JovialFeline
944bc7667a Fix some campaign Fluent keys 2025-01-28 18:57:43 +02:00
JovialFeline
0ac28e1f5f Restore StartGameNotification in missions 2025-01-28 16:23:29 +02:00
JovialFeline
ca3a57289e Hide owner row for D2k special mission Outposts 2025-01-28 16:11:40 +02:00
JovialFeline
a4641be7e7 Fix ordos-04 to use internal bot names 2025-01-16 13:09:38 +02:00
Paul Chote
0651308210 Only expose Facing for actors that define a facing. 2025-01-15 18:07:31 +02:00
Paul Chote
cac6aebb6c Only expose Location and CenterPosition for actors that occupy space. 2025-01-15 18:07:31 +02:00
JovialFeline
0baddf46e2 Correct nod-06c capture objective 2025-01-15 17:39:49 +02:00
Gustas
e0d893b736 Raname GrantConditionOnDeployWithCharge to GrantChargedConditionOnToggle
Also allow deploying without cancelling current activity, and make saboteurs use it

(cherry picked from commit 8b4478260e95170f392ef00c1b06b76dfc29bf2b)
2024-12-27 16:38:07 +02:00
Gustas
2ccfa425b2 Cancel movement in invalid states
(cherry picked from commit d9cfb2c278aa7fd9c7928ebfc43cded5e85ed1a3)
2024-12-27 16:31:05 +02:00
Gustas
52102da839 Fix harvesters refusing to queue dock orders whenn empty
(cherry picked from commit 19040126046a53fd5092515f2a4895fdd5c9c87a)
2024-12-27 16:30:58 +02:00
test1232156
fc08f18663 Fix translation key
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
2024-12-21 19:30:50 +02:00
Pavel Penev
cc064d0dad Added descriptions to ContentInstallerFileSystemLoader
(cherry picked from commit 955bf96a84af425da48963c79c43c5d3adca8cac)
2024-12-21 16:04:36 +02:00
Pavel Penev
8b997a5656 Just some package definition reordering
Mostly superficial, but in case of file name collisions we want the mod-specific files to override the common ones.

(cherry picked from commit f210b39bbcc427bb6c36dfadb71cc15cb1fec2f9)
2024-12-21 16:04:32 +02:00
Pavel Penev
7d8fff535d Added missing D2k packages
(cherry picked from commit 59c0c908cc22be5c70be3d163d07ccb9ff933515)
2024-12-21 16:04:28 +02:00
Pavel Penev
0855a9e5bf Fixed whitespace issues in mod.yaml files
An oversight from PR 21598.

(cherry picked from commit 640aa6545b1f592415a153fea005c2518c550dcb)
2024-12-21 16:04:24 +02:00
Gustas
3fa2b9fe79 Add delete hotkey
(cherry picked from commit 269542c4b0373e1f200afaab01d7d102c5d6194c)
2024-12-20 23:57:55 +02:00
Gustas
c561509c82 Add delete button to the map editor
(cherry picked from commit f820adab51734020600d9758c4b78a1f6585b133)
2024-12-20 23:57:50 +02:00
Gustas
0566b6f787 Update extractors to preserve indentation
(cherry picked from commit 48d5b3c8d946ca71b6d52ead70f0f706db06957e)
2024-12-20 22:00:58 +02:00
Gustas
e2821933a4 Fixup new lines in encyclopedias
(cherry picked from commit 81875a1f3f17d263061c7815809544c1651ba1fd)
2024-12-20 22:00:57 +02:00
Gustas
34c29f872a Fix unit descriptions having incorrect indentation
(cherry picked from commit 0bd139b061c735d79d6e79bf1d2adcdf39b1eb81)
2024-12-20 22:00:56 +02:00
test1232156
2192a6b4f9 Encyclopedia fixup
(cherry picked from commit 9719c1a3b04f77f4f9070ce7bc66cec9732dc153)
2024-12-20 22:00:54 +02:00
RoosterDragon
68ad9163ff Fix BaseBuilderBotModule.LocomotorsForProducibles.
Account for per-actor production (e.g. ProductionQueue) and per-player production (e.g. ClassicProductionQueue). This requires resolving the Production and ProductionQueue traits on both the producing actor, and the owning player actor.

When setting rally points, check the actor didn't die first.
2024-12-19 19:36:39 +02:00
Gustas
5f6eecc1f6 Reduce the size of battlefield news when messages are short
(cherry picked from commit 2a1eeae7ca55e814a8a648b7306648d6585a26c3)
2024-12-19 18:18:26 +02:00
Gustas
5edca87622 Fix Package 'NuGet.CommandLine' 4.4.1 has a known moderate severity vulnerability
(cherry picked from commit 43325fdee7c65e30019ab74c2ee38e4b8cd319d5)
2024-12-12 16:01:35 +02:00
Mustafa Alperen Seki
fa01c24f23 Fix broken condition support for InstantlyRepairable. 2024-12-12 00:30:11 +02:00
Pavel Penev
593c2735ce Revert "Restore threaded renderer on Windows in windowed mode."
This reverts commit 874fa67036.
2024-12-12 00:26:05 +02:00
test1232156
48e72c62f3 Translation key fixes
Co-Authored-By: Gustas <37534529+punkpun@users.noreply.github.com>
2024-12-04 22:10:37 +02:00
Matthias Mailänder
f316d4c757 Update dependency model to fix CVE warnings. 2024-12-02 21:31:01 +02:00
RoosterDragon
d6dba3e545 Fix Animation shadow to account for height.
If a unit is above the terrain, the shadow shouldn't display directly underneath the unit, it should take the height into account and display further down. This fix uses the same adjustment as applied by the WithShadow trait.
2024-11-30 14:45:40 +02:00
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
2266 changed files with 59993 additions and 27977 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

@@ -14,11 +14,14 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Remove System .NET
run: sudo apt-get remove -y dotnet*
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.0.x'
@@ -39,7 +42,7 @@ jobs:
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Check Code
run: |
@@ -57,10 +60,10 @@ jobs:
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.0.x'

View File

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

View File

@@ -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@v4
- name: Prepare Environment
run: echo "GIT_TAG=${GITHUB_REF#refs/tags/}" >> ${GITHUB_ENV}
@@ -40,10 +40,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.0.x'
@@ -53,27 +53,25 @@ 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-13
steps:
- name: Clone Repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.0.x'
@@ -92,23 +90,21 @@ 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@v4
- name: Install .NET 6.0
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.0.x'
@@ -124,10 +120,8 @@ jobs:
./packaging/windows/buildpackage.sh "${GIT_TAG}" "${PWD}/build/windows"
- 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/windows/*
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
gh release upload ${{ github.ref_name }} build/windows/*

60
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)
@@ -98,6 +97,7 @@ Also thanks to:
* Kanar
* Kenny Hoxworth (hoxworth)
* Kevin Azzam (ChaoticMind)
* Kevin Streser
* Krishnakanth Mallik
* Kyle Smith (Smitty)
* Kyrre Soerensen (zypres)
@@ -150,6 +150,7 @@ Also thanks to:
* Teemu Nieminen (Temeez)
* Thomas Christlieb (ThomasChr)
* Tim Mylemans (gecko)
* Tinix
* Tirili
* Tomas Einarsson (Mesacer)
* Tom van Leth (tovl)
@@ -161,61 +162,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.

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

@@ -33,7 +33,7 @@
<Optimize>false</Optimize>
<!-- Enable only for Debug builds to improve compile-time performance for Release builds -->
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<!-- Enabling GenerateDocumentationFile is required for IDE0005 (Remove unnecessary import)
<!-- Enabling GenerateDocumentationFile is required for IDE0005 (Remove unnecessary import)
rule to run in command line builds. https://github.com/dotnet/roslyn/issues/41640
Enable only for Debug builds to improve compile-time performance for Release builds -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -51,8 +51,11 @@
</ItemGroup>
</Target>
<!-- StyleCop -->
<!-- StyleCop/Roslynator -->
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
<!-- Roslynator analyzers fail to run under Mono (AD0001) -->
<PackageReference Include="Roslynator.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.2.0" PrivateAssets="All" Condition="'$(MSBuildRuntimeType)'!='Mono'" />
</ItemGroup>
</Project>

29
Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
FROM mcr.microsoft.com/dotnet/sdk:6.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,7 +7,7 @@ Windows
=======
Compiling OpenRA requires the following dependencies:
* [Windows PowerShell >= 4.0](http://microsoft.com/powershell) (included by default in recent Windows 10 versions)
* [Windows PowerShell >= 4.0](https://microsoft.com/powershell) (included by default in recent Windows 10 versions)
* [.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0) (or via Visual Studio)
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.
@@ -25,7 +25,7 @@ To compile OpenRA, run `make` from the command line (or `make RUNTIME=mono` if u
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:

View File

@@ -153,12 +153,12 @@ 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'

View File

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

View File

@@ -71,7 +71,12 @@ namespace OpenRA
public IEffectiveOwner EffectiveOwner { get; }
public IOccupySpace OccupiesSpace { get; }
public ITargetable[] Targetables { get; }
public IEnumerable<ITargetablePositions> EnabledTargetablePositions { get; private set; }
public IEnumerable<ITargetablePositions> EnabledTargetablePositions { get; }
readonly ICrushable[] crushables;
public ICrushable[] Crushables
{
get => crushables ?? throw new InvalidOperationException($"Crushables for {Info.Name} are not initialized.");
}
public bool IsIdle => CurrentActivity == null;
public bool IsDead => Disposed || (health != null && health.IsDead);
@@ -155,6 +160,7 @@ namespace OpenRA
var targetablesList = new List<ITargetable>();
var targetablePositionsList = new List<ITargetablePositions>();
var syncHashesList = new List<SyncHash>();
var crushablesList = new List<ICrushable>();
foreach (var traitInfo in Info.TraitsInConstructOrder())
{
@@ -181,6 +187,7 @@ namespace OpenRA
{ if (trait is ITargetable t) targetablesList.Add(t); }
{ if (trait is ITargetablePositions t) targetablePositionsList.Add(t); }
{ if (trait is ISync t) syncHashesList.Add(new SyncHash(t)); }
{ if (trait is ICrushable t) crushablesList.Add(t); }
}
resolveOrders = resolveOrdersList.ToArray();
@@ -195,6 +202,7 @@ namespace OpenRA
EnabledTargetablePositions = targetablePositions.Where(Exts.IsTraitEnabled);
enabledTargetableWorldPositions = EnabledTargetablePositions.SelectMany(tp => tp.TargetablePositions(this));
SyncHashes = syncHashesList.ToArray();
crushables = crushablesList.ToArray();
}
}

View File

@@ -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
@@ -96,7 +97,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 +107,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 +121,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 +149,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;
@@ -61,14 +63,14 @@ namespace OpenRA
public static readonly CVec[] Directions =
{
new CVec(-1, -1),
new CVec(-1, 0),
new CVec(-1, 1),
new CVec(0, -1),
new CVec(0, 1),
new CVec(1, -1),
new CVec(1, 0),
new CVec(1, 1),
new(-1, -1),
new(-1, 0),
new(-1, 1),
new(0, -1),
new(0, 1),
new(1, -1),
new(1, 0),
new(1, 1),
};
#region Scripting interface
@@ -76,7 +78,8 @@ namespace OpenRA
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

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

View File

@@ -27,7 +27,6 @@ namespace OpenRA
{
public readonly string Id;
public readonly string Version;
public readonly string Title;
public readonly string LaunchPath;
public readonly string[] LaunchArgs;
public Sprite Icon { get; internal set; }
@@ -66,6 +65,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 +76,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 +94,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 +122,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[]
{
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 +204,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

@@ -14,6 +14,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using OpenRA.Primitives;
using OpenRA.Support;
using OpenRA.Traits;
@@ -22,15 +23,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); }
@@ -131,11 +141,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 +182,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 +337,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 +378,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 +390,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);
@@ -394,15 +433,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,14 +463,15 @@ namespace OpenRA
continue;
// Check for a key conflict:
if (!d.TryAdd(key, element))
if (!output.TryAdd(key, element))
{
dupKeys ??= new Dictionary<TKey, List<string>>();
if (!dupKeys.TryGetValue(key, out var dupKeyMessages))
{
// Log the initial conflicting value already inserted:
dupKeyMessages = new List<string>
{
logValue(d[key])
logValue(output[key])
};
dupKeys.Add(key, dupKeyMessages);
}
@@ -431,15 +482,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($"{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)
@@ -493,17 +543,22 @@ namespace OpenRA
return result;
}
public static int ParseIntegerInvariant(string s)
{
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static byte ParseByte(string s)
public static byte ParseByteInvariant(string s)
{
return byte.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static bool TryParseIntegerInvariant(string s, out int i)
public static short ParseInt16Invariant(string s)
{
return short.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static int ParseInt32Invariant(string s)
{
return int.Parse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo);
}
public static bool TryParseInt32Invariant(string s, out int i)
{
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
@@ -513,6 +568,26 @@ namespace OpenRA
return long.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
public static string ToStringInvariant(this byte i)
{
return i.ToString(NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this byte i, string format)
{
return i.ToString(format, NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this int i)
{
return i.ToString(NumberFormatInfo.InvariantInfo);
}
public static string ToStringInvariant(this int i, string format)
{
return i.ToString(format, NumberFormatInfo.InvariantInfo);
}
public static bool IsTraitEnabled<T>(this T trait)
{
return trait is not IDisabledTrait disabledTrait || !disabledTrait.IsTraitDisabled;
@@ -562,6 +637,11 @@ namespace OpenRA
{
return new LineSplitEnumerator(str.AsSpan(), separator);
}
public static bool TryParseInt32Invariant(string s, out int i)
{
return int.TryParse(s, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out i);
}
}
public ref struct LineSplitEnumerator
@@ -576,7 +656,7 @@ namespace OpenRA
Current = default;
}
public LineSplitEnumerator GetEnumerator() => this;
public readonly LineSplitEnumerator GetEnumerator() => this;
public bool MoveNext()
{

View File

@@ -87,6 +87,7 @@ namespace OpenRA
{ typeof(WAngle), ParseWAngle },
{ typeof(WRot), ParseWRot },
{ typeof(CPos), ParseCPos },
{ typeof(CPos[]), ParseCPosArray },
{ typeof(CVec), ParseCVec },
{ typeof(CVec[]), ParseCVecArray },
{ typeof(BooleanExpression), ParseBooleanExpression },
@@ -118,7 +119,7 @@ namespace OpenRA
static object ParseInt(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (Exts.TryParseIntegerInvariant(value, out var res))
if (Exts.TryParseInt32Invariant(value, out var res))
{
if (res >= 0 && res < BoxedInts.Length)
return BoxedInts[res];
@@ -195,11 +196,11 @@ namespace OpenRA
if (value != null)
{
var parts = value.Split(SplitComma);
if (parts.Length == 3)
{
if (WDist.TryParse(parts[0], out var rx) && WDist.TryParse(parts[1], out var ry) && WDist.TryParse(parts[2], out var rz))
return new WVec(rx, ry, rz);
}
if (parts.Length == 3
&& WDist.TryParse(parts[0], out var rx)
&& WDist.TryParse(parts[1], out var ry)
&& WDist.TryParse(parts[2], out var rz))
return new WVec(rx, ry, rz);
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -219,8 +220,8 @@ namespace OpenRA
for (var i = 0; i < vecs.Length; ++i)
{
if (WDist.TryParse(parts[3 * i], out var rx)
&& WDist.TryParse(parts[3 * i + 1], out var ry)
&& WDist.TryParse(parts[3 * i + 2], out var rz))
&& WDist.TryParse(parts[3 * i + 1], out var ry)
&& WDist.TryParse(parts[3 * i + 2], out var rz))
vecs[i] = new WVec(rx, ry, rz);
}
@@ -235,13 +236,11 @@ namespace OpenRA
if (value != null)
{
var parts = value.Split(SplitComma);
if (parts.Length == 3)
{
if (WDist.TryParse(parts[0], out var rx)
&& WDist.TryParse(parts[1], out var ry)
&& WDist.TryParse(parts[2], out var rz))
return new WPos(rx, ry, rz);
}
if (parts.Length == 3
&& WDist.TryParse(parts[0], out var rx)
&& WDist.TryParse(parts[1], out var ry)
&& WDist.TryParse(parts[2], out var rz))
return new WPos(rx, ry, rz);
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -249,7 +248,7 @@ namespace OpenRA
static object ParseWAngle(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (Exts.TryParseIntegerInvariant(value, out var res))
if (Exts.TryParseInt32Invariant(value, out var res))
return new WAngle(res);
return InvalidValueAction(value, fieldType, fieldName);
}
@@ -259,13 +258,11 @@ namespace OpenRA
if (value != null)
{
var parts = value.Split(SplitComma);
if (parts.Length == 3)
{
if (Exts.TryParseIntegerInvariant(parts[0], out var rr)
&& Exts.TryParseIntegerInvariant(parts[1], out var rp)
&& Exts.TryParseIntegerInvariant(parts[2], out var ry))
return new WRot(new WAngle(rr), new WAngle(rp), new WAngle(ry));
}
if (parts.Length == 3
&& Exts.TryParseInt32Invariant(parts[0], out var rr)
&& Exts.TryParseInt32Invariant(parts[1], out var rp)
&& Exts.TryParseInt32Invariant(parts[2], out var ry))
return new WRot(new WAngle(rr), new WAngle(rp), new WAngle(ry));
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -278,10 +275,33 @@ namespace OpenRA
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 3)
return new CPos(
Exts.ParseIntegerInvariant(parts[0]),
Exts.ParseIntegerInvariant(parts[1]),
Exts.ParseByte(parts[2]));
return new CPos(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
Exts.ParseInt32Invariant(parts[0]),
Exts.ParseInt32Invariant(parts[1]),
Exts.ParseByteInvariant(parts[2]));
return new CPos(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseCPosArray(string fieldName, Type fieldType, string value, MemberInfo field)
{
if (value != null)
{
var parts = value.Split(SplitComma);
if (parts.Length % 2 != 0)
return InvalidValueAction(value, fieldType, fieldName);
var vecs = new CPos[parts.Length / 2];
for (var i = 0; i < vecs.Length; i++)
{
if (int.TryParse(parts[2 * i], out var rx)
&& int.TryParse(parts[2 * i + 1], out var ry))
vecs[i] = new CPos(rx, ry);
}
return vecs;
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -292,7 +312,7 @@ namespace OpenRA
if (value != null)
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new CVec(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
return new CVec(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -385,7 +405,7 @@ namespace OpenRA
var ints = new int2[parts.Length / 2];
for (var i = 0; i < ints.Length; i++)
ints[i] = new int2(Exts.ParseIntegerInvariant(parts[2 * i]), Exts.ParseIntegerInvariant(parts[2 * i + 1]));
ints[i] = new int2(Exts.ParseInt32Invariant(parts[2 * i]), Exts.ParseInt32Invariant(parts[2 * i + 1]));
return ints;
}
@@ -398,7 +418,7 @@ namespace OpenRA
if (value != null)
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new Size(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
return new Size(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -412,7 +432,7 @@ namespace OpenRA
if (parts.Length != 2)
return InvalidValueAction(value, fieldType, fieldName);
return new int2(Exts.ParseIntegerInvariant(parts[0]), Exts.ParseIntegerInvariant(parts[1]));
return new int2(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -460,10 +480,10 @@ namespace OpenRA
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new Rectangle(
Exts.ParseIntegerInvariant(parts[0]),
Exts.ParseIntegerInvariant(parts[1]),
Exts.ParseIntegerInvariant(parts[2]),
Exts.ParseIntegerInvariant(parts[3]));
Exts.ParseInt32Invariant(parts[0]),
Exts.ParseInt32Invariant(parts[1]),
Exts.ParseInt32Invariant(parts[2]),
Exts.ParseInt32Invariant(parts[3]));
}
return InvalidValueAction(value, fieldType, fieldName);
@@ -500,7 +520,7 @@ namespace OpenRA
if (yaml == null)
return Activator.CreateInstance(fieldType);
var dict = Activator.CreateInstance(fieldType, yaml.Nodes.Count);
var dict = Activator.CreateInstance(fieldType, yaml.Nodes.Length);
var arguments = fieldType.GetGenericArguments();
var addMethod = fieldType.GetMethod(nameof(Dictionary<object, object>.Add), arguments);
var addArgs = new object[2];
@@ -531,7 +551,7 @@ namespace OpenRA
if (string.IsNullOrEmpty(value))
return null;
var innerType = fieldType.GetGenericArguments().First();
var innerType = fieldType.GetGenericArguments()[0];
var innerValue = GetValue("Nullable<T>", innerType, value, field);
return fieldType.GetConstructor(new[] { innerType }).Invoke(new[] { innerValue });
}

View File

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

View File

@@ -16,6 +16,7 @@ using System.Linq;
using System.Net;
using System.Text;
using ICSharpCode.SharpZipLib.Checksum;
using ICSharpCode.SharpZipLib.Zip.Compression;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using OpenRA.Graphics;
using OpenRA.Primitives;
@@ -45,12 +46,13 @@ namespace OpenRA.FileFormats
var data = new List<byte>();
Type = SpriteFrameType.Rgba32;
byte bitDepth = 8;
while (true)
{
var length = IPAddress.NetworkToHostOrder(s.ReadInt32());
var type = Encoding.UTF8.GetString(s.ReadBytes(4));
var type = s.ReadASCII(4);
var content = s.ReadBytes(length);
/*var crc = */s.ReadInt32();
s.ReadInt32(); // crc
if (!headerParsed && type != "IHDR")
throw new InvalidDataException("Invalid PNG file - header does not appear first.");
@@ -66,7 +68,7 @@ namespace OpenRA.FileFormats
Width = IPAddress.NetworkToHostOrder(ms.ReadInt32());
Height = IPAddress.NetworkToHostOrder(ms.ReadInt32());
var bitDepth = ms.ReadUInt8();
bitDepth = ms.ReadUInt8();
var colorType = (PngColorType)ms.ReadUInt8();
if (IsPaletted(bitDepth, colorType))
Type = SpriteFrameType.Indexed8;
@@ -76,7 +78,7 @@ namespace OpenRA.FileFormats
Data = new byte[Width * Height * PixelStride];
var compression = ms.ReadUInt8();
/*var filter = */ms.ReadUInt8();
ms.ReadUInt8(); // filter
var interlace = ms.ReadUInt8();
if (compression != 0)
@@ -92,8 +94,8 @@ namespace OpenRA.FileFormats
case "PLTE":
{
Palette = new Color[256];
for (var i = 0; i < length / 3; i++)
Palette = new Color[length / 3];
for (var i = 0; i < Palette.Length; i++)
{
var r = ms.ReadUInt8(); var g = ms.ReadUInt8(); var b = ms.ReadUInt8();
Palette[i] = Color.FromArgb(r, g, b);
@@ -136,14 +138,34 @@ namespace OpenRA.FileFormats
{
var pxStride = PixelStride;
var rowStride = Width * pxStride;
var pixelsPerByte = 8 / bitDepth;
var sourceRowStride = Exts.IntegerDivisionRoundingAwayFromZero(rowStride, pixelsPerByte);
Span<byte> prevLine = new byte[rowStride];
for (var y = 0; y < Height; y++)
{
var filter = (PngFilter)ds.ReadUInt8();
ds.ReadBytes(Data, y * rowStride, rowStride);
ds.ReadBytes(Data, y * rowStride, sourceRowStride);
var line = Data.AsSpan(y * rowStride, rowStride);
// If the source has a bit depth of 1, 2 or 4 it packs multiple pixels per byte.
// Unpack to bit depth of 8, yielding 1 pixel per byte.
// This makes life easier for consumers of palleted data.
if (bitDepth < 8)
{
var mask = 0xFF >> (8 - bitDepth);
for (var i = sourceRowStride - 1; i >= 0; i--)
{
var packed = line[i];
for (var j = 0; j < pixelsPerByte; j++)
{
var dest = i * pixelsPerByte + j;
if (dest < line.Length) // Guard against last byte being only partially packed
line[dest] = (byte)(packed >> (8 - (j + 1) * bitDepth) & mask);
}
}
}
switch (filter)
{
case PngFilter.None:
@@ -269,7 +291,7 @@ namespace OpenRA.FileFormats
static bool IsPaletted(byte bitDepth, PngColorType colorType)
{
if (bitDepth == 8 && colorType == (PngColorType.Indexed | PngColorType.Color))
if (bitDepth <= 8 && colorType == (PngColorType.Indexed | PngColorType.Color))
return true;
if (bitDepth == 8 && colorType == (PngColorType.Color | PngColorType.Alpha))
@@ -287,10 +309,10 @@ namespace OpenRA.FileFormats
var typeBytes = Encoding.ASCII.GetBytes(type);
output.Write(IPAddress.HostToNetworkOrder((int)input.Length));
output.WriteArray(typeBytes);
output.Write(typeBytes);
var data = input.ReadAllBytes();
output.WriteArray(data);
output.Write(data);
var crc32 = new Crc32();
crc32.Update(typeBytes);
@@ -302,7 +324,7 @@ namespace OpenRA.FileFormats
{
using (var output = new MemoryStream())
{
output.WriteArray(Signature);
output.Write(Signature);
using (var header = new MemoryStream())
{
header.Write(IPAddress.HostToNetworkOrder(Width));
@@ -350,13 +372,14 @@ namespace OpenRA.FileFormats
using (var data = new MemoryStream())
{
using (var compressed = new DeflaterOutputStream(data))
using (var compressed = new DeflaterOutputStream(data, new Deflater(Deflater.BEST_COMPRESSION)))
{
var rowStride = Width * PixelStride;
for (var y = 0; y < Height; y++)
{
// Write uncompressed scanlines for simplicity
compressed.WriteByte(0);
// Assuming no filtering for simplicity
const byte FilterType = 0;
compressed.WriteByte(FilterType);
compressed.Write(Data, y * rowStride, rowStride);
}
@@ -371,7 +394,7 @@ namespace OpenRA.FileFormats
{
using (var text = new MemoryStream())
{
text.WriteArray(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
text.Write(Encoding.ASCII.GetBytes(kv.Key + (char)0 + kv.Value));
WritePngChunk(output, "tEXt", text);
}
}

View File

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

View File

@@ -23,7 +23,7 @@ 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
@@ -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
@@ -185,11 +181,13 @@ namespace OpenRA.FileSystem
fileIndex = new Cache<string, List<IReadOnlyPackage>>(_ => new 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

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

View File

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

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

@@ -0,0 +1,167 @@
#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.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;
}
}
public class FluentBundle
{
readonly Linguini.Bundle.FluentBundle bundle;
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem)
: this(culture, paths, fileSystem, error => Log.Write("debug", error.Message)) { }
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, string text)
: this(culture, paths, fileSystem, text, error => Log.Write("debug", error.Message)) { }
public FluentBundle(string culture, string[] paths, IReadOnlyFileSystem fileSystem, Action<ParseError> onError)
: this(culture, paths, fileSystem, null, onError) { }
public FluentBundle(string culture, string text, Action<ParseError> onError)
: this(culture, null, null, text, onError) { }
public FluentBundle(string culture, 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)
{
if (key == null)
throw new ArgumentNullException(nameof(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 = new Dictionary<string, IFluentType>();
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,93 @@
#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.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(ModData modData, IReadOnlyFileSystem fileSystem)
{
lock (SyncObject)
{
modFluentBundle = new FluentBundle(modData.Manifest.FluentCulture, modData.Manifest.FluentMessages, fileSystem);
if (fileSystem is Map map && map.FluentMessageDefinitions != null)
{
var files = Array.Empty<string>();
if (map.FluentMessageDefinitions.Value != null)
files = FieldLoader.GetValue<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(modData.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

@@ -29,7 +29,7 @@ namespace OpenRA
{
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 +86,9 @@ namespace OpenRA
static void JoinInner(OrderManager om)
{
// Refresh TextNotificationsManager before the game starts.
// Refresh static classes before the game starts.
TextNotificationsManager.Clear();
UnitOrders.Clear();
// HACK: The shellmap World and OrderManager are owned by the main menu's WorldRenderer instead of Game.
// This allows us to switch Game.OrderManager from the shellmap to the new network connection when joining
@@ -179,6 +180,7 @@ namespace OpenRA
}
public static event Action BeforeGameStart = () => { };
public static event Action AfterGameStart = () => { };
internal static void StartGame(string mapUID, WorldType type)
{
// Dispose of the old world before creating a new one.
@@ -222,6 +224,12 @@ namespace OpenRA
// Much better to clean up now then to drop frames during gameplay for GC pauses.
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
// PostLoadComplete is designed for anything that should trigger at the very end of loading.
// e.g. audio notifications that the game is starting.
OrderManager.World.PostLoadComplete(worldRenderer);
AfterGameStart();
}
public static void RestartGame()
@@ -360,22 +368,7 @@ 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);
var platform = CreatePlatform(p);
Renderer = new Renderer(platform, Settings.Graphics);
Sound = new Sound(platform, Settings.Sound);
@@ -402,7 +395,7 @@ namespace OpenRA
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})");
Console.WriteLine($"\t{mod.Key} ({mod.Value.Metadata.Version})");
modLaunchWrapper = args.GetValue("Engine.LaunchWrapper", null);
@@ -415,7 +408,7 @@ namespace OpenRA
// Sanitize input from platform-specific launchers
// Process.Start requires paths to not be quoted, even if they contain spaces
if (launchPath != null && launchPath.First() == '"' && launchPath.Last() == '"')
if (launchPath != null && launchPath[0] == '"' && launchPath.Last() == '"')
launchPath = launchPath[1..^1];
// Metadata registration requires an explicit launch path
@@ -427,11 +420,31 @@ namespace OpenRA
Console.WriteLine("External mods:");
foreach (var mod in ExternalMods)
Console.WriteLine($"\t{mod.Key}: {mod.Value.Title} ({mod.Value.Version})");
Console.WriteLine($"\t{mod.Key} ({mod.Value.Version})");
InitializeMod(modID, args);
}
public static IPlatform CreatePlatform(string platformName)
{
var rendererPath = Path.Combine(Platform.BinDir, "OpenRA.Platforms." + platformName + ".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.");
return (IPlatform)platformType.GetConstructor(Type.EmptyTypes).Invoke(null);
}
public static void InitializeMod(string mod, Arguments args)
{
// Clear static state if we have switched mods
@@ -483,11 +496,11 @@ namespace OpenRA
Renderer.InitializeDepthBuffer(grid);
Cursor?.Dispose();
Cursor = new CursorManager(ModData.CursorProvider);
Cursor = new CursorManager(ModData.CursorProvider, ModData.Manifest.CursorSheetSize);
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;
@@ -523,10 +536,11 @@ namespace OpenRA
.Where(m => m.Status == MapStatus.Available && m.Visibility.HasFlag(MapVisibility.Shellmap))
.Select(m => m.Uid);
if (!shellmaps.Any())
var shellmap = shellmaps.RandomOrDefault(CosmeticRandom);
if (shellmap == null)
throw new InvalidDataException("No valid shellmaps available");
return shellmaps.Random(CosmeticRandom);
return shellmap;
}
public static void SwitchToExternalMod(ExternalMod mod, string[] launchArguments = null, Action onFailed = null)
@@ -581,7 +595,7 @@ namespace OpenRA
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 +614,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 +626,11 @@ namespace OpenRA
Sync.RunUnsynced(world, orderManager.TickImmediate);
if (world == null)
{
if (orderManager.GameStarted)
PerfHistory.Reset(); // Remove old history when a new game starts.
return;
}
if (orderManager.TryTick())
{
@@ -663,7 +684,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 +694,7 @@ namespace OpenRA
}
Ui.PrepareRenderables();
Renderer.WorldModelRenderer.EndFrame();
worldRenderer?.EndFrame();
}
// worldRenderer is null during the initial install/download screen
@@ -777,7 +798,7 @@ namespace OpenRA
var logicWorld = worldRenderer?.World;
// ReplayTimestep = 0 means the replay is paused: we need to keep logicInterval as UI.Timestep to avoid breakage
if (logicWorld != null && !(logicWorld.IsReplay && logicWorld.ReplayTimestep == 0))
if (logicWorld != null && (!logicWorld.IsReplay || logicWorld.ReplayTimestep != 0))
logicInterval = logicWorld == OrderManager.World ? OrderManager.SuggestedTimestep : logicWorld.Timestep;
// Ideal time between screen updates
@@ -912,15 +933,15 @@ namespace OpenRA
{
var endpoints = new List<IPEndPoint>
{
new IPEndPoint(IPAddress.IPv6Any, settings.ListenPort),
new IPEndPoint(IPAddress.Any, settings.ListenPort)
new(IPAddress.IPv6Any, settings.ListenPort),
new(IPAddress.Any, settings.ListenPort)
};
server = new Server.Server(endpoints, settings, ModData, ServerType.Multiplayer);
return server.GetEndpointForLocalConnection();
}
public static ConnectionTarget CreateLocalServer(string map)
public static ConnectionTarget CreateLocalServer(string map, bool isSkirmish = false)
{
var settings = new ServerSettings()
{
@@ -934,9 +955,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 +985,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.PackageName) == launchMap);
if (map == null)
throw new ArgumentException($"Could not find map '{launchMap}'.");

View File

@@ -19,6 +19,9 @@ namespace OpenRA
{
public class GameInformation
{
[FluentReference("name", "number")]
const string EnumeratedBotName = "enumerated-bot-name";
public string Mod;
public string Version;
@@ -49,13 +52,13 @@ namespace OpenRA
playersByRuntime = new Dictionary<OpenRA.Player, Player>();
}
public static GameInformation Deserialize(string data)
public static GameInformation Deserialize(string data, string path)
{
try
{
var info = new GameInformation();
var nodes = MiniYaml.FromString(data);
var nodes = MiniYaml.FromString(data, path);
foreach (var node in nodes)
{
var keyParts = node.Key.Split('@');
@@ -85,7 +88,7 @@ namespace OpenRA
{
var nodes = new List<MiniYamlNode>
{
new MiniYamlNode("Root", FieldSaver.Save(this))
new("Root", FieldSaver.Save(this))
};
for (var i = 0; i < Players.Count; i++)
@@ -118,11 +121,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 +147,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 +170,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>
@@ -33,7 +33,7 @@ namespace OpenRA
/// </summary>
public readonly string Name;
readonly TypeDictionary traits = new();
List<TraitInfo> constructOrderCache = null;
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

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

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,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 +44,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,13 +60,18 @@ 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 };
}

View File

@@ -77,11 +77,12 @@ namespace OpenRA.Graphics
cachedPanelSprites = new Dictionary<string, Sprite[]>();
cachedCollectionSheets = new Dictionary<Collection, (Sheet, int)>();
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var chrome = MiniYaml.Merge(modData.Manifest.Chrome
.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s)));
.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool)));
foreach (var c in chrome)
if (!c.Key.StartsWith("^", StringComparison.Ordinal))
if (!c.Key.StartsWith('^'))
LoadCollection(c.Key, c.Value);
}
@@ -227,14 +228,18 @@ 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())
if (collection.Regions.Count == 0)
return Array.Empty<Sprite>();
// Support manual definitions for unusual dialog layouts

View File

@@ -11,11 +11,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Primitives;
namespace OpenRA.Graphics
{
public sealed class CursorManager
public sealed class CursorManager : IDisposable
{
sealed class Cursor
{
@@ -38,13 +39,16 @@ namespace OpenRA.Graphics
readonly bool hardwareCursorsDisabled = false;
bool hardwareCursorsDoubled = false;
public CursorManager(CursorProvider cursorProvider)
public CursorManager(CursorProvider cursorProvider, int cursorSheetSize)
{
hardwareCursorsDisabled = Game.Settings.Graphics.DisableHardwareCursors;
graphicSettings = Game.Settings.Graphics;
sheetBuilder = new SheetBuilder(SheetType.BGRA);
foreach (var kv in cursorProvider.Cursors)
sheetBuilder = new SheetBuilder(SheetType.BGRA, cursorSheetSize);
// Sort the cursors for better packing onto the sheet.
foreach (var kv in cursorProvider.Cursors
.OrderBy(kvp => kvp.Value.Frames.Max(f => f.Size.Height)))
{
var frames = kv.Value.Frames;
var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? cursorProvider.Palettes[kv.Value.Palette] : null;
@@ -91,10 +95,6 @@ namespace OpenRA.Graphics
}
CreateOrUpdateHardwareCursors();
foreach (var s in sheetBuilder.AllSheets)
s.ReleaseBuffer();
Update();
}
@@ -128,6 +128,8 @@ namespace OpenRA.Graphics
}
}
sheetBuilder.Current.ReleaseBuffer();
hardwareCursorsDoubled = graphicSettings.CursorDouble;
}
@@ -229,6 +231,10 @@ namespace OpenRA.Graphics
var width = frame.Size.Width;
var height = frame.Size.Height;
if (width == 0 || height == 0)
return Array.Empty<byte>();
var data = new byte[4 * width * height];
unsafe
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,13 +20,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);
}
@@ -83,16 +83,18 @@ namespace OpenRA
public interface IGraphicsContext : IDisposable
{
IVertexBuffer<Vertex> CreateVertexBuffer(int size);
Vertex[] CreateVertices(int size);
IVertexBuffer<T> CreateVertexBuffer<T>(int size) where T : struct;
T[] CreateVertices<T>(int size) where T : struct;
IIndexBuffer CreateIndexBuffer(uint[] indices);
ITexture CreateTexture();
IFrameBuffer CreateFrameBuffer(Size s);
IFrameBuffer CreateFrameBuffer(Size s, Color clearColor);
IShader CreateShader(string name);
IShader CreateShader(IShaderBindings shaderBindings);
void EnableScissor(int x, int y, int width, int height);
void DisableScissor();
void Present();
void DrawPrimitives(PrimitiveType pt, int firstVertex, int numVertices);
void DrawElements(int numIndices, int offset);
void Clear();
void EnableDepthBuffer();
void DisableDepthBuffer();
@@ -102,7 +104,14 @@ namespace OpenRA
string GLVersion { get; }
}
public interface IVertexBuffer<T> : IDisposable
public interface IRenderer
{
void BeginFrame();
void EndFrame();
void SetPalette(HardwarePalette palette);
}
public interface IVertexBuffer<T> : IDisposable where T : struct
{
void Bind();
void SetData(T[] vertices, int length);
@@ -114,6 +123,11 @@ namespace OpenRA
void SetData(T[] vertices, int offset, int start, int length);
}
public interface IIndexBuffer : IDisposable
{
void Bind();
}
public interface IShader
{
void SetBool(string name, bool value);
@@ -124,6 +138,17 @@ namespace OpenRA
void SetTexture(string param, ITexture texture);
void SetMatrix(string param, float[] mtx);
void PrepareRender();
void Bind();
}
public interface IShaderBindings
{
string VertexShaderName { get; }
string VertexShaderCode { get; }
string FragmentShaderName { get; }
string FragmentShaderCode { get; }
int Stride { get; }
ShaderVertexAttribute[] Attributes { get; }
}
public enum TextureScaleFilter { Nearest, Linear }
@@ -132,6 +157,7 @@ namespace OpenRA
{
void SetData(byte[] colors, int width, int height);
void SetFloatData(float[] data, int width, int height);
void SetDataFromReadBuffer(Rectangle rect);
byte[] GetData();
Size Size { get; }
TextureScaleFilter ScaleFilter { get; set; }

View File

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

View File

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

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

View File

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

View File

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

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

@@ -14,28 +14,30 @@ using System.Collections.Generic;
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<
int,
(int[] Frames, MiniYamlNode.SourceLocation Location, AdjustFrame AdjustFrame, bool Premultiplied)> spriteReservations = new();
readonly Dictionary<string, List<int>> reservationsByFilename = new();
readonly Dictionary<int, ISpriteFrame[]> resolvedFrames = new();
readonly Dictionary<int, Sprite[]> resolvedSprites = new();
readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = new();
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,97 +49,110 @@ namespace OpenRA.Graphics
this.loaders = loaders;
}
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location)
public int ReserveSprites(string filename, IEnumerable<int> frames, MiniYamlNode.SourceLocation location,
AdjustFrame adjustFrame = null, bool premultiplied = false)
{
var token = nextReservationToken++;
spriteReservations[token] = (frames?.ToArray(), location);
spriteReservations[token] = (frames?.ToArray(), location, adjustFrame, premultiplied);
reservationsByFilename.GetOrAdd(filename, _ => new List<int>()).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 ?? Enumerable.Range(0, loadedFrames.Length);
var total = 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();
@@ -145,18 +160,11 @@ namespace OpenRA.Graphics
public Sprite[] ResolveSprites(int token)
{
var resolved = resolvedSprites[token];
resolvedSprites.Remove(token);
if (missingFiles.TryGetValue(token, out var r))
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);
if (!resolvedSprites.Remove(token, out var resolved))
throw new InvalidOperationException($"{nameof(token)} {token} has either already been resolved, or was never reserved via {nameof(ReserveSprites)}");
return resolved;
}
resolvedSprites.TrimExcess();
public ISpriteFrame[] ResolveFrames(int token)
{
var resolved = resolvedFrames[token];
resolvedFrames.Remove(token);
if (missingFiles.TryGetValue(token, out var r))
throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename);

View File

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

@@ -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 = new();
readonly IndexBufferRc indexBufferWrapper;
public readonly BlendMode BlendMode;
@@ -28,7 +31,8 @@ namespace OpenRA.Graphics
readonly Vertex[] vertices;
readonly bool[] ignoreTint;
readonly HashSet<int> dirtyRows = new();
readonly int rowStride;
readonly int indexRowStride;
readonly int vertexRowStride;
readonly bool restrictToBounds;
readonly WorldRenderer worldRenderer;
@@ -43,19 +47,25 @@ namespace OpenRA.Graphics
this.emptySprite = emptySprite;
sheets = new Sheet[SpriteRenderer.SheetCount];
BlendMode = blendMode;
map = world.Map;
rowStride = 6 * map.MapSize.X;
vertices = new Vertex[rowStride * map.MapSize.Y];
vertexRowStride = 4 * map.MapSize.X;
vertices = new Vertex[vertexRowStride * map.MapSize.Y];
vertexBuffer = Game.Renderer.Context.CreateVertexBuffer<Vertex>(vertices.Length);
indexRowStride = 6 * map.MapSize.X;
lock (IndexBuffers)
{
indexBufferWrapper = IndexBuffers.GetValue(world, world => new IndexBufferRc(world));
indexBufferWrapper.AddRef();
}
palettes = new PaletteReference[map.MapSize.X * map.MapSize.Y];
vertexBuffer = Game.Renderer.Context.CreateVertexBuffer(vertices.Length);
wr.PaletteInvalidated += UpdatePaletteIndices;
if (wr.TerrainLighting != null)
{
ignoreTint = new bool[rowStride * map.MapSize.Y];
ignoreTint = new bool[vertexRowStride * map.MapSize.Y];
wr.TerrainLighting.CellChanged += UpdateTint;
}
}
@@ -65,8 +75,9 @@ namespace OpenRA.Graphics
for (var i = 0; i < vertices.Length; i++)
{
var v = vertices[i];
var p = palettes[i / 6]?.TextureIndex ?? 0;
vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, p, v.C, v.R, v.G, v.B, v.A);
var p = palettes[i / 4]?.TextureIndex ?? 0;
var c = (uint)((p & 0xFFFF) << 16) | (v.C & 0xFFFF);
vertices[i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, c, v.R, v.G, v.B, v.A);
}
for (var row = 0; row < map.MapSize.Y; row++)
@@ -97,13 +108,13 @@ namespace OpenRA.Graphics
void UpdateTint(MPos uv)
{
var offset = rowStride * uv.V + 6 * uv.U;
var offset = vertexRowStride * uv.V + 4 * uv.U;
if (ignoreTint[offset])
{
for (var i = 0; i < 6; i++)
for (var i = 0; i < 4; i++)
{
var v = vertices[offset + i];
vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.P, v.C, v.A * float3.Ones, v.A);
vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.C, v.A * float3.Ones, v.A);
}
return;
@@ -125,10 +136,10 @@ namespace OpenRA.Graphics
// Apply tint directly to the underlying vertices
// This saves us from having to re-query the sprite information, which has not changed
for (var i = 0; i < 6; i++)
for (var i = 0; i < 4; i++)
{
var v = vertices[offset + i];
vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.P, v.C, v.A * weights[CornerVertexMap[i]], v.A);
vertices[offset + i] = new Vertex(v.X, v.Y, v.Z, v.S, v.T, v.U, v.V, v.C, v.A * weights[i], v.A);
}
dirtyRows.Add(uv.V);
@@ -180,7 +191,7 @@ namespace OpenRA.Graphics
if (!map.Tiles.Contains(uv))
return;
var offset = rowStride * uv.V + 6 * uv.U;
var offset = vertexRowStride * uv.V + 4 * uv.U;
Util.FastCreateQuad(vertices, pos, sprite, samplers, palette?.TextureIndex ?? 0, offset, scale * sprite.Size, alpha * float3.Ones, alpha);
palettes[uv.V * map.MapSize.X + uv.U] = palette;
@@ -209,13 +220,13 @@ namespace OpenRA.Graphics
if (!dirtyRows.Remove(row))
continue;
var rowOffset = rowStride * row;
vertexBuffer.SetData(vertices, rowOffset, rowOffset, rowStride);
var rowOffset = vertexRowStride * row;
vertexBuffer.SetData(vertices, rowOffset, rowOffset, vertexRowStride);
}
Game.Renderer.WorldSpriteRenderer.DrawVertexBuffer(
vertexBuffer, rowStride * firstRow, rowStride * (lastRow - firstRow),
PrimitiveType.TriangleList, sheets, BlendMode);
vertexBuffer, indexBufferWrapper.Buffer, indexRowStride * firstRow,
indexRowStride * (lastRow - firstRow), sheets, BlendMode);
Game.Renderer.Flush();
}
@@ -227,6 +238,29 @@ namespace OpenRA.Graphics
worldRenderer.TerrainLighting.CellChanged -= UpdateTint;
vertexBuffer.Dispose();
lock (IndexBuffers)
indexBufferWrapper.Dispose();
}
sealed class IndexBufferRc : IDisposable
{
public IIndexBuffer Buffer;
int count;
public IndexBufferRc(World world)
{
Buffer = Game.Renderer.Context.CreateIndexBuffer(Util.CreateQuadIndices(world.Map.MapSize.X * world.Map.MapSize.Y));
}
public void AddRef() { count++; }
public void Dispose()
{
count--;
if (count == 0)
Buffer.Dispose();
}
}
}
}

View File

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

View File

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

View File

@@ -215,7 +215,9 @@ namespace OpenRA.Graphics
MinZoom = CalculateMinimumZoom(range.X, range.Y) * viewportSizes.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 * viewportSizes.DefaultScale / viewportSizes.MaxZoomWindowHeight);
if (unlockMinZoom)
{
@@ -231,11 +233,12 @@ 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)
@@ -282,11 +285,23 @@ namespace OpenRA.Graphics
IEnumerable<MPos> CandidateMouseoverCells(int2 world)
{
var map = worldRenderer.World.Map;
var tileScale = map.Grid.TileScale / 2;
var minPos = worldRenderer.ProjectedPosition(world);
// Find all the cells that could potentially have been clicked
var a = map.CellContaining(minPos - new WVec(1024, 0, 0)).ToMPos(map.Grid.Type);
var b = map.CellContaining(minPos + new WVec(512, 512 * map.Grid.MaximumTerrainHeight, 0)).ToMPos(map.Grid.Type);
// Find all the cells that could potentially have been clicked.
MPos a;
MPos b;
if (map.Grid.Type == MapGridType.RectangularIsometric)
{
// TODO: this generates too many cells.
a = map.CellContaining(minPos - new WVec(tileScale, 0, 0)).ToMPos(map.Grid.Type);
b = map.CellContaining(minPos + new WVec(tileScale, tileScale * map.Grid.MaximumTerrainHeight, 0)).ToMPos(map.Grid.Type);
}
else
{
a = map.CellContaining(minPos).ToMPos(map.Grid.Type);
b = map.CellContaining(minPos + new WVec(0, tileScale * map.Grid.MaximumTerrainHeight, 0)).ToMPos(map.Grid.Type);
}
for (var v = b.V; v >= a.V; v--)
for (var u = b.U; u >= a.U; u--)
@@ -299,10 +314,13 @@ namespace OpenRA.Graphics
public void Center(IEnumerable<Actor> actors)
{
if (!actors.Any())
var actorsCollection = actors as IReadOnlyCollection<Actor>;
actorsCollection ??= actors.ToList();
if (actorsCollection.Count == 0)
return;
Center(actors.Select(a => a.CenterPosition).Average());
Center(actorsCollection.Select(a => a.CenterPosition).Average());
}
public void Center(WPos pos)

View File

@@ -44,6 +44,8 @@ namespace OpenRA.Graphics
readonly List<IFinalizedRenderable> preparedAnnotationRenderables = new();
readonly List<IRenderable> renderablesBuffer = new();
readonly IRenderer[] renderers;
readonly IRenderPostProcessPass[] postProcessPasses;
internal WorldRenderer(ModData modData, World world)
{
@@ -60,15 +62,29 @@ namespace OpenRA.Graphics
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>());
postProcessPasses = world.WorldActor.TraitsImplementing<IRenderPostProcessPass>().ToArray();
}
public void BeginFrame()
{
foreach (var r in renderers)
r.BeginFrame();
}
public void EndFrame()
{
foreach (var r in renderers)
r.EndFrame();
}
public void UpdatePalettesForPlayer(string internalName, Color color, bool replaceExisting)
@@ -270,6 +286,8 @@ namespace OpenRA.Graphics
if (enableDepthBuffer)
Game.Renderer.ClearDepthBuffer();
ApplyPostProcessing(PostProcessPassType.AfterActors);
World.ApplyToActorsWithTrait<IRenderAboveWorld>((actor, trait) =>
{
if (actor.IsInWorld && !actor.Disposed)
@@ -279,6 +297,8 @@ namespace OpenRA.Graphics
if (enableDepthBuffer)
Game.Renderer.ClearDepthBuffer();
ApplyPostProcessing(PostProcessPassType.AfterWorld);
World.ApplyToActorsWithTrait<IRenderShroud>((actor, trait) => trait.RenderShroud(this));
if (enableDepthBuffer)
@@ -292,9 +312,23 @@ namespace OpenRA.Graphics
foreach (var r in g)
r.Render(this);
ApplyPostProcessing(PostProcessPassType.AfterShroud);
Game.Renderer.Flush();
}
void ApplyPostProcessing(PostProcessPassType type)
{
foreach (var pass in postProcessPasses)
{
if (pass.Type != type || !pass.Enabled)
continue;
Game.Renderer.Flush();
pass.Draw(this);
}
}
public void DrawAnnotations()
{
Game.Renderer.EnableAntialiasingFilter();

View File

@@ -16,11 +16,19 @@ 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();
[FluentReference]
public readonly HashSet<string> Contexts = new();
public readonly bool Readonly = false;
public bool HasDuplicates { get; internal set; }
@@ -31,29 +39,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<HashSet<string>>("Types", typesYaml.Value);
var platformNode = node.Nodes.FirstOrDefault(n => n.Key == "Platform");
if (platformNode != null)
if (nodeDict.TryGetValue("Contexts", out var contextYaml))
Contexts = FieldLoader.GetValue<HashSet<string>>("Contexts", contextYaml.Value)
.Select(c => ContextFluentPrefix + "." + c).ToHashSet();
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

@@ -91,16 +91,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
{
@@ -61,6 +62,33 @@ namespace OpenRA
Meta = 8,
}
public static class ModifiersExts
{
[FluentReference]
public 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

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

@@ -43,25 +43,22 @@ namespace OpenRA
}
}
public sealed class ModelSequenceFormat : IGlobalModData
{
public readonly string Type;
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
public ModelSequenceFormat(MiniYaml yaml)
{
Type = yaml.Value;
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
}
}
public class ModMetadata
{
public string Title;
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]
readonly string Title;
public readonly string Version;
public readonly string Website;
public readonly string WebIcon32;
[FluentReference]
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;
}
/// <summary>Describes what is to be loaded in order to run a mod.</summary>
@@ -72,27 +69,34 @@ namespace OpenRA
public readonly ModMetadata Metadata;
public readonly 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 MiniYaml FileSystem;
public readonly MiniYaml LoadScreen;
public readonly string DefaultOrderGenerator;
public readonly string[] Assemblies = Array.Empty<string>();
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 int FontSheetSize = 512;
public readonly int CursorSheetSize = 512;
// TODO: This should be controlled by a user-selected translation bundle!
public readonly string FluentCulture = "en";
public readonly bool AllowUnusedFluentMessagesInExternalPackages = true;
readonly string[] reservedModuleNames =
{
"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"
"RequiresMods", "PackageFormats", "AllowUnusedFluentMessagesInExternalPackages", "FontSheetSize", "CursorSheetSize"
};
readonly TypeDictionary modules = new();
@@ -105,7 +109,8 @@ namespace OpenRA
Id = modId;
Package = package;
var nodes = MiniYaml.FromStream(package.GetStream("mod.yaml"), "mod.yaml");
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var nodes = MiniYaml.FromStream(package.GetStream("mod.yaml"), $"{package.Name}:mod.yaml", stringPool: stringPool);
for (var i = nodes.Count - 1; i >= 0; i--)
{
if (nodes[i].Key != "Include")
@@ -118,7 +123,7 @@ namespace OpenRA
throw new YamlException($"{nodes[i].Location}: File `{filename}` not found.");
nodes.RemoveAt(i);
nodes.InsertRange(i, MiniYaml.FromStream(contents, filename));
nodes.InsertRange(i, MiniYaml.FromStream(contents, $"{package.Name}:{filename}", stringPool: stringPool));
}
// Merge inherited overrides
@@ -129,21 +134,20 @@ namespace OpenRA
// 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");
@@ -165,6 +169,9 @@ namespace OpenRA
if (yaml.TryGetValue("DefaultOrderGenerator", out entry))
DefaultOrderGenerator = entry.Value;
if (yaml.TryGetValue("Assemblies", out entry))
Assemblies = FieldLoader.GetValue<string[]>("Assemblies", entry.Value);
if (yaml.TryGetValue("PackageFormats", out entry))
PackageFormats = FieldLoader.GetValue<string[]>("PackageFormats", entry.Value);
@@ -176,6 +183,16 @@ namespace OpenRA
if (yaml.TryGetValue("VideoFormats", out entry))
VideoFormats = FieldLoader.GetValue<string[]>("VideoFormats", entry.Value);
if (yaml.TryGetValue("AllowUnusedFluentMessagesInExternalPackages", out entry))
AllowUnusedFluentMessagesInExternalPackages =
FieldLoader.GetValue<bool>("AllowUnusedFluentMessagesInExternalPackages", entry.Value);
if (yaml.TryGetValue("FontSheetSize", out entry))
FontSheetSize = FieldLoader.GetValue<int>("FontSheetSize", entry.Value);
if (yaml.TryGetValue("CursorSheetSize", out entry))
CursorSheetSize = FieldLoader.GetValue<int>("CursorSheetSize", entry.Value);
}
public void LoadCustomData(ObjectCreator oc)
@@ -211,18 +228,18 @@ namespace OpenRA
static string[] YamlList(Dictionary<string, MiniYaml> yaml, string key)
{
if (!yaml.ContainsKey(key))
if (!yaml.TryGetValue(key, out var value))
return Array.Empty<string>();
return yaml[key].ToDictionary().Keys.ToArray();
return value.Nodes.Select(n => n.Key).ToArray();
}
static IReadOnlyDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
{
if (!yaml.ContainsKey(key))
if (!yaml.TryGetValue(key, out var value))
return new Dictionary<string, string>();
return yaml[key].ToDictionary(my => my.Value);
return value.ToDictionary(my => my.Value);
}
public bool Contains<T>() where T : IGlobalModData

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -90,13 +91,13 @@ namespace OpenRA
throw new InvalidOperationException("Map does not have a field/property " + fieldName);
var t = field != null ? field.FieldType : property.PropertyType;
type = t == typeof(List<MiniYamlNode>) ? Type.NodeList :
type = t == typeof(IReadOnlyCollection<MiniYamlNode>) ? Type.NodeList :
t == typeof(MiniYaml) ? Type.MiniYaml : Type.Normal;
}
public void Deserialize(Map map, List<MiniYamlNode> nodes)
public void Deserialize(Map map, MiniYaml yaml)
{
var node = nodes.FirstOrDefault(n => n.Key == key);
var node = yaml.NodeWithKeyOrDefault(key);
if (node == null)
{
if (required)
@@ -130,14 +131,14 @@ namespace OpenRA
var value = field != null ? field.GetValue(map) : property.GetValue(map, null);
if (type == Type.NodeList)
{
var listValue = (List<MiniYamlNode>)value;
var listValue = (IReadOnlyCollection<MiniYamlNode>)value;
if (required || listValue.Count > 0)
nodes.Add(new MiniYamlNode(key, null, listValue));
}
else if (type == Type.MiniYaml)
{
var yamlValue = (MiniYaml)value;
if (required || (yamlValue != null && (yamlValue.Value != null || yamlValue.Nodes.Count > 0)))
if (required || (yamlValue != null && (yamlValue.Value != null || yamlValue.Nodes.Length > 0)))
nodes.Add(new MiniYamlNode(key, yamlValue));
}
else
@@ -158,26 +159,26 @@ namespace OpenRA
/// <summary>Defines the order of the fields in map.yaml.</summary>
static readonly MapField[] YamlFields =
{
new MapField("MapFormat"),
new MapField("RequiresMod"),
new MapField("Title"),
new MapField("Author"),
new MapField("Tileset"),
new MapField("MapSize"),
new MapField("Bounds"),
new MapField("Visibility"),
new MapField("Categories"),
new MapField("LockPreview", required: false, ignoreIfValue: "False"),
new MapField("Players", "PlayerDefinitions"),
new MapField("Actors", "ActorDefinitions"),
new MapField("Rules", "RuleDefinitions", required: false),
new MapField("Translations", "TranslationDefinitions", required: false),
new MapField("Sequences", "SequenceDefinitions", required: false),
new MapField("ModelSequences", "ModelSequenceDefinitions", required: false),
new MapField("Weapons", "WeaponDefinitions", required: false),
new MapField("Voices", "VoiceDefinitions", required: false),
new MapField("Music", "MusicDefinitions", required: false),
new MapField("Notifications", "NotificationDefinitions", required: false),
new("MapFormat"),
new("RequiresMod"),
new("Title"),
new("Author"),
new("Tileset"),
new("MapSize"),
new("Bounds"),
new("Visibility"),
new("Categories"),
new("LockPreview", required: false, ignoreIfValue: "False"),
new("Players", 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
@@ -197,18 +198,18 @@ namespace OpenRA
public int2 MapSize { get; private set; }
// Player and actor yaml. Public for access by the map importers and lint checks.
public List<MiniYamlNode> PlayerDefinitions = new();
public List<MiniYamlNode> ActorDefinitions = new();
public IReadOnlyCollection<MiniYamlNode> PlayerDefinitions = ImmutableArray<MiniYamlNode>.Empty;
public IReadOnlyCollection<MiniYamlNode> ActorDefinitions = ImmutableArray<MiniYamlNode>.Empty;
// Custom map yaml. Public for access by the map importers and lint checks
public readonly MiniYaml RuleDefinitions;
public readonly MiniYaml TranslationDefinitions;
public readonly MiniYaml SequenceDefinitions;
public readonly MiniYaml ModelSequenceDefinitions;
public readonly MiniYaml WeaponDefinitions;
public readonly MiniYaml VoiceDefinitions;
public readonly MiniYaml MusicDefinitions;
public readonly MiniYaml NotificationDefinitions;
public MiniYaml RuleDefinitions;
public MiniYaml 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();
@@ -274,7 +275,10 @@ namespace OpenRA
try
{
foreach (var filename in contents)
if (filename.EndsWith(".yaml") || filename.EndsWith(".bin") || filename.EndsWith(".lua") || (format >= 12 && filename == "map.png"))
if (filename.EndsWith(".yaml", StringComparison.Ordinal) ||
filename.EndsWith(".bin", StringComparison.Ordinal) ||
filename.EndsWith(".lua", StringComparison.Ordinal) ||
(format >= 12 && filename == "map.png"))
streams.Add(package.GetStream(filename));
// Take the SHA1
@@ -357,15 +361,15 @@ namespace OpenRA
if (!Package.Contains("map.yaml") || !Package.Contains("map.bin"))
throw new InvalidDataException($"Not a valid map\n File: {package.Name}");
var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), package.Name));
var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), $"{package.Name}:map.yaml"));
foreach (var field in YamlFields)
field.Deserialize(this, yaml.Nodes);
field.Deserialize(this, yaml);
if (MapFormat < SupportedMapFormat)
throw new InvalidDataException($"Map format {MapFormat} is not supported.\n File: {package.Name}");
PlayerDefinitions = MiniYaml.NodesOrEmpty(yaml, "Players");
ActorDefinitions = MiniYaml.NodesOrEmpty(yaml, "Actors");
PlayerDefinitions = yaml.NodeWithKeyOrDefault("Players")?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty;
ActorDefinitions = yaml.NodeWithKeyOrDefault("Actors")?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty;
Grid = modData.Manifest.Get<MapGrid>();
@@ -607,7 +611,7 @@ namespace OpenRA
// Odd-height ramps get bumped up a level to the next even height layer
if ((height & 1) == 1 && Ramp[uv] != 0)
height += 1;
height++;
var candidates = new List<PPos>();
@@ -646,21 +650,24 @@ namespace OpenRA
foreach (var file in Package.Contents)
toPackage.Update(file, Package.GetStream(file).ReadAllBytes());
if (!LockPreview)
void UpdatePackage(string filename, byte[] data)
{
var previewData = SavePreview();
if (Package != toPackage || !Enumerable.SequenceEqual(previewData, Package.GetStream("map.png").ReadAllBytes()))
toPackage.Update("map.png", previewData);
if (Package != toPackage)
toPackage.Update(filename, data);
else
{
var stream = Package.GetStream(filename);
if (stream == null || !Enumerable.SequenceEqual(data, stream.ReadAllBytes()))
toPackage.Update(filename, data);
}
}
// Update the package with the new map data
var textData = Encoding.UTF8.GetBytes(root.WriteToString());
if (Package != toPackage || !Enumerable.SequenceEqual(textData, Package.GetStream("map.yaml").ReadAllBytes()))
toPackage.Update("map.yaml", textData);
if (!LockPreview)
UpdatePackage("map.png", SavePreview());
var binaryData = SaveBinaryData();
if (Package != toPackage || !Enumerable.SequenceEqual(binaryData, Package.GetStream("map.bin").ReadAllBytes()))
toPackage.Update("map.bin", binaryData);
// Update the package with the new map data
UpdatePackage("map.yaml", Encoding.UTF8.GetBytes(root.WriteToString()));
UpdatePackage("map.bin", SaveBinaryData());
Package = toPackage;
@@ -681,16 +688,16 @@ namespace OpenRA
writer.Write((ushort)MapSize.Y);
// Data offsets
var tilesOffset = 17;
const int TilesOffset = 17;
var heightsOffset = Grid.MaximumTerrainHeight > 0 ? 3 * MapSize.X * MapSize.Y + 17 : 0;
var resourcesOffset = (Grid.MaximumTerrainHeight > 0 ? 4 : 3) * MapSize.X * MapSize.Y + 17;
writer.Write((uint)tilesOffset);
writer.Write((uint)TilesOffset);
writer.Write((uint)heightsOffset);
writer.Write((uint)resourcesOffset);
// Tile data
if (tilesOffset != 0)
if (TilesOffset != 0)
{
for (var i = 0; i < MapSize.X; i++)
{
@@ -772,19 +779,10 @@ namespace OpenRA
if (Grid.MaximumTerrainHeight > 0)
{
// The minimap is drawn in cell space, so we need to
// unproject the PPos bounds to find the MPos boundaries.
// This matches the calculation in RadarWidget that is used ingame
for (var x = Bounds.Left; x < Bounds.Right; x++)
{
var allTop = Unproject(new PPos(x, Bounds.Top));
var allBottom = Unproject(new PPos(x, Bounds.Bottom));
if (allTop.Count > 0)
top = Math.Min(top, allTop.MinBy(uv => uv.V).V);
(top, bottom) = GetCellSpaceBounds();
if (allBottom.Count > 0)
bottom = Math.Max(bottom, allBottom.MaxBy(uv => uv.V).V);
}
if (top == int.MaxValue || bottom == int.MinValue)
throw new InvalidDataException("The map has invalid boundaries");
}
else
{
@@ -801,7 +799,7 @@ namespace OpenRA
bitmapWidth = 2 * bitmapWidth - 1;
var stride = bitmapWidth * 4;
var pxStride = 4;
const int PxStride = 4;
var minimapData = new byte[stride * height];
(Color Left, Color Right) terrainColor = default;
@@ -823,10 +821,10 @@ namespace OpenRA
{
// Odd rows are shifted right by 1px
var dx = uv.V & 1;
var xOffset = pxStride * (2 * x + dx);
var xOffset = PxStride * (2 * x + dx);
if (x + dx > 0)
{
var z = y * stride + xOffset - pxStride;
var z = y * stride + xOffset - PxStride;
var c = actorColor.A == 0 ? terrainColor.Left : actorColor;
minimapData[z++] = c.R;
minimapData[z++] = c.G;
@@ -846,7 +844,7 @@ namespace OpenRA
}
else
{
var z = y * stride + pxStride * x;
var z = y * stride + PxStride * x;
var c = actorColor.A == 0 ? terrainColor.Left : actorColor;
minimapData[z++] = c.R;
minimapData[z++] = c.G;
@@ -860,6 +858,28 @@ namespace OpenRA
return png.Save();
}
public (int Top, int Bottom) GetCellSpaceBounds()
{
var top = int.MaxValue;
var bottom = int.MinValue;
// The minimap is drawn in cell space, so we need to
// unproject the PPos bounds to find the MPos boundaries.
// This matches the calculation in RadarWidget that is used ingame
for (var x = Bounds.Left; x < Bounds.Right; x++)
{
var allTop = Unproject(new PPos(x, Bounds.Top));
var allBottom = Unproject(new PPos(x, Bounds.Bottom));
if (allTop.Count > 0)
top = Math.Min(top, allTop.MinBy(uv => uv.V).V);
if (allBottom.Count > 0)
bottom = Math.Max(bottom, allBottom.MaxBy(uv => uv.V).V);
}
return (top, bottom);
}
public bool Contains(CPos cell)
{
if (Grid.Type == MapGridType.RectangularIsometric)
@@ -1180,7 +1200,7 @@ namespace OpenRA
// Project this guessed cell and take the first available cell
// If it is projected outside the layer, then make another guess.
var allProjected = ProjectedCellsCovering(uv);
var projected = allProjected.Length > 0 ? allProjected.First()
var projected = allProjected.Length > 0 ? allProjected[0]
: new PPos(uv.U, uv.V.Clamp(Bounds.Top, Bounds.Bottom));
// Clamp the projected cell to the map area
@@ -1249,7 +1269,7 @@ namespace OpenRA
PPos edge;
if (allProjected.Length > 0)
{
var puv = allProjected.First();
var puv = allProjected[0];
var horizontalBound = (puv.U - Bounds.Left < Bounds.Width / 2) ? Bounds.Left : Bounds.Right;
var verticalBound = (puv.V - Bounds.Top < Bounds.Height / 2) ? Bounds.Top : Bounds.Bottom;
@@ -1349,13 +1369,18 @@ namespace OpenRA
throw new ArgumentOutOfRangeException(nameof(maxRange),
$"The requested range ({maxRange}) cannot exceed the value of MaximumTileSearchRange ({Grid.MaximumTileSearchRange})");
for (var i = minRange; i <= maxRange; i++)
return FindTilesInAnnulus();
IEnumerable<CPos> FindTilesInAnnulus()
{
foreach (var offset in Grid.TilesByDistance[i])
for (var i = minRange; i <= maxRange; i++)
{
var t = offset + center;
if (allowOutsideBounds ? Tiles.Contains(t) : Contains(t))
yield return t;
foreach (var offset in Grid.TilesByDistance[i])
{
var t = offset + center;
if (allowOutsideBounds ? Tiles.Contains(t) : Contains(t))
yield return t;
}
}
}
}
@@ -1402,11 +1427,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

@@ -14,6 +14,7 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using OpenRA.FileSystem;
@@ -38,7 +39,7 @@ namespace OpenRA
readonly object syncRoot = new();
readonly Queue<MapPreview> generateMinimap = new();
public Dictionary<string, string> StringPool { get; } = new Dictionary<string, string>();
public HashSet<string> StringPool { get; } = new();
readonly List<MapDirectoryTracker> mapDirectoryTrackers = new();
@@ -97,7 +98,7 @@ namespace OpenRA
? MapClassification.Unknown : Enum<MapClassification>.Parse(kv.Value);
IReadOnlyPackage package;
var optional = name.StartsWith("~", StringComparison.Ordinal);
var optional = name.StartsWith('~');
if (optional)
name = name[1..];
@@ -106,7 +107,7 @@ namespace OpenRA
// HACK: If the path is inside the support directory then we may need to create it
// Assume that the path is a directory if there is not an existing file with the same name
var resolved = Platform.ResolvePath(name);
if (resolved.StartsWith(Platform.SupportDir) && !File.Exists(resolved))
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && !File.Exists(resolved))
Directory.CreateDirectory(resolved);
package = modData.ModFiles.OpenPackage(name);
@@ -140,7 +141,8 @@ namespace OpenRA
LoadMapInternal(map, package, classification, mapGrid, oldMap, null);
}
void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap, IEnumerable<List<MiniYamlNode>> modDataRules)
void LoadMapInternal(string map, IReadOnlyPackage package, MapClassification classification, MapGrid mapGrid, string oldMap,
IEnumerable<List<MiniYamlNode>> modDataRules)
{
IReadOnlyPackage mapPackage = null;
try
@@ -153,6 +155,9 @@ namespace OpenRA
var uid = Map.ComputeUID(mapPackage);
previews[uid].UpdateFromMap(mapPackage, package, classification, modData.Manifest.MapCompatibility, mapGrid.Type, modDataRules);
// Freeing the package to save memory if there is a lot of Maps
previews[uid].DisposePackage();
if (oldMap != uid)
{
LastModifiedMap = uid;
@@ -190,13 +195,13 @@ namespace OpenRA
continue;
var name = kv.Key;
var optional = name.StartsWith("~", StringComparison.Ordinal);
var optional = name.StartsWith('~');
if (optional)
name = name[1..];
// Don't try to open the map directory in the support directory if it doesn't exist
var resolved = Platform.ResolvePath(name);
if (resolved.StartsWith(Platform.SupportDir) && (!Directory.Exists(resolved) || !File.Exists(resolved)))
if (resolved.StartsWith(Platform.SupportDir, StringComparison.Ordinal) && (!Directory.Exists(resolved) || !File.Exists(resolved)))
continue;
using (var package = (IReadWritePackage)modData.ModFiles.OpenPackage(name))
@@ -223,7 +228,8 @@ namespace OpenRA
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 +239,67 @@ namespace OpenRA
.ToList();
foreach (var uid in queryUids)
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null);
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null, null);
Task.Run(async () =>
{
var client = HttpClientFactory.Create();
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var buffer = new StringBuilder();
// Limit each query to 50 maps at a time to avoid request size limits
for (var i = 0; i < queryUids.Count; i += 50)
{
var batchUids = queryUids.Skip(i).Take(50).ToList();
var url = repositoryUrl + "hash/" + string.Join(",", batchUids) + "/yaml";
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);
await using (var resultStream = await client.GetStreamAsync(url))
{
using (var resultReader = new StreamReader(resultStream))
{
while (true)
{
var line = await resultReader.ReadLineAsync();
if (line == null || !line.StartsWith('\t'))
{
var yaml = MiniYaml.FromString(buffer.ToString(), url, stringPool: stringPool);
buffer.Clear();
foreach (var kv in yaml)
previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, modData.Manifest.MapCompatibility, mapDetailsReceived);
if (line == null)
break;
}
buffer.Append(line);
buffer.Append('\n');
}
}
}
foreach (var uid in batchUids)
{
var p = previews[uid];
if (p.Status != MapStatus.DownloadAvailable)
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null);
}
}
}
catch (Exception e)
{
Log.Write("debug", "Remote map query failed with error:");
Log.Write("debug", e);
Log.Write("debug", $"URL was: {url}");
foreach (var uid in batchUids)
catch (Exception e)
{
var p = previews[uid];
p.UpdateRemoteSearch(MapStatus.Unavailable, null);
mapQueryFailed?.Invoke(p);
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, null);
mapQueryFailed?.Invoke(p);
}
}
}
}
@@ -282,11 +311,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 +335,11 @@ namespace OpenRA
if (todo.Count == 0)
{
Thread.Sleep(emptyDelay);
Thread.Sleep(EmptyDelay);
continue;
}
else
keepAlive = maxKeepAlive;
keepAlive = MaxKeepAlive;
// Render the minimap into the shared sheet
foreach (var p in todo)
@@ -348,8 +377,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 +435,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

@@ -86,7 +86,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.PackageName == mapAction.Key && x.Status == MapStatus.Available);
if (map != null)
{
if (mapAction.Value == MapAction.Delete)

View File

@@ -116,12 +116,12 @@ namespace OpenRA
public readonly WVec[] SubCellOffsets =
{
new WVec(0, 0, 0), // full cell - index 0
new WVec(-299, -256, 0), // top left - index 1
new WVec(256, -256, 0), // top right - index 2
new WVec(0, 0, 0), // center - index 3
new WVec(-299, 256, 0), // bottom left - index 4
new WVec(256, 256, 0), // bottom right - index 5
new(0, 0, 0), // full cell - index 0
new(-299, -256, 0), // top left - index 1
new(256, -256, 0), // top right - index 2
new(0, 0, 0), // center - index 3
new(-299, 256, 0), // bottom left - index 4
new(256, 256, 0), // bottom right - index 5
};
public CellRamp[] Ramps { get; }

View File

@@ -59,6 +59,7 @@ namespace OpenRA
public readonly string rules;
public readonly string players_block;
public readonly int mapformat;
public readonly string game_mod;
}
public sealed class MapPreview : IDisposable, IReadOnlyFileSystem
@@ -89,8 +90,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; }
@@ -120,13 +122,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 = Array.Empty<string>();
if (FluentMessageDefinitions.Value != null)
files = FieldLoader.GetValue<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)
@@ -139,15 +160,22 @@ namespace OpenRA
files = files.Append(mapFiles);
}
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
var sources =
modDataRules.Select(x => x.Where(IsLoadableRuleDefinition).ToList())
.Concat(files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s).Where(IsLoadableRuleDefinition).ToList()));
if (RuleDefinitions.Nodes.Count > 0)
.Concat(files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool).Where(IsLoadableRuleDefinition).ToList()));
if (RuleDefinitions.Nodes.Length > 0)
sources = sources.Append(RuleDefinitions.Nodes.Where(IsLoadableRuleDefinition).ToList());
var yamlNodes = MiniYaml.Merge(sources);
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;
}
}
@@ -172,7 +200,19 @@ namespace OpenRA
readonly ModData modData;
public readonly string Uid;
public IReadOnlyPackage Package { get; private set; }
public string PackageName { get; private set; }
IReadOnlyPackage package;
public IReadOnlyPackage Package
{
get
{
package ??= parentPackage.OpenPackage(PackageName, modData.ModFiles);
return package;
}
private set => package = value;
}
IReadOnlyPackage parentPackage;
volatile InnerData innerData;
@@ -204,16 +244,31 @@ namespace OpenRA
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;
@@ -284,7 +339,7 @@ namespace OpenRA
cache = modData.MapCache;
Uid = map.Uid;
Package = map.Package;
PackageName = map.Package.Name;
var mapPlayers = new MapPlayers(map.PlayerDefinitions);
var spawns = new List<CPos>();
@@ -315,7 +370,7 @@ namespace OpenRA
innerData.SetCustomRules(modData, this, new Dictionary<string, MiniYaml>()
{
{ "Rules", map.RuleDefinitions },
{ "Translations", map.TranslationDefinitions },
{ "FluentMessages", map.FluentMessageDefinitions },
{ "Weapons", map.WeaponDefinitions },
{ "Voices", map.VoiceDefinitions },
{ "Music", map.MusicDefinitions },
@@ -325,7 +380,8 @@ namespace OpenRA
}, null);
}
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType, IEnumerable<List<MiniYamlNode>> modDataRules)
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification,
string[] mapCompatibility, MapGridType gridType, IEnumerable<List<MiniYamlNode>> modDataRules)
{
Dictionary<string, MiniYaml> yaml;
using (var yamlStream = p.GetStream("map.yaml"))
@@ -333,10 +389,10 @@ namespace OpenRA
if (yamlStream == null)
throw new FileNotFoundException("Required file map.yaml not present in this map");
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, "map.yaml", stringPool: cache.StringPool)).ToDictionary();
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, $"{p.Name}:map.yaml", stringPool: cache.StringPool)).ToDictionary();
}
Package = p;
PackageName = p.Name;
parentPackage = parent;
var newData = innerData.Clone();
@@ -427,7 +483,7 @@ namespace OpenRA
innerData = newData;
}
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action<MapPreview> parseMetadata = null)
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, string[] mapCompatibility, Action<MapPreview> parseMetadata = null)
{
var newData = innerData.Clone();
newData.Status = status;
@@ -474,11 +530,19 @@ namespace OpenRA
}
var playersString = Encoding.UTF8.GetString(Convert.FromBase64String(r.players_block));
newData.Players = new MapPlayers(MiniYaml.FromString(playersString));
newData.Players = new MapPlayers(MiniYaml.FromString(playersString,
$"{yaml.NodeWithKey(nameof(r.players_block)).Location.Name}:{nameof(r.players_block)}"));
var rulesString = Encoding.UTF8.GetString(Convert.FromBase64String(r.rules));
var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString)).ToDictionary();
var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString,
$"{yaml.NodeWithKey(nameof(r.rules)).Location.Name}:{nameof(r.rules)}")).ToDictionary();
newData.SetCustomRules(modData, this, rulesYaml, null);
// Map is for a different mod: update its information so it can be displayed
// in the cross-mod server browser UI, but mark it as unavailable so it can't
// be selected in a server for the current mod.
if (!mapCompatibility.Contains(r.game_mod))
newData.Status = MapStatus.Unavailable;
}
catch (Exception e)
{
@@ -577,10 +641,15 @@ namespace OpenRA
public void Dispose()
{
if (Package != null)
DisposePackage();
}
public void DisposePackage()
{
if (package != null)
{
Package.Dispose();
Package = null;
package.Dispose();
package = null;
}
}
@@ -627,11 +696,11 @@ namespace OpenRA
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,6 +9,7 @@
*/
#endregion
using System;
using OpenRA.Primitives;
namespace OpenRA
@@ -53,5 +54,15 @@ namespace OpenRA
{
return Bounds.Contains(uv.U, uv.V);
}
public int IndexOf(T value, int startIndex)
{
return Array.IndexOf(Entries, value, startIndex);
}
public void SetAll(T value)
{
Array.Fill(Entries, value);
}
}
}

View File

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

View File

@@ -20,18 +20,36 @@ namespace OpenRA
{
public static class MiniYamlExts
{
public static void WriteToFile(this List<MiniYamlNode> y, string filename)
public static void WriteToFile(this IEnumerable<MiniYamlNode> y, string filename)
{
File.WriteAllLines(filename, y.ToLines().Select(x => x.TrimEnd()).ToArray());
}
public static string WriteToString(this List<MiniYamlNode> y)
public static string WriteToString(this IEnumerable<MiniYamlNode> y)
{
// Remove all trailing newlines and restore the final EOF newline
return y.ToLines().JoinWith("\n").TrimEnd('\n') + "\n";
}
public static IEnumerable<string> ToLines(this List<MiniYamlNode> y)
public static IEnumerable<string> ToLines(this IEnumerable<MiniYamlNode> y)
{
foreach (var kv in y)
foreach (var line in kv.Value.ToLines(kv.Key, kv.Comment))
yield return line;
}
public static void WriteToFile(this IEnumerable<MiniYamlNodeBuilder> y, string filename)
{
File.WriteAllLines(filename, y.ToLines().Select(x => x.TrimEnd()).ToArray());
}
public static string WriteToString(this IEnumerable<MiniYamlNodeBuilder> y)
{
// Remove all trailing newlines and restore the final EOF newline
return y.ToLines().JoinWith("\n").TrimEnd('\n') + "\n";
}
public static IEnumerable<string> ToLines(this IEnumerable<MiniYamlNodeBuilder> y)
{
foreach (var kv in y)
foreach (var line in kv.Value.ToLines(kv.Key, kv.Comment))
@@ -43,22 +61,29 @@ namespace OpenRA
{
public readonly struct SourceLocation
{
public readonly string Filename;
public readonly string Name;
public readonly int Line;
public SourceLocation(string filename, int line)
public SourceLocation(string name, int line)
{
Filename = filename;
Name = name;
Line = line;
}
public override string ToString() { return $"{Filename}:{Line}"; }
public override string ToString() { return $"{Name}:{Line}"; }
}
public SourceLocation Location;
public string Key;
public MiniYaml Value;
public string Comment;
public readonly SourceLocation Location;
public readonly string Key;
public readonly MiniYaml Value;
public readonly string Comment;
public MiniYamlNode WithValue(MiniYaml value)
{
if (Value == value)
return this;
return new MiniYamlNode(Key, value, Comment, Location);
}
public MiniYamlNode(string k, MiniYaml v, string c = null)
{
@@ -74,26 +99,15 @@ namespace OpenRA
}
public MiniYamlNode(string k, string v, string c = null)
: this(k, v, c, null) { }
: this(k, new MiniYaml(v, Enumerable.Empty<MiniYamlNode>()), c) { }
public MiniYamlNode(string k, string v, List<MiniYamlNode> n)
public MiniYamlNode(string k, string v, IEnumerable<MiniYamlNode> n)
: this(k, new MiniYaml(v, n), null) { }
public MiniYamlNode(string k, string v, string c, List<MiniYamlNode> n)
: this(k, new MiniYaml(v, n), c) { }
public MiniYamlNode(string k, string v, string c, List<MiniYamlNode> n, SourceLocation loc)
: this(k, new MiniYaml(v, n), c, loc) { }
public override string ToString()
{
return $"{{YamlNode: {Key} @ {Location}}}";
}
public MiniYamlNode Clone()
{
return new MiniYamlNode(Key, Value.Clone(), Comment, Location);
}
}
public sealed class MiniYaml
@@ -101,15 +115,59 @@ namespace OpenRA
const int SpacesPerLevel = 4;
static readonly Func<string, string> StringIdentity = s => s;
static readonly Func<MiniYaml, MiniYaml> MiniYamlIdentity = my => my;
public string Value;
public List<MiniYamlNode> Nodes;
static readonly Dictionary<string, MiniYamlNode> ConflictScratch = new();
public MiniYaml Clone()
public readonly string Value;
public readonly ImmutableArray<MiniYamlNode> Nodes;
public MiniYaml WithValue(string value)
{
var clonedNodes = new List<MiniYamlNode>(Nodes.Count);
if (Value == value)
return this;
return new MiniYaml(value, Nodes);
}
public MiniYaml WithNodes(IEnumerable<MiniYamlNode> nodes)
{
if (nodes is ImmutableArray<MiniYamlNode> n && Nodes == n)
return this;
return new MiniYaml(Value, nodes);
}
public MiniYaml WithNodesAppended(IEnumerable<MiniYamlNode> nodes)
{
var newNodes = Nodes.AddRange(nodes);
if (Nodes == newNodes)
return this;
return new MiniYaml(Value, newNodes);
}
public MiniYamlNode NodeWithKey(string key)
{
var result = NodeWithKeyOrDefault(key);
if (result == null)
throw new InvalidDataException($"No node with key '{key}'");
return result;
}
public MiniYamlNode NodeWithKeyOrDefault(string key)
{
// PERF: Avoid LINQ.
var first = true;
MiniYamlNode result = null;
foreach (var node in Nodes)
clonedNodes.Add(node.Clone());
return new MiniYaml(Value, clonedNodes);
{
if (node.Key != key)
continue;
if (!first)
throw new InvalidDataException($"Duplicate key '{node.Key}' in {node.Location}");
first = false;
result = node;
}
return result;
}
public Dictionary<string, MiniYaml> ToDictionary()
@@ -125,7 +183,7 @@ namespace OpenRA
public Dictionary<TKey, TElement> ToDictionary<TKey, TElement>(
Func<string, TKey> keySelector, Func<MiniYaml, TElement> elementSelector)
{
var ret = new Dictionary<TKey, TElement>(Nodes.Count);
var ret = new Dictionary<TKey, TElement>(Nodes.Length);
foreach (var y in Nodes)
{
var key = keySelector(y.Key);
@@ -138,28 +196,27 @@ namespace OpenRA
}
public MiniYaml(string value)
: this(value, null) { }
: this(value, Enumerable.Empty<MiniYamlNode>()) { }
public MiniYaml(string value, List<MiniYamlNode> nodes)
public MiniYaml(string value, IEnumerable<MiniYamlNode> nodes)
{
Value = value;
Nodes = nodes ?? new List<MiniYamlNode>();
Nodes = ImmutableArray.CreateRange(nodes);
}
public static List<MiniYamlNode> NodesOrEmpty(MiniYaml y, string s)
static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string name, bool discardCommentsAndWhitespace, HashSet<string> stringPool)
{
var nd = y.ToDictionary();
return nd.TryGetValue(s, out var v) ? v.Nodes : new List<MiniYamlNode>();
}
// YAML config often contains repeated strings for key, values, comments.
// Pool these strings so we only need one copy of each unique string.
// This saves on long-term memory usage as parsed values can often live a long time.
// A caller can also provide a pool as input, allowing de-duplication across multiple parses.
stringPool ??= new HashSet<string>();
static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string filename, bool discardCommentsAndWhitespace, Dictionary<string, string> stringPool)
{
stringPool ??= new Dictionary<string, string>();
var levels = new List<List<MiniYamlNode>>
var result = new List<List<MiniYamlNode>>
{
new List<MiniYamlNode>()
new()
};
var parsedLines = new List<(int Level, string Key, string Value, string Comment, MiniYamlNode.SourceLocation Location)>();
var lineNo = 0;
foreach (var ll in lines)
@@ -175,7 +232,7 @@ namespace OpenRA
ReadOnlySpan<char> key = default;
ReadOnlySpan<char> value = default;
ReadOnlySpan<char> comment = default;
var location = new MiniYamlNode.SourceLocation(filename, lineNo);
var location = new MiniYamlNode.SourceLocation(name, lineNo);
if (line.Length > 0)
{
@@ -206,15 +263,6 @@ namespace OpenRA
}
}
if (levels.Count <= level)
throw new YamlException($"Bad indent in miniyaml at {location}");
while (levels.Count > level + 1)
{
levels[^1].TrimExcess();
levels.RemoveAt(levels.Count - 1);
}
// Extract key, value, comment from line as `<key>: <value>#<comment>`
// The # character is allowed in the value if escaped (\#).
// Leading and trailing whitespace is always trimmed from keys.
@@ -236,7 +284,7 @@ namespace OpenRA
if (commentStart < 0 && line[i] == '#' && (i == 0 || line[i - 1] != '\\'))
{
commentStart = i + 1;
if (commentStart <= keyLength)
if (i <= keyStart + keyLength)
keyLength = i - keyStart;
else
valueLength = i - valueStart;
@@ -274,46 +322,81 @@ namespace OpenRA
if (!key.IsEmpty || !discardCommentsAndWhitespace)
{
if (parsedLines.Count > 0 && parsedLines[^1].Level < level - 1)
throw new YamlException($"Bad indent in miniyaml at {location}");
while (parsedLines.Count > 0 && parsedLines[^1].Level > level)
BuildCompletedSubNode(level);
var keyString = key.IsEmpty ? null : key.ToString();
var valueString = value.IsEmpty ? null : value.ToString();
// Note: We need to support empty comments here to ensure that empty comments
// (i.e. a lone # at the end of a line) can be correctly re-serialized
var commentString = comment == default ? null : comment.ToString();
var commentString = comment == ReadOnlySpan<char>.Empty ? null : comment.ToString();
keyString = keyString == null ? null : stringPool.GetOrAdd(keyString, keyString);
valueString = valueString == null ? null : stringPool.GetOrAdd(valueString, valueString);
commentString = commentString == null ? null : stringPool.GetOrAdd(commentString, commentString);
keyString = keyString == null ? null : stringPool.GetOrAdd(keyString);
valueString = valueString == null ? null : stringPool.GetOrAdd(valueString);
commentString = commentString == null ? null : stringPool.GetOrAdd(commentString);
var nodes = new List<MiniYamlNode>();
levels[level].Add(new MiniYamlNode(keyString, valueString, commentString, nodes, location));
levels.Add(nodes);
parsedLines.Add((level, keyString, valueString, commentString, location));
}
}
foreach (var nodes in levels)
nodes.TrimExcess();
if (parsedLines.Count > 0)
BuildCompletedSubNode(0);
return levels[0];
return result[0];
void BuildCompletedSubNode(int level)
{
var lastLevel = parsedLines[^1].Level;
while (lastLevel >= result.Count)
result.Add(new List<MiniYamlNode>());
while (parsedLines.Count > 0 && parsedLines[^1].Level >= level)
{
var parent = parsedLines[^1];
var startOfRange = parsedLines.Count - 1;
while (startOfRange > 0 && parsedLines[startOfRange - 1].Level == parent.Level)
startOfRange--;
for (var i = startOfRange; i < parsedLines.Count - 1; i++)
{
var sibling = parsedLines[i];
result[parent.Level].Add(
new MiniYamlNode(sibling.Key, new MiniYaml(sibling.Value), sibling.Comment, sibling.Location));
}
var childNodes = parent.Level + 1 < result.Count ? result[parent.Level + 1] : null;
result[parent.Level].Add(new MiniYamlNode(
parent.Key,
new MiniYaml(parent.Value, childNodes ?? Enumerable.Empty<MiniYamlNode>()),
parent.Comment,
parent.Location));
childNodes?.Clear();
parsedLines.RemoveRange(startOfRange, parsedLines.Count - startOfRange);
}
}
}
public static List<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
public static List<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
{
return FromStream(File.OpenRead(path), path, discardCommentsAndWhitespace, stringPool);
}
public static List<MiniYamlNode> FromStream(Stream s, string fileName = "<no filename available>", bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
public static List<MiniYamlNode> FromStream(Stream s, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
{
return FromLines(s.ReadAllLinesAsMemory(), fileName, discardCommentsAndWhitespace, stringPool);
return FromLines(s.ReadAllLinesAsMemory(), name, discardCommentsAndWhitespace, stringPool);
}
public static List<MiniYamlNode> FromString(string text, string fileName = "<no filename available>", bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
public static List<MiniYamlNode> FromString(string text, string name, bool discardCommentsAndWhitespace = true, HashSet<string> stringPool = null)
{
return FromLines(text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(s => s.AsMemory()), fileName, discardCommentsAndWhitespace, stringPool);
return FromLines(text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(s => s.AsMemory()), name, discardCommentsAndWhitespace, stringPool);
}
public static List<MiniYamlNode> Merge(IEnumerable<List<MiniYamlNode>> sources)
public static List<MiniYamlNode> Merge(IEnumerable<IReadOnlyCollection<MiniYamlNode>> sources)
{
var sourcesList = sources.ToList();
if (sourcesList.Count == 0)
@@ -336,28 +419,41 @@ namespace OpenRA
}
// Resolve any top-level removals (e.g. removing whole actor blocks)
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)).ToList());
return ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty);
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)));
var result = ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty);
return result as List<MiniYamlNode> ?? result.ToList();
}
static void MergeIntoResolved(MiniYamlNode overrideNode, List<MiniYamlNode> existingNodes, HashSet<string> existingNodeKeys,
Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
{
if (existingNodeKeys.Add(overrideNode.Key))
var existingNodeIndex = -1;
MiniYamlNode existingNode = null;
if (!existingNodeKeys.Add(overrideNode.Key))
{
existingNodes.Add(overrideNode.Clone());
return;
existingNodeIndex = IndexOfKey(existingNodes, overrideNode.Key);
existingNode = existingNodes[existingNodeIndex];
}
var existingNode = existingNodes.Find(n => n.Key == overrideNode.Key);
existingNode.Value = MergePartial(existingNode.Value, overrideNode.Value);
existingNode.Value.Nodes = ResolveInherits(existingNode.Value, tree, inherited);
var value = MergePartial(existingNode?.Value, overrideNode.Value);
var nodes = ResolveInherits(value, tree, inherited);
if (!value.Nodes.SequenceEqual(nodes))
value = value.WithNodes(nodes);
if (existingNode != null)
existingNodes[existingNodeIndex] = existingNode.WithValue(value);
else
existingNodes.Add(overrideNode.WithValue(value));
}
static List<MiniYamlNode> ResolveInherits(MiniYaml node, Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
static IReadOnlyCollection<MiniYamlNode> ResolveInherits(
MiniYaml node, Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
{
var resolved = new List<MiniYamlNode>(node.Nodes.Count);
var resolvedKeys = new HashSet<string>(node.Nodes.Count);
if (node.Nodes.Length == 0)
return node.Nodes;
var resolved = new List<MiniYamlNode>(node.Nodes.Length);
var resolvedKeys = new HashSet<string>(node.Nodes.Length);
foreach (var n in node.Nodes)
{
@@ -373,13 +469,14 @@ namespace OpenRA
}
catch (ArgumentException)
{
throw new YamlException($"{n.Location}: Parent type `{n.Value.Value}` was already inherited by this yaml tree at {inherited[n.Value.Value]} (note: may be from a derived tree)");
throw new YamlException(
$"{n.Location}: Parent type `{n.Value.Value}` was already inherited by this yaml tree at {inherited[n.Value.Value]} (note: may be from a derived tree)");
}
foreach (var r in ResolveInherits(parent, tree, inherited))
MergeIntoResolved(r, resolved, resolvedKeys, tree, inherited);
}
else if (n.Key.StartsWith("-", StringComparison.Ordinal))
else if (n.Key.StartsWith('-'))
{
var removed = n.Key[1..];
if (resolved.RemoveAll(r => r.Key == removed) == 0)
@@ -390,7 +487,6 @@ namespace OpenRA
MergeIntoResolved(n, resolved, resolvedKeys, tree, inherited);
}
resolved.TrimExcess();
return resolved;
}
@@ -398,8 +494,11 @@ namespace OpenRA
/// Merges any duplicate keys that are defined within the same set of nodes.
/// Does not resolve inheritance or node removals.
/// </summary>
static List<MiniYamlNode> MergeSelfPartial(List<MiniYamlNode> existingNodes)
static IReadOnlyCollection<MiniYamlNode> MergeSelfPartial(IReadOnlyCollection<MiniYamlNode> existingNodes)
{
if (existingNodes.Count == 0)
return existingNodes;
var keys = new HashSet<string>(existingNodes.Count);
var ret = new List<MiniYamlNode>(existingNodes.Count);
foreach (var n in existingNodes)
@@ -409,19 +508,26 @@ namespace OpenRA
else
{
// Node with the same key has already been added: merge new node over the existing one
var original = ret.First(r => r.Key == n.Key);
original.Value = MergePartial(original.Value, n.Value);
var originalIndex = IndexOfKey(ret, n.Key);
var original = ret[originalIndex];
ret[originalIndex] = original.WithValue(MergePartial(original.Value, n.Value));
}
}
ret.TrimExcess();
return ret;
}
static MiniYaml MergePartial(MiniYaml existingNodes, MiniYaml overrideNodes)
{
existingNodes?.Nodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
overrideNodes?.Nodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
lock (ConflictScratch)
{
// PERF: Reuse ConflictScratch for all conflict checks to avoid allocations.
existingNodes?.Nodes.IntoDictionaryWithConflictLog(
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
overrideNodes?.Nodes.IntoDictionaryWithConflictLog(
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
ConflictScratch.Clear();
}
if (existingNodes == null)
return overrideNodes;
@@ -432,7 +538,7 @@ namespace OpenRA
return new MiniYaml(overrideNodes.Value ?? existingNodes.Value, MergePartial(existingNodes.Nodes, overrideNodes.Nodes));
}
static List<MiniYamlNode> MergePartial(List<MiniYamlNode> existingNodes, List<MiniYamlNode> overrideNodes)
static IReadOnlyCollection<MiniYamlNode> MergePartial(IReadOnlyCollection<MiniYamlNode> existingNodes, IReadOnlyCollection<MiniYamlNode> overrideNodes)
{
if (existingNodes.Count == 0)
return overrideNodes;
@@ -452,7 +558,7 @@ namespace OpenRA
{
// Append Removal nodes to the result.
// Therefore: we know the remainder of the method deals with a plain node.
if (node.Key.StartsWith("-", StringComparison.Ordinal))
if (node.Key.StartsWith('-'))
{
ret.Add(node);
return;
@@ -468,9 +574,8 @@ namespace OpenRA
// A Removal node is closer than the previous node.
// We should not merge the new node, as the data being merged will jump before the Removal.
// Instead, append it so the previous node is applied, then removed, then the new node is applied.
var removalKey = $"-{node.Key}";
var previousNodeIndex = ret.FindLastIndex(n => n.Key == node.Key);
var previousRemovalNodeIndex = ret.FindLastIndex(n => n.Key == removalKey);
var previousNodeIndex = LastIndexOfKey(ret, node.Key);
var previousRemovalNodeIndex = LastIndexOfKey(ret, $"-{node.Key}");
if (previousRemovalNodeIndex != -1 && previousRemovalNodeIndex > previousNodeIndex)
{
ret.Add(node);
@@ -479,13 +584,30 @@ namespace OpenRA
// A previous node is present with no intervening Removal.
// We should merge the new one into it, in place.
ret[previousNodeIndex] = new MiniYamlNode(node.Key, MergePartial(ret[previousNodeIndex].Value, node.Value), node.Comment, node.Location);
ret[previousNodeIndex] = node.WithValue(MergePartial(ret[previousNodeIndex].Value, node.Value));
}
ret.TrimExcess();
return ret;
}
static int IndexOfKey(List<MiniYamlNode> nodes, string key)
{
// PERF: Avoid LINQ.
for (var i = 0; i < nodes.Count; i++)
if (nodes[i].Key == key)
return i;
return -1;
}
static int LastIndexOfKey(List<MiniYamlNode> nodes, string key)
{
// PERF: Avoid LINQ.
for (var i = nodes.Count - 1; i >= 0; i--)
if (nodes[i].Key == key)
return i;
return -1;
}
public IEnumerable<string> ToLines(string key, string comment = null)
{
var hasKey = !string.IsNullOrEmpty(key);
@@ -508,14 +630,100 @@ namespace OpenRA
files = files.Append(mapFiles);
}
var yaml = files.Select(s => FromStream(fileSystem.Open(s), s));
if (mapRules != null && mapRules.Nodes.Count > 0)
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
IEnumerable<IReadOnlyCollection<MiniYamlNode>> yaml = files.Select(s => FromStream(fileSystem.Open(s), s, stringPool: stringPool));
if (mapRules != null && mapRules.Nodes.Length > 0)
yaml = yaml.Append(mapRules.Nodes);
return Merge(yaml);
}
}
public sealed class MiniYamlNodeBuilder
{
public MiniYamlNode.SourceLocation Location;
public string Key;
public MiniYamlBuilder Value;
public string Comment;
public MiniYamlNodeBuilder(MiniYamlNode node)
{
Location = node.Location;
Key = node.Key;
Value = new MiniYamlBuilder(node.Value);
Comment = node.Comment;
}
public MiniYamlNodeBuilder(string k, MiniYamlBuilder v, string c = null)
{
Key = k;
Value = v;
Comment = c;
}
public MiniYamlNodeBuilder(string k, MiniYamlBuilder v, string c, MiniYamlNode.SourceLocation loc)
: this(k, v, c)
{
Location = loc;
}
public MiniYamlNodeBuilder(string k, string v, string c = null)
: this(k, new MiniYamlBuilder(v, null), c) { }
public MiniYamlNodeBuilder(string k, string v, List<MiniYamlNode> n)
: this(k, new MiniYamlBuilder(v, n), null) { }
public MiniYamlNode Build()
{
return new MiniYamlNode(Key, Value.Build(), Comment, Location);
}
}
public sealed class MiniYamlBuilder
{
public string Value;
public List<MiniYamlNodeBuilder> Nodes;
public MiniYamlBuilder(MiniYaml yaml)
{
Value = yaml.Value;
Nodes = yaml.Nodes.Select(n => new MiniYamlNodeBuilder(n)).ToList();
}
public MiniYamlBuilder(string value)
: this(value, null) { }
public MiniYamlBuilder(string value, List<MiniYamlNode> nodes)
{
Value = value;
Nodes = nodes == null ? new List<MiniYamlNodeBuilder>() : nodes.ConvertAll(x => new MiniYamlNodeBuilder(x));
}
public MiniYaml Build()
{
return new MiniYaml(Value, Nodes.Select(n => n.Build()));
}
public IEnumerable<string> ToLines(string key, string comment = null)
{
var hasKey = !string.IsNullOrEmpty(key);
var hasValue = !string.IsNullOrEmpty(Value);
var hasComment = comment != null;
yield return (hasKey ? key + ":" : "")
+ (hasValue ? " " + Value.Replace("#", "\\#") : "")
+ (hasComment ? (hasKey || hasValue ? " " : "") + "#" + comment : "");
if (Nodes != null)
foreach (var line in Nodes.ToLines())
yield return "\t" + line;
}
public MiniYamlNodeBuilder NodeWithKeyOrDefault(string key)
{
return Nodes.SingleOrDefault(n => n.Key == key);
}
}
[Serializable]
public class YamlException : Exception
{

View File

@@ -33,9 +33,10 @@ namespace OpenRA
public readonly ISpriteLoader[] SpriteLoaders;
public readonly ITerrainLoader TerrainLoader;
public readonly ISpriteSequenceLoader SpriteSequenceLoader;
public readonly IModelSequenceLoader ModelSequenceLoader;
public readonly IVideoLoader[] VideoLoaders;
public readonly HotkeyManager Hotkeys;
public readonly IFileSystemLoader FileSystemLoader;
public ILoadScreen LoadScreen { get; }
public CursorProvider CursorProvider { get; private set; }
public FS ModFiles;
@@ -55,11 +56,17 @@ namespace OpenRA
Manifest = new Manifest(mod.Id, mod.Package);
ObjectCreator = new ObjectCreator(Manifest, mods);
PackageLoaders = ObjectCreator.GetLoaders<IPackageLoader>(Manifest.PackageFormats, "package");
ModFiles = new FS(mod.Id, mods, PackageLoaders);
ModFiles.LoadFromManifest(Manifest);
FileSystemLoader = ObjectCreator.GetLoader<IFileSystemLoader>(Manifest.FileSystem.Value, "filesystem");
FieldLoader.Load(FileSystemLoader, Manifest.FileSystem);
FileSystemLoader.Mount(ModFiles, ObjectCreator);
ModFiles.TrimExcess();
Manifest.LoadCustomData(ObjectCreator);
FluentProvider.Initialize(this, DefaultFileSystem);
if (useLoadScreen)
{
LoadScreen = ObjectCreator.CreateObject<ILoadScreen>(Manifest.LoadScreen.Value);
@@ -90,15 +97,6 @@ namespace OpenRA
SpriteSequenceLoader = (ISpriteSequenceLoader)sequenceCtor.Invoke(new[] { this });
var modelFormat = Manifest.Get<ModelSequenceFormat>();
var modelLoader = ObjectCreator.FindType(modelFormat.Type + "Loader");
var modelCtor = modelLoader?.GetConstructor(new[] { typeof(ModData) });
if (modelLoader == null || !modelLoader.GetInterfaces().Contains(typeof(IModelSequenceLoader)) || modelCtor == null)
throw new InvalidOperationException($"Unable to find a model loader for type '{modelFormat.Type}'.");
ModelSequenceLoader = (IModelSequenceLoader)modelCtor.Invoke(new[] { this });
ModelSequenceLoader.OnMissingModelError = s => Log.Write("debug", s);
Hotkeys = new HotkeyManager(ModFiles, Game.Settings.Keys, Manifest);
defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this));
@@ -134,7 +132,7 @@ namespace OpenRA
// horribly when you use ModData in unexpected ways.
ChromeMetrics.Initialize(this);
ChromeProvider.Initialize(this);
TranslationProvider.Initialize(this, fileSystem);
FluentProvider.Initialize(this, fileSystem);
Game.Sound.Initialize(SoundLoaders, fileSystem);
@@ -168,7 +166,8 @@ namespace OpenRA
public List<MiniYamlNode>[] GetRulesYaml()
{
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s)).ToArray();
var stringPool = new HashSet<string>(); // Reuse common strings in YAML
return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s, stringPool: stringPool)).ToArray();
}
public void Dispose()
@@ -199,4 +198,9 @@ namespace OpenRA
/// <summary>Called when the engine expects to connect to a server/replay or load the shellmap.</summary>
void StartGame(Arguments args);
}
public interface IFileSystemLoader
{
void Mount(FS fileSystem, ObjectCreator objectCreator);
}
}

View File

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

View File

@@ -11,8 +11,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Linguini.Shared.Types.Bundle;
namespace OpenRA.Network
@@ -50,74 +48,75 @@ namespace OpenRA.Network
}
}
public class LocalizedMessage
public class FluentMessage
{
public const int ProtocolVersion = 1;
public readonly string Key = string.Empty;
[FieldLoader.LoadUsing(nameof(LoadArguments))]
public readonly FluentArgument[] Arguments = Array.Empty<FluentArgument>();
public string TranslatedText { get; }
public readonly object[] Arguments;
static object LoadArguments(MiniYaml yaml)
{
var arguments = new List<FluentArgument>();
var argumentsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Arguments");
var arguments = new List<object>();
var argumentsNode = yaml.NodeWithKeyOrDefault("Arguments");
if (argumentsNode != null)
{
var regex = new Regex(@"Argument@\d+");
foreach (var argument in argumentsNode.Value.Nodes)
if (regex.IsMatch(argument.Key))
arguments.Add(FieldLoader.Load<FluentArgument>(argument.Value));
foreach (var argumentNode in argumentsNode.Value.Nodes)
{
var argument = FieldLoader.Load<FluentArgument>(argumentNode.Value);
arguments.Add(argument.Key);
if (argument.Type == FluentArgument.FluentArgumentType.Number)
{
if (!double.TryParse(argument.Value, out var number))
Log.Write("debug", $"Failed to parse {argument.Value}");
arguments.Add(number);
}
else
arguments.Add(argument.Value);
}
}
return arguments.ToArray();
}
public LocalizedMessage(MiniYaml yaml)
public FluentMessage(MiniYaml yaml)
{
// Let the FieldLoader do the dirty work of loading the public fields.
FieldLoader.Load(this, yaml);
var argumentDictionary = new Dictionary<string, object>();
foreach (var argument in Arguments)
{
if (argument.Type == FluentArgument.FluentArgumentType.Number)
{
if (!double.TryParse(argument.Value, out var number))
Log.Write("debug", $"Failed to parse {argument.Value}");
argumentDictionary.Add(argument.Key, number);
}
else
argumentDictionary.Add(argument.Key, argument.Value);
}
TranslatedText = TranslationProvider.GetString(Key, argumentDictionary);
}
public static string Serialize(string key, Dictionary<string, object> arguments = null)
public static string Serialize(string key, object[] args)
{
var root = new List<MiniYamlNode>
{
new MiniYamlNode("Protocol", ProtocolVersion.ToString()),
new MiniYamlNode("Key", key)
new("Protocol", ProtocolVersion.ToStringInvariant()),
new("Key", key),
};
if (arguments != null)
if (args != null)
{
var argumentsNode = new MiniYaml("");
var i = 0;
foreach (var argument in arguments.Select(a => new FluentArgument(a.Key, a.Value)))
argumentsNode.Nodes.Add(new MiniYamlNode("Argument@" + i++, FieldSaver.Save(argument)));
var nodes = new List<MiniYamlNode>();
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));
root.Add(new MiniYamlNode("Arguments", argumentsNode));
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");
nodes.Add(new MiniYamlNode($"Argument@{i / 2}", FieldSaver.Save(new FluentArgument(argKey, argValue))));
}
root.Add(new MiniYamlNode("Arguments", new MiniYaml("", nodes)));
}
return new MiniYaml("", root)
.ToLines("LocalizedMessage")
.ToLines("FluentMessage")
.JoinWith("\n");
}
}

View File

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

View File

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

View File

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

View File

@@ -71,8 +71,8 @@ namespace OpenRA.Network
}
catch (Exception e)
{
Console.WriteLine("Port forwarding failed: {0}", e.Message);
Log.Write("nat", e.StackTrace);
Log.Write("nat", "Port forwarding failed.");
Log.Write("nat", e);
return false;
}
@@ -90,8 +90,8 @@ namespace OpenRA.Network
}
catch (Exception e)
{
Console.WriteLine("Port removal failed: {0}", e.Message);
Log.Write("nat", e.StackTrace);
Log.Write("nat", "Port removal failed.");
Log.Write("nat", e);
return false;
}

View File

@@ -78,7 +78,8 @@ namespace OpenRA
readonly Target target;
readonly Target visualFeedbackTarget;
Order(string orderString, Actor subject, in Target target, string targetString, bool queued, Actor[] extraActors, CPos extraLocation, uint extraData, Actor[] groupedActors = null)
Order(string orderString, Actor subject, in Target target, string targetString, bool queued,
Actor[] extraActors, CPos extraLocation, uint extraData, Actor[] groupedActors = null)
{
OrderString = orderString ?? "";
Subject = subject;
@@ -156,7 +157,18 @@ namespace OpenRA
else
{
var pos = new WPos(r.ReadInt32(), r.ReadInt32(), r.ReadInt32());
target = Target.FromPos(pos);
var numberOfTerrainPositions = r.ReadInt16();
if (numberOfTerrainPositions == -1)
target = Target.FromPos(pos);
else
{
var terrainPositions = new WPos[numberOfTerrainPositions];
for (var i = 0; i < numberOfTerrainPositions; i++)
terrainPositions[i] = new WPos(r.ReadInt32(), r.ReadInt32(), r.ReadInt32());
target = Target.FromSerializedTerrainPosition(pos, terrainPositions);
}
}
break;
@@ -259,7 +271,15 @@ namespace OpenRA
public static Order FromGroupedOrder(Order grouped, Actor subject)
{
return new Order(grouped.OrderString, subject, grouped.Target, grouped.TargetString, grouped.Queued, grouped.ExtraActors, grouped.ExtraLocation, grouped.ExtraData);
return new Order(
grouped.OrderString,
subject,
grouped.Target,
grouped.TargetString,
grouped.Queued,
grouped.ExtraActors,
grouped.ExtraLocation,
grouped.ExtraData);
}
public static Order Command(string text)
@@ -388,6 +408,21 @@ namespace OpenRA
w.Write(targetState.Pos.X);
w.Write(targetState.Pos.Y);
w.Write(targetState.Pos.Z);
// Don't send extra data over the network that will be restored by the Target ctor
var terrainPositions = targetState.TerrainPositions.Length;
if (terrainPositions == 1 && targetState.TerrainPositions[0] == targetState.Pos)
w.Write((short)-1);
else
{
w.Write((short)terrainPositions);
foreach (var position in targetState.TerrainPositions)
{
w.Write(position.X);
w.Write(position.Y);
w.Write(position.Z);
}
}
}
break;
@@ -430,7 +465,7 @@ namespace OpenRA
public override string ToString()
{
return $"OrderString: \"{OrderString}\" \n\t Type: \"{Type}\". \n\t Subject: \"{Subject}\". \n\t Target: \"{Target}\"." +
$"\n\t TargetString: \"{TargetString}\".\n\t IsImmediate: {IsImmediate}.\n\t Player(PlayerName): {Player?.PlayerName}\n";
$"\n\t TargetString: \"{TargetString}\".\n\t IsImmediate: {IsImmediate}.\n\t Player(PlayerName): {Player?.ResolvedPlayerName}\n";
}
}
}

View File

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

View File

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

View File

@@ -69,7 +69,7 @@ namespace OpenRA.Network
if (o.OrderString == "StartGame")
IsValid = true;
else if (o.OrderString == "SyncInfo" && !IsValid)
LobbyInfo = Session.Deserialize(o.TargetString);
LobbyInfo = Session.Deserialize(o.TargetString, o.OrderString);
}
}
}

View File

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

View File

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

View File

@@ -122,7 +122,7 @@ namespace OpenRA.Network
Log.Write("sync", $"Player: {Game.Settings.Player.Name} ({Platform.CurrentPlatform} {Environment.OSVersion} {Platform.RuntimeVersion})");
if (Game.IsHost)
Log.Write("sync", "Player is host.");
Log.Write("sync", $"Game ID: {orderManager.LobbyInfo.GlobalSettings.GameUid} (Mod: {mod.Title} at Version {mod.Version})");
Log.Write("sync", $"Game ID: {orderManager.LobbyInfo.GlobalSettings.GameUid} (Mod: {mod.TitleTranslated} at Version {mod.Version})");
Log.Write("sync", $"Sync for net frame {r.Frame} -------------");
Log.Write("sync", $"SharedRandom: {r.SyncedRandom} (#{r.TotalCount})");
Log.Write("sync", "Synced Traits:");
@@ -201,8 +201,12 @@ namespace OpenRA.Network
public TypeInfo(Type type)
{
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
var fields = type.GetFields(Flags).Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>());
var properties = type.GetProperties(Flags).Where(pi => pi.HasAttribute<SyncAttribute>());
var fields = type.GetFields(Flags)
.Where(fi => !fi.IsLiteral && !fi.IsStatic && fi.HasAttribute<SyncAttribute>())
.ToList();
var properties = type.GetProperties(Flags)
.Where(pi => pi.HasAttribute<SyncAttribute>())
.ToList();
foreach (var prop in properties)
if (!prop.CanRead || prop.GetIndexParameters().Length > 0)
@@ -300,7 +304,7 @@ namespace OpenRA.Network
public object this[int index]
{
get
readonly get
{
if (item2OrSentinel == Sentinel)
return ((object[])item1OrArray)[index];

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