diff --git a/OpenRA.Game/FileSystem/FileSystem.cs b/OpenRA.Game/FileSystem/FileSystem.cs index f1e8a7a6f0..c6b7f9260c 100644 --- a/OpenRA.Game/FileSystem/FileSystem.cs +++ b/OpenRA.Game/FileSystem/FileSystem.cs @@ -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 @@ -270,21 +270,11 @@ namespace OpenRA.FileSystem } /// - /// 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. /// - 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); + return !filename.StartsWith($"{modID}|", StringComparison.Ordinal); } /// diff --git a/OpenRA.Game/Manifest.cs b/OpenRA.Game/Manifest.cs index 9bf719e7d2..632d4f5ee7 100644 --- a/OpenRA.Game/Manifest.cs +++ b/OpenRA.Game/Manifest.cs @@ -74,6 +74,7 @@ namespace OpenRA public readonly string[] SpriteFormats = Array.Empty(); public readonly string[] PackageFormats = Array.Empty(); public readonly string[] VideoFormats = Array.Empty(); + public bool AllowUnusedTranslationsInExternalPackages = true; readonly string[] reservedModuleNames = { @@ -81,7 +82,7 @@ namespace OpenRA "Sequences", "ModelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons", "Voices", "Notifications", "Music", "Translations", "TileSets", "ChromeMetrics", "Missions", "Hotkeys", "ServerTraits", "LoadScreen", "DefaultOrderGenerator", "SupportsMapsFrom", "SoundFormats", "SpriteFormats", "VideoFormats", - "RequiresMods", "PackageFormats" + "RequiresMods", "PackageFormats", "AllowUnusedTranslationsInExternalPackages" }; readonly TypeDictionary modules = new(); @@ -166,6 +167,10 @@ namespace OpenRA if (yaml.TryGetValue("VideoFormats", out entry)) VideoFormats = FieldLoader.GetValue("VideoFormats", entry.Value); + + if (yaml.TryGetValue("AllowUnusedTranslationsInExternalPackages", out entry)) + AllowUnusedTranslationsInExternalPackages = + FieldLoader.GetValue("AllowUnusedTranslationsInExternalPackages", entry.Value); } public void LoadCustomData(ObjectCreator oc) diff --git a/OpenRA.Game/Map/Map.cs b/OpenRA.Game/Map/Map.cs index 06418f98c5..6e3ea206b2 100644 --- a/OpenRA.Game/Map/Map.cs +++ b/OpenRA.Game/Map/Map.cs @@ -1427,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; } diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs index f4c7a53f45..37c7e7cb84 100644 --- a/OpenRA.Game/Map/MapPreview.cs +++ b/OpenRA.Game/Map/MapPreview.cs @@ -654,11 +654,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; } diff --git a/OpenRA.Mods.Common/Lint/CheckTranslationReference.cs b/OpenRA.Mods.Common/Lint/CheckTranslationReference.cs index 14196bc260..67cea55ecb 100644 --- a/OpenRA.Mods.Common/Lint/CheckTranslationReference.cs +++ b/OpenRA.Mods.Common/Lint/CheckTranslationReference.cs @@ -44,7 +44,12 @@ namespace OpenRA.Mods.Common.Lint foreach (var language in GetTranslationLanguages(modData)) { - CheckKeys(modData.Manifest.Translations.Concat(mapTranslations), map.Open, usedKeys, language, false, emitError, emitWarning); + // Check keys and variables are not missing across all language files. + // But for maps we don't warn on unused keys. They might be unused on *this* map, + // but the mod or another map may use them and we don't have sight of that. + CheckKeys( + modData.Manifest.Translations.Concat(mapTranslations), map.Open, usedKeys, + language, _ => false, emitError, emitWarning); var modTranslation = new Translation(language, modData.Manifest.Translations, modData.DefaultFileSystem, _ => { }); var mapTranslation = new Translation(language, mapTranslations, map, error => emitError(error.Message)); @@ -79,7 +84,13 @@ namespace OpenRA.Mods.Common.Lint CheckModWidgets(modData, usedKeys, testedFields); // With the fully populated keys, check keys and variables are not missing and not unused across all language files. - var keyWithAttrs = CheckKeys(modData.Manifest.Translations, modData.DefaultFileSystem.Open, usedKeys, language, true, emitError, emitWarning); + var keyWithAttrs = CheckKeys( + modData.Manifest.Translations, modData.DefaultFileSystem.Open, usedKeys, + language, + file => + !modData.Manifest.AllowUnusedTranslationsInExternalPackages || + !modData.DefaultFileSystem.IsExternalFile(file), + emitError, emitWarning); foreach (var group in usedKeys.KeysWithContext) { @@ -322,7 +333,10 @@ namespace OpenRA.Mods.Common.Lint CheckChrome(n, translationReferencesByWidgetField, usedKeys); } - static HashSet CheckKeys(IEnumerable translationFiles, Func openFile, TranslationKeys usedKeys, string language, bool checkUnusedKeys, Action emitError, Action emitWarning) + static HashSet CheckKeys( + IEnumerable translationFiles, Func openFile, TranslationKeys usedKeys, + string language, Func checkUnusedKeysForFile, + Action emitError, Action emitWarning) { var keyWithAttrs = new HashSet(); foreach (var file in translationFiles) @@ -351,7 +365,7 @@ namespace OpenRA.Mods.Common.Lint foreach (var (node, attributeName) in nodeAndAttributeNames) { keyWithAttrs.Add(attributeName == null ? key : $"{key}.{attributeName}"); - if (checkUnusedKeys) + if (checkUnusedKeysForFile(file)) CheckUnusedKey(key, attributeName, file, usedKeys, emitWarning); CheckVariables(node, key, attributeName, file, usedKeys, emitError, emitWarning); } diff --git a/OpenRA.Mods.Common/UpdateRules/UpdateUtils.cs b/OpenRA.Mods.Common/UpdateRules/UpdateUtils.cs index b54d77edd3..6c817d067f 100644 --- a/OpenRA.Mods.Common/UpdateRules/UpdateUtils.cs +++ b/OpenRA.Mods.Common/UpdateRules/UpdateUtils.cs @@ -48,7 +48,7 @@ namespace OpenRA.Mods.Common.UpdateRules { return FieldLoader.GetValue("value", yaml.Value) .Where(f => f.Contains('|')) - .SelectMany(f => LoadModYaml(modData, FilterExternalModFiles(modData, new[] { f }, externalFilenames))) + .SelectMany(f => LoadModYaml(modData, FilterExternalFiles(modData, new[] { f }, externalFilenames))) .ToList(); } @@ -182,11 +182,11 @@ namespace OpenRA.Mods.Common.UpdateRules return MiniYaml.Merge(yaml).ConvertAll(n => new MiniYamlNodeBuilder(n)); } - public static IEnumerable FilterExternalModFiles(ModData modData, IEnumerable files, HashSet externalFilenames) + public static IEnumerable FilterExternalFiles(ModData modData, IEnumerable files, HashSet externalFilenames) { foreach (var f in files) { - if (f.Contains('|') && modData.DefaultFileSystem.IsExternalModFile(f)) + if (f.Contains('|') && modData.DefaultFileSystem.IsExternalFile(f)) { externalFilenames.Add(f); continue; @@ -200,12 +200,12 @@ namespace OpenRA.Mods.Common.UpdateRules { var manualSteps = new List(); - var modRules = LoadModYaml(modData, FilterExternalModFiles(modData, modData.Manifest.Rules, externalFilenames)); - var modWeapons = LoadModYaml(modData, FilterExternalModFiles(modData, modData.Manifest.Weapons, externalFilenames)); - var modSequences = LoadModYaml(modData, FilterExternalModFiles(modData, modData.Manifest.Sequences, externalFilenames)); - var modTilesets = LoadModYaml(modData, FilterExternalModFiles(modData, modData.Manifest.TileSets, externalFilenames)); - var modChromeLayout = LoadModYaml(modData, FilterExternalModFiles(modData, modData.Manifest.ChromeLayout, externalFilenames)); - var modChromeProvider = LoadModYaml(modData, FilterExternalModFiles(modData, modData.Manifest.Chrome, externalFilenames)); + var modRules = LoadModYaml(modData, FilterExternalFiles(modData, modData.Manifest.Rules, externalFilenames)); + var modWeapons = LoadModYaml(modData, FilterExternalFiles(modData, modData.Manifest.Weapons, externalFilenames)); + var modSequences = LoadModYaml(modData, FilterExternalFiles(modData, modData.Manifest.Sequences, externalFilenames)); + var modTilesets = LoadModYaml(modData, FilterExternalFiles(modData, modData.Manifest.TileSets, externalFilenames)); + var modChromeLayout = LoadModYaml(modData, FilterExternalFiles(modData, modData.Manifest.ChromeLayout, externalFilenames)); + var modChromeProvider = LoadModYaml(modData, FilterExternalFiles(modData, modData.Manifest.Chrome, externalFilenames)); // Find and add shared map includes foreach (var package in modData.MapCache.EnumerateMapPackagesWithoutCaching()) diff --git a/OpenRA.Mods.Common/UtilityCommands/ExtractYamlStrings.cs b/OpenRA.Mods.Common/UtilityCommands/ExtractYamlStrings.cs index 4780c579b2..e0362ee2d4 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ExtractYamlStrings.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ExtractYamlStrings.cs @@ -47,7 +47,7 @@ namespace OpenRA.Mods.Common.UtilityCommands .Where(t => t.Value.Length > 0) .ToDictionary(t => t.Key, t => t.Value); - var modRules = UpdateUtils.LoadModYaml(modData, UpdateUtils.FilterExternalModFiles(modData, modData.Manifest.Rules, new HashSet())); + var modRules = UpdateUtils.LoadModYaml(modData, UpdateUtils.FilterExternalFiles(modData, modData.Manifest.Rules, new HashSet())); // Include files referenced in maps. foreach (var package in modData.MapCache.EnumerateMapPackagesWithoutCaching()) diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index 2756051b80..0b14cba55e 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -149,6 +149,8 @@ Translations: cnc|languages/chrome/en.ftl cnc|languages/rules/en.ftl +AllowUnusedTranslationsInExternalPackages: false + Voices: cnc|audio/voices.yaml diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index 96489bd691..5256b7d1c2 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -130,6 +130,8 @@ Translations: d2k|languages/chrome/en.ftl d2k|languages/rules/en.ftl +AllowUnusedTranslationsInExternalPackages: false + Weapons: d2k|weapons/debris.yaml d2k|weapons/smallguns.yaml diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index 7fd1fb3a9a..b188ed1a83 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -146,6 +146,8 @@ Translations: ra|languages/chrome/en.ftl ra|languages/rules/en.ftl +AllowUnusedTranslationsInExternalPackages: false + Weapons: ra|weapons/explosions.yaml ra|weapons/ballistics.yaml diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index b06d986f60..cdf0521df0 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -191,6 +191,8 @@ Translations: ts|languages/chrome/en.ftl ts|languages/rules/en.ftl +AllowUnusedTranslationsInExternalPackages: false + Voices: ts|audio/voices.yaml