From 5a0c8439fc3469776c05bad39e74a1d6756754b2 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sat, 19 Oct 2024 14:26:06 +0100 Subject: [PATCH] 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. --- OpenRA.Game/FluentProvider.cs | 24 +++++++++++++-- OpenRA.Game/Map/MapPreview.cs | 27 ++++++++++++++--- .../Lint/CheckFluentReferences.cs | 5 ++++ .../UtilityCommands/ExtractMapRules.cs | 30 +++++++++++++++++++ 4 files changed, 79 insertions(+), 7 deletions(-) diff --git a/OpenRA.Game/FluentProvider.cs b/OpenRA.Game/FluentProvider.cs index 25f1b7c8fd..29bbeb2fe4 100644 --- a/OpenRA.Game/FluentProvider.cs +++ b/OpenRA.Game/FluentProvider.cs @@ -9,6 +9,8 @@ */ #endregion +using System; +using System.Text; using OpenRA.FileSystem; namespace OpenRA @@ -25,9 +27,25 @@ namespace OpenRA lock (SyncObject) { modFluentBundle = new FluentBundle(Game.Settings.Player.Language, modData.Manifest.Translations, fileSystem); - mapFluentBundle = fileSystem is Map map && map.FluentMessageDefinitions != null - ? new FluentBundle(Game.Settings.Player.Language, FieldLoader.GetValue("value", map.FluentMessageDefinitions.Value), fileSystem) - : null; + if (fileSystem is Map map && map.FluentMessageDefinitions != null) + { + var files = Array.Empty(); + if (map.FluentMessageDefinitions.Value != null) + files = FieldLoader.GetValue("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(Game.Settings.Player.Language, files, fileSystem, text); + } } } diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs index 1e5d4118eb..e7408ae7ce 100644 --- a/OpenRA.Game/Map/MapPreview.cs +++ b/OpenRA.Game/Map/MapPreview.cs @@ -122,13 +122,32 @@ namespace OpenRA NotificationDefinitions = LoadRuleSection(yaml, "Notifications"); SequenceDefinitions = LoadRuleSection(yaml, "Sequences"); ModelSequenceDefinitions = LoadRuleSection(yaml, "ModelSequences"); - - FluentBundle = yaml.TryGetValue("Translations", out var node) && node != null - ? new FluentBundle(Game.Settings.Player.Language, FieldLoader.GetValue("value", node.Value), fileSystem) - : null; + FluentMessageDefinitions = LoadRuleSection(yaml, "Translations"); try { + if (FluentMessageDefinitions != null) + { + var files = Array.Empty(); + if (FluentMessageDefinitions.Value != null) + files = FieldLoader.GetValue("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(Game.Settings.Player.Language, 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) diff --git a/OpenRA.Mods.Common/Lint/CheckFluentReferences.cs b/OpenRA.Mods.Common/Lint/CheckFluentReferences.cs index 1abf1cb384..03d7ffb937 100644 --- a/OpenRA.Mods.Common/Lint/CheckFluentReferences.cs +++ b/OpenRA.Mods.Common/Lint/CheckFluentReferences.cs @@ -71,6 +71,11 @@ namespace OpenRA.Mods.Common.Lint } } } + + if (map.FluentMessageDefinitions.Nodes.Length > 0) + emitWarning( + $"Lint pass ({nameof(CheckFluentReferences)}) lacks the know-how to test inline map fluent messages " + + "- previous warnings may be incorrect"); } void ILintPass.Run(Action emitError, Action emitWarning, ModData modData) diff --git a/OpenRA.Mods.Common/UtilityCommands/ExtractMapRules.cs b/OpenRA.Mods.Common/UtilityCommands/ExtractMapRules.cs index 49b4bfd196..2237f1b54c 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ExtractMapRules.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ExtractMapRules.cs @@ -47,6 +47,35 @@ namespace OpenRA.Mods.Common.UtilityCommands Console.WriteLine(output.ToLines(key).JoinWith("\n")); } + static void MergeAndPrintFluentMessages(Map map, string key, MiniYaml value) + { + var nodes = new List(); + var includes = new List(); + if (value != null && value.Value != null) + { + // The order of the included files matter, so we can defer to system files + // only as long as they are included first. + var include = false; + var files = FieldLoader.GetValue("value", value.Value); + foreach (var f in files) + { + include |= map.Package.Contains(f); + if (include) + { + nodes.Add(new MiniYamlNode("base64", Convert.ToBase64String(map.Open(f).ReadAllBytes()))); + } + else + includes.Add(f); + } + } + + if (value != null) + nodes.AddRange(value.Nodes); + + var output = new MiniYaml(includes.JoinWith(", "), nodes); + Console.WriteLine(output.ToLines(key).JoinWith("\n")); + } + [Desc("MAPFILE", "Merge custom map rules into a form suitable for including in map.yaml.")] void IUtilityCommand.Run(Utility utility, string[] args) { @@ -60,6 +89,7 @@ namespace OpenRA.Mods.Common.UtilityCommands MergeAndPrint(map, "Voices", map.VoiceDefinitions); MergeAndPrint(map, "Music", map.MusicDefinitions); MergeAndPrint(map, "Notifications", map.NotificationDefinitions); + MergeAndPrintFluentMessages(map, "Translations", map.FluentMessageDefinitions); } } }