#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