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.
286 lines
9.0 KiB
C#
286 lines
9.0 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using OpenRA.FileSystem;
|
|
using OpenRA.Primitives;
|
|
|
|
namespace OpenRA
|
|
{
|
|
public interface IGlobalModData { }
|
|
|
|
public sealed class TerrainFormat : IGlobalModData
|
|
{
|
|
public readonly string Type;
|
|
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
|
|
public TerrainFormat(MiniYaml yaml)
|
|
{
|
|
Type = yaml.Value;
|
|
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
|
|
}
|
|
}
|
|
|
|
public sealed class SpriteSequenceFormat : IGlobalModData
|
|
{
|
|
public readonly string Type;
|
|
public readonly IReadOnlyDictionary<string, MiniYaml> Metadata;
|
|
public SpriteSequenceFormat(MiniYaml yaml)
|
|
{
|
|
Type = yaml.Value;
|
|
Metadata = new ReadOnlyDictionary<string, MiniYaml>(yaml.ToDictionary());
|
|
}
|
|
}
|
|
|
|
public class ModMetadata
|
|
{
|
|
public string Title;
|
|
public string Version;
|
|
public string Website;
|
|
public string WebIcon32;
|
|
public string WindowTitle;
|
|
public bool Hidden;
|
|
}
|
|
|
|
/// <summary>Describes what is to be loaded in order to run a mod.</summary>
|
|
public sealed class Manifest : IDisposable
|
|
{
|
|
public readonly string Id;
|
|
public readonly IReadOnlyPackage Package;
|
|
public readonly ModMetadata Metadata;
|
|
public readonly string[]
|
|
Rules, ServerTraits,
|
|
Sequences, ModelSequences, Cursors, Chrome, Assemblies, ChromeLayout,
|
|
Weapons, Voices, Notifications, Music, Translations, TileSets,
|
|
ChromeMetrics, MapCompatibility, Missions, Hotkeys;
|
|
|
|
public readonly IReadOnlyDictionary<string, string> Packages;
|
|
public readonly IReadOnlyDictionary<string, string> MapFolders;
|
|
public readonly MiniYaml LoadScreen;
|
|
public readonly string DefaultOrderGenerator;
|
|
|
|
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 bool AllowUnusedTranslationsInExternalPackages = true;
|
|
|
|
readonly string[] reservedModuleNames =
|
|
{
|
|
"Include", "Metadata", "Folders", "MapFolders", "Packages", "Rules",
|
|
"Sequences", "ModelSequences", "Cursors", "Chrome", "Assemblies", "ChromeLayout", "Weapons",
|
|
"Voices", "Notifications", "Music", "Translations", "TileSets", "ChromeMetrics", "Missions", "Hotkeys",
|
|
"ServerTraits", "LoadScreen", "DefaultOrderGenerator", "SupportsMapsFrom", "SoundFormats", "SpriteFormats", "VideoFormats",
|
|
"RequiresMods", "PackageFormats", "AllowUnusedTranslationsInExternalPackages"
|
|
};
|
|
|
|
readonly TypeDictionary modules = new();
|
|
readonly Dictionary<string, MiniYaml> yaml;
|
|
|
|
bool customDataLoaded;
|
|
|
|
public Manifest(string modId, IReadOnlyPackage package)
|
|
{
|
|
Id = modId;
|
|
Package = package;
|
|
|
|
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")
|
|
continue;
|
|
|
|
// Replace `Includes: filename.yaml` with the contents of filename.yaml
|
|
var filename = nodes[i].Value.Value;
|
|
var contents = package.GetStream(filename);
|
|
if (contents == null)
|
|
throw new YamlException($"{nodes[i].Location}: File `{filename}` not found.");
|
|
|
|
nodes.RemoveAt(i);
|
|
nodes.InsertRange(i, MiniYaml.FromStream(contents, $"{package.Name}:{filename}", stringPool: stringPool));
|
|
}
|
|
|
|
// Merge inherited overrides
|
|
yaml = new MiniYaml(null, MiniYaml.Merge(new[] { nodes })).ToDictionary();
|
|
|
|
Metadata = FieldLoader.Load<ModMetadata>(yaml["Metadata"]);
|
|
|
|
// TODO: Use fieldloader
|
|
MapFolders = YamlDictionary(yaml, "MapFolders");
|
|
|
|
if (yaml.TryGetValue("Packages", out var packages))
|
|
Packages = packages.ToDictionary(x => x.Value);
|
|
|
|
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");
|
|
TileSets = YamlList(yaml, "TileSets");
|
|
ChromeMetrics = YamlList(yaml, "ChromeMetrics");
|
|
Missions = YamlList(yaml, "Missions");
|
|
Hotkeys = YamlList(yaml, "Hotkeys");
|
|
|
|
ServerTraits = YamlList(yaml, "ServerTraits");
|
|
|
|
if (!yaml.TryGetValue("LoadScreen", out LoadScreen))
|
|
throw new InvalidDataException("`LoadScreen` section is not defined.");
|
|
|
|
// Allow inherited mods to import parent maps.
|
|
var compat = new List<string> { Id };
|
|
|
|
if (yaml.TryGetValue("SupportsMapsFrom", out var entry))
|
|
compat.AddRange(entry.Value.Split(',').Select(c => c.Trim()));
|
|
|
|
MapCompatibility = compat.ToArray();
|
|
|
|
if (yaml.TryGetValue("DefaultOrderGenerator", out entry))
|
|
DefaultOrderGenerator = entry.Value;
|
|
|
|
if (yaml.TryGetValue("PackageFormats", out entry))
|
|
PackageFormats = FieldLoader.GetValue<string[]>("PackageFormats", entry.Value);
|
|
|
|
if (yaml.TryGetValue("SoundFormats", out entry))
|
|
SoundFormats = FieldLoader.GetValue<string[]>("SoundFormats", entry.Value);
|
|
|
|
if (yaml.TryGetValue("SpriteFormats", out entry))
|
|
SpriteFormats = FieldLoader.GetValue<string[]>("SpriteFormats", entry.Value);
|
|
|
|
if (yaml.TryGetValue("VideoFormats", out entry))
|
|
VideoFormats = FieldLoader.GetValue<string[]>("VideoFormats", entry.Value);
|
|
|
|
if (yaml.TryGetValue("AllowUnusedTranslationsInExternalPackages", out entry))
|
|
AllowUnusedTranslationsInExternalPackages =
|
|
FieldLoader.GetValue<bool>("AllowUnusedTranslationsInExternalPackages", entry.Value);
|
|
}
|
|
|
|
public void LoadCustomData(ObjectCreator oc)
|
|
{
|
|
foreach (var kv in yaml)
|
|
{
|
|
if (reservedModuleNames.Contains(kv.Key))
|
|
continue;
|
|
|
|
var t = oc.FindType(kv.Key);
|
|
if (t == null || !typeof(IGlobalModData).IsAssignableFrom(t))
|
|
throw new InvalidDataException($"`{kv.Key}` is not a valid mod manifest entry.");
|
|
|
|
IGlobalModData module;
|
|
var ctor = t.GetConstructor(new[] { typeof(MiniYaml) });
|
|
if (ctor != null)
|
|
{
|
|
// Class has opted-in to DIY initialization
|
|
module = (IGlobalModData)ctor.Invoke(new object[] { kv.Value });
|
|
}
|
|
else
|
|
{
|
|
// Automatically load the child nodes using FieldLoader
|
|
module = oc.CreateObject<IGlobalModData>(kv.Key);
|
|
FieldLoader.Load(module, kv.Value);
|
|
}
|
|
|
|
modules.Add(module);
|
|
}
|
|
|
|
customDataLoaded = true;
|
|
}
|
|
|
|
static string[] YamlList(Dictionary<string, MiniYaml> yaml, string key)
|
|
{
|
|
if (!yaml.TryGetValue(key, out var value))
|
|
return Array.Empty<string>();
|
|
|
|
return value.Nodes.Select(n => n.Key).ToArray();
|
|
}
|
|
|
|
static IReadOnlyDictionary<string, string> YamlDictionary(Dictionary<string, MiniYaml> yaml, string key)
|
|
{
|
|
if (!yaml.TryGetValue(key, out var value))
|
|
return new Dictionary<string, string>();
|
|
|
|
return value.ToDictionary(my => my.Value);
|
|
}
|
|
|
|
public bool Contains<T>() where T : IGlobalModData
|
|
{
|
|
return modules.Contains<T>();
|
|
}
|
|
|
|
/// <summary>Load a cached IGlobalModData instance.</summary>
|
|
public T Get<T>() where T : IGlobalModData
|
|
{
|
|
if (!customDataLoaded)
|
|
throw new InvalidOperationException("Attempted to call Manifest.Get() before loading custom data!");
|
|
|
|
var module = modules.GetOrDefault<T>();
|
|
|
|
// Lazily create the default values if not explicitly defined.
|
|
if (module == null)
|
|
{
|
|
module = (T)Game.ModData.ObjectCreator.CreateBasic(typeof(T));
|
|
modules.Add(module);
|
|
}
|
|
|
|
return module;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load an uncached IGlobalModData instance directly from the manifest yaml.
|
|
/// This should only be used by external mods that want to query data from this mod.
|
|
/// </summary>
|
|
public T Get<T>(ObjectCreator oc) where T : IGlobalModData
|
|
{
|
|
var t = typeof(T);
|
|
if (!yaml.TryGetValue(t.Name, out var data))
|
|
{
|
|
// Lazily create the default values if not explicitly defined.
|
|
return (T)oc.CreateBasic(t);
|
|
}
|
|
|
|
IGlobalModData module;
|
|
var ctor = t.GetConstructor(new[] { typeof(MiniYaml) });
|
|
if (ctor != null)
|
|
{
|
|
// Class has opted-in to DIY initialization
|
|
module = (IGlobalModData)ctor.Invoke(new object[] { data.Value });
|
|
}
|
|
else
|
|
{
|
|
// Automatically load the child nodes using FieldLoader
|
|
module = oc.CreateObject<IGlobalModData>(t.Name);
|
|
FieldLoader.Load(module, data);
|
|
}
|
|
|
|
return (T)module;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (var module in modules)
|
|
{
|
|
var disposableModule = module as IDisposable;
|
|
disposableModule?.Dispose();
|
|
}
|
|
}
|
|
}
|
|
}
|