#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; namespace OpenRA.Mods.Common.MapGenerator { public sealed class MapGeneratorSettings { /// How an option should be treated for UI purposes. public enum UiType { Hidden, DropDown, Checkbox, Integer, Float, String, } /// Represents a bunch of settings, along with UI information. public sealed class Choice { /// Uniquely identifies a Choice within an Option. public readonly string Id; /// The label to use for UI selection. Post-fluent. public readonly string Label = null; /// The tooltip to use for UI selection. Post-fluent. public readonly string Description = null; /// /// Only offer the Choice for these tilesets. (If null, show for all.) /// public readonly IReadOnlySet Tileset = null; /// (Partial) settings to combine into the final overall settings. public readonly MiniYaml Settings; public Choice(string id, MiniYaml my) { Id = id; var label = my.NodeWithKeyOrDefault("Label")?.Value.Value; if (label != null) { Label = FluentProvider.GetMessage($"{label}.label"); FluentProvider.TryGetMessage($"{label}.description", out Description); } Tileset = my.NodeWithKeyOrDefault("Tileset")?.Value.Value ?.Split(',') .ToImmutableHashSet(); Settings = my.NodeWithKey("Settings").Value; } /// Create a choice that represents a top-level setting with a given value. public Choice(string setting, string value) { Id = value; Label = value; Description = null; Tileset = null; Settings = new MiniYaml(null, new[] { new MiniYamlNode(setting, value) }); } public static void DumpFluent(MiniYaml my, List references) { var label = my.NodeWithKeyOrDefault("Label")?.Value.Value; if (label != null) { references.Add($"{label}.label"); // Descriptions are optional. if (FluentProvider.TryGetMessage($"{label}.description", out _)) references.Add($"{label}.description"); } } /// Check whether this choice is permitted for this map. public bool Allowed(ITerrainInfo terrainInfo) { if (Tileset != null && !Tileset.Contains(terrainInfo.Id)) return false; return true; } /// For single-setting choices, creates a new choice for that setting with a given value. public Choice NewValue(string value) { if (Settings.Nodes.Length != 1) throw new InvalidOperationException("NewValue can only be used on single-setting Choices"); return new(Settings.Nodes[0].Key, value); } } public sealed class Option { /// Unique identifier for this Option. [FieldLoader.Ignore] public readonly string Id; /// The label to use for UI selection. Post-fluent. [FieldLoader.Ignore] public readonly string Label = null; /// /// For multiple-choice options (dropdowns/checkboxes) holds the allowed Choices. /// For text entry Options, contains the default value. /// If this is empty, the map is not compatible with the generator. /// [FieldLoader.Ignore] public readonly IReadOnlyList Choices; /// /// The default Choice for this option. /// Default will be ignored if it is not valid for the map. /// [FieldLoader.Ignore] public readonly Choice Default; public readonly double Min = double.NegativeInfinity; public readonly double Max = double.PositiveInfinity; /// Whether the Option can be randomized. public readonly bool Random = false; /// How an option should be treated for UI purposes. [FieldLoader.Ignore] public readonly UiType Ui; /// Settings layering priority. Higher overrides lower. public readonly int Priority = 0; public Option(string id, MiniYaml my, ITerrainInfo terrainInfo) { Id = id; FieldLoader.Load(this, my); var label = my.NodeWithKeyOrDefault("Label")?.Value.Value; if (label != null) Label = FluentProvider.GetMessage(label); var choices = new List(); Choices = choices; var integerSetting = my.NodeWithKeyOrDefault("Integer"); var floatSetting = my.NodeWithKeyOrDefault("Float"); var stringSetting = my.NodeWithKeyOrDefault("String"); var checkboxSetting = my.NodeWithKeyOrDefault("Checkbox"); var simpleChoiceSetting = my.NodeWithKeyOrDefault("SimpleChoice"); var textSetting = integerSetting ?? floatSetting ?? stringSetting; if (textSetting != null) { var setting = textSetting.Value.Value; var value = my.NodeWithKeyOrDefault("Default")?.Value.Value ?? ""; var choice = new Choice(setting, value); choices.Add(choice); Default = choice; Ui = integerSetting != null ? UiType.Integer : floatSetting != null ? UiType.Float : UiType.String; if (Ui == UiType.Integer) { if (Min == double.NegativeInfinity) Min = int.MinValue; if (Max == double.PositiveInfinity) Max = int.MaxValue; } } else if (checkboxSetting != null) { var setting = checkboxSetting.Value.Value; var falseChoice = new Choice(setting, "False"); var trueChoice = new Choice(setting, "True"); choices.Add(falseChoice); choices.Add(trueChoice); var enabled = (my.NodeWithKeyOrDefault("Default")?.Value.Value ?? "False") == "True"; Default = enabled ? trueChoice : falseChoice; Ui = UiType.Checkbox; } else { if (simpleChoiceSetting != null) { var setting = simpleChoiceSetting.Value.Value; var values = simpleChoiceSetting.Value .NodeWithKey("Values").Value.Value .Split(','); foreach (var value in values) choices.Add(new Choice(setting, value)); } else { foreach (var node in my.Nodes) { var split = node.Key.Split('@'); if (split[0] == "Choice") { string choiceId = null; if (split.Length >= 2) choiceId = split[1]; var choice = new Choice(choiceId, node.Value); if (choice.Allowed(terrainInfo)) choices.Add(choice); } } } if (Choices.Count > 0) { var defaultOrder = my.NodeWithKeyOrDefault("Default")?.Value.Value; if (defaultOrder != null) { foreach (var defaultChoice in defaultOrder.Split(',')) { Default = Choices.FirstOrDefault(choice => choice.Id == defaultChoice); if (Default != null) break; } if (Default == null) throw new YamlException($"None of option `{id}`'s default choices `{defaultOrder}` are not valid"); } else { Default = Choices[0]; } } Ui = Label == null ? UiType.Hidden : UiType.DropDown; } } public static void DumpFluent(MiniYaml my, List references) { var label = my.NodeWithKeyOrDefault("Label")?.Value.Value; if (label != null) references.Add(label); foreach (var node in my.Nodes) if (node.Key.Split('@')[0] == "Choice") Choice.DumpFluent(node.Value, references); } public bool ValidateChoice(Choice choice) { switch (Ui) { case UiType.Integer: { var valid = long.TryParse(choice.Id, out var value); if (value < Min || value > Max) valid = false; return valid; } case UiType.Float: { var valid = double.TryParse(choice.Id, out var value); if (value < Min || value > Max) valid = false; return valid; } case UiType.String: return true; default: return Choices.Contains(choice); } } public Choice RandomChoice() { if (Random) { var random = (long)Guid.NewGuid().GetHashCode() - int.MinValue; switch (Ui) { case UiType.Integer: var intRandom = (int)(random % (long)(Max - Min + 1) + (long)Min); return Default.NewValue(intRandom.ToStringInvariant()); case UiType.Float: var floatRandom = (float)((double)0xffffffff * random / (Max - Min) + Min); return Default.NewValue(floatRandom.ToStringInvariant()); case UiType.Checkbox: case UiType.DropDown: if (Choices.Count == 0) throw new InvalidOperationException($"Map is not compatible with Option `{Id}`"); return Choices[(int)(random % Choices.Count)]; default: throw new InvalidOperationException($"Option `{Id}` does not have a randomizable type"); } } else { return Default; } } public bool IsFreeform() { switch (Ui) { case UiType.Hidden: case UiType.DropDown: case UiType.Checkbox: return false; case UiType.Integer: case UiType.Float: case UiType.String: return true; default: throw new InvalidOperationException("Bad UiType"); } } } public readonly IReadOnlyList