Rewrite map generator settings UI logic.

This commit is contained in:
Paul Chote
2025-04-14 09:35:13 +01:00
committed by Gustas Kažukauskas
parent 8f46247dc9
commit 78f660124c
9 changed files with 390 additions and 548 deletions

View File

@@ -13,389 +13,231 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.Mods.Common.Traits;
using OpenRA.Support;
namespace OpenRA.Mods.Common.MapGenerator
{
public sealed class MapGeneratorSettings
public abstract class MapGeneratorOption
{
/// <summary>How an option should be treated for UI purposes.</summary>
public enum UiType
[FieldLoader.Ignore]
public readonly string Id;
public readonly string Label = null;
public readonly int Priority = 0;
protected MapGeneratorOption(string id, MiniYaml yaml)
{
Hidden,
DropDown,
Checkbox,
Integer,
Float,
String,
Id = id;
FieldLoader.Load(this, yaml);
}
/// <summary>Represents a bunch of settings, along with UI information.</summary>
public sealed class Choice
{
/// <summary>Uniquely identifies a Choice within an Option.</summary>
public readonly string Id;
public abstract IReadOnlyCollection<MiniYamlNode> GetSettings(ITerrainInfo terrainInfo);
/// <summary>The label to use for UI selection. Post-fluent.</summary>
public virtual IEnumerable<string> GetFluentReferences()
{
if (Label != null)
yield return Label;
}
}
public class MapGeneratorBooleanOption : MapGeneratorOption
{
[FieldLoader.Require]
public readonly string Parameter = null;
public readonly bool Default = false;
public bool Value;
public MapGeneratorBooleanOption(string id, MiniYaml yaml)
: base(id, yaml)
{
Value = Default;
}
public override IReadOnlyCollection<MiniYamlNode> GetSettings(ITerrainInfo terrainInfo)
{
return [new MiniYamlNode(Parameter, FieldSaver.FormatValue(Value))];
}
}
public class MapGeneratorIntegerOption : MapGeneratorOption
{
[FieldLoader.Require]
public readonly string Parameter = null;
public readonly int Default = 0;
public int Value;
public MapGeneratorIntegerOption(string id, MiniYaml yaml)
: base(id, yaml)
{
Value = Default;
}
public override IReadOnlyCollection<MiniYamlNode> GetSettings(ITerrainInfo terrainInfo)
{
return [new MiniYamlNode(Parameter, FieldSaver.FormatValue(Value))];
}
}
public class MapGeneratorMultiChoiceOption : MapGeneratorOption
{
public class MapGeneratorDropdownChoice
{
public readonly string Label = null;
public readonly string[] Tileset = null;
/// <summary>The tooltip to use for UI selection. Post-fluent.</summary>
public readonly string Description = null;
[FieldLoader.LoadUsing(nameof(LoadSettings))]
[FieldLoader.Require]
public readonly ImmutableList<MiniYamlNode> Settings = null;
/// <summary>
/// Only offer the Choice for these tilesets. (If null, show for all.)
/// </summary>
public readonly IReadOnlySet<string> Tileset = null;
/// <summary>(Partial) settings to combine into the final overall settings.</summary>
public readonly MiniYaml Settings;
public Choice(string id, MiniYaml my)
static ImmutableList<MiniYamlNode> LoadSettings(MiniYaml yaml)
{
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;
}
/// <summary>Create a choice that represents a top-level setting with a given value.</summary>
public Choice(string setting, string value)
{
Id = value;
Label = value;
Description = null;
Tileset = null;
Settings = new MiniYaml(null, [new MiniYamlNode(setting, value)]);
}
public static void DumpFluent(MiniYaml my, List<string> 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");
}
}
/// <summary>Check whether this choice is permitted for this map.</summary>
public bool Allowed(ITerrainInfo terrainInfo)
{
if (Tileset != null && !Tileset.Contains(terrainInfo.Id))
return false;
return true;
}
/// <summary>For single-setting choices, creates a new choice for that setting with a given value.</summary>
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);
return yaml.NodeWithKey("Settings").Value.Nodes.ToImmutableList();
}
}
public sealed class Option
[FieldLoader.LoadUsing(nameof(LoadChoices))]
public readonly Dictionary<string, MapGeneratorDropdownChoice> Choices = null;
static Dictionary<string, MapGeneratorDropdownChoice> LoadChoices(MiniYaml yaml)
{
/// <summary>Unique identifier for this Option.</summary>
[FieldLoader.Ignore]
public readonly string Id;
/// <summary>The label to use for UI selection. Post-fluent.</summary>
[FieldLoader.Ignore]
public readonly string Label = null;
/// <summary>
/// <para>For multiple-choice options (dropdowns/checkboxes) holds the allowed Choices.</para>
/// <para>For text entry Options, contains the default value.</para>
/// <para>If this is empty, the map is not compatible with the generator.</para>
/// </summary>
[FieldLoader.Ignore]
public readonly IReadOnlyList<Choice> Choices;
/// <summary>
/// <para>The default Choice for this option.</para>
/// <para>Default will be ignored if it is not valid for the map.</para>
/// </summary>
[FieldLoader.Ignore]
public readonly Choice Default;
public readonly double Min = double.NegativeInfinity;
public readonly double Max = double.PositiveInfinity;
/// <summary>Whether the Option can be randomized.</summary>
public readonly bool Random = false;
/// <summary>How an option should be treated for UI purposes.</summary>
[FieldLoader.Ignore]
public readonly UiType Ui;
/// <summary>Settings layering priority. Higher overrides lower.</summary>
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<Choice>();
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<string> 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<Option> Options;
/// <summary>
/// Parse settings from a MiniYaml definition. Returns null if the map isn't compatible.
/// </summary>
public static MapGeneratorSettings LoadSettings(MiniYaml my, ITerrainInfo terrainInfo)
{
var options = new List<Option>();
foreach (var node in my.Nodes)
var ret = new Dictionary<string, MapGeneratorDropdownChoice>();
foreach (var node in yaml.Nodes)
{
var split = node.Key.Split('@');
if (split[0] == "Option")
{
string id = null;
if (split.Length >= 2)
id = split[1];
var option = new Option(id, node.Value, terrainInfo);
if (option.Choices.Count == 0)
return null;
options.Add(option);
}
if (split.Length == 2 && split[0] == "Choice")
ret.Add(split[1], FieldLoader.Load<MapGeneratorDropdownChoice>(node.Value));
}
return new MapGeneratorSettings(options);
return ret;
}
public static List<string> DumpFluent(MiniYaml my)
public readonly string[] Default = null;
string value = null;
public MapGeneratorMultiChoiceOption(string id, MiniYaml yaml)
: base(id, yaml) { }
public override IReadOnlyCollection<MiniYamlNode> GetSettings(ITerrainInfo terrainInfo)
{
var references = new List<string>();
DumpFluent(my, references);
return references;
var validChoices = ValidChoices(terrainInfo);
if (validChoices.Contains(value))
return Choices[value].Settings;
var fallback = Default?.FirstOrDefault(validChoices.Contains) ?? validChoices.FirstOrDefault();
return fallback != null ? Choices[fallback].Settings : [];
}
public static void DumpFluent(MiniYaml my, List<string> references)
public string Value
{
foreach (var node in my.Nodes)
if (node.Key.Split('@')[0] == "Option")
Option.DumpFluent(node.Value, references);
}
MapGeneratorSettings(IReadOnlyList<Option> options)
{
Options = options;
}
public Dictionary<Option, Choice> DefaultChoices()
{
return Options.ToDictionary(option => option, option => option.Default);
}
/// <summary>Merge all choices into a complete settings MiniYaml.</summary>
public MiniYaml Compile(IReadOnlyDictionary<Option, Choice> choices)
{
var layers = new List<IReadOnlyCollection<MiniYamlNode>>();
// Apply the choices in their canonical order.
foreach (var option in Options.OrderBy(option => option.Priority))
get => value;
set
{
var choice = choices[option];
if (!option.ValidateChoice(choice))
throw new ArgumentException($"Option `{option.Id}` has illegal choice");
layers.Add(choice.Settings.Nodes);
}
if (!Choices.ContainsKey(value))
throw new ArgumentException($"{value} is not in the list of valid choices");
var settingsNodes = MiniYaml.Merge(layers);
return new MiniYaml(null, settingsNodes);
this.value = value;
}
}
public List<string> ValidChoices(ITerrainInfo terrainInfo)
{
return Choices
.Where(kv => kv.Value.Tileset == null || kv.Value.Tileset.Contains(terrainInfo.Id))
.Select(kv => kv.Key)
.ToList();
}
public override IEnumerable<string> GetFluentReferences()
{
if (Label != null)
yield return Label;
foreach (var c in Choices.Values)
{
if (c.Label == null)
continue;
yield return c.Label + ".label";
// Descriptions are optional
if (FluentProvider.TryGetMessage(c.Label + ".description", out _))
yield return c.Label + ".description";
}
}
}
public class MapGeneratorMultiIntegerChoiceOption : MapGeneratorOption
{
[FieldLoader.Require]
public readonly string Parameter = null;
[FieldLoader.Require]
public readonly int[] Choices = null;
public readonly int? Default = null;
int value;
public MapGeneratorMultiIntegerChoiceOption(string id, MiniYaml yaml)
: base(id, yaml)
{
Value = Default ?? Choices?.First() ?? 0;
}
public int Value
{
get => value;
set
{
if (!Choices.Contains(value))
throw new ArgumentException($"{value} is not in the list of valid choices");
this.value = value;
}
}
public override IReadOnlyCollection<MiniYamlNode> GetSettings(ITerrainInfo terrainInfo)
{
return [new MiniYamlNode(Parameter, FieldSaver.FormatValue(Value))];
}
}
public sealed class MapGeneratorSettings : IMapGeneratorSettings
{
public MapGeneratorSettings(MiniYaml yaml)
{
foreach (var node in yaml.Nodes)
{
var split = node.Key.Split('@');
if (split.Length != 2)
continue;
if (split[0] == "BooleanOption")
Options.Add(new MapGeneratorBooleanOption(split[1], node.Value));
else if (split[0] == "IntegerOption")
Options.Add(new MapGeneratorIntegerOption(split[1], node.Value));
else if (split[0] == "MultiIntegerChoiceOption")
Options.Add(new MapGeneratorMultiIntegerChoiceOption(split[1], node.Value));
else if (split[0] == "MultiChoiceOption")
Options.Add(new MapGeneratorMultiChoiceOption(split[1], node.Value));
}
}
public List<MapGeneratorOption> Options { get; } = [];
public void Randomize(MersenneTwister random)
{
if (Options.FirstOrDefault(o => o.Id == "Seed") is MapGeneratorIntegerOption seed)
seed.Value = random.Next();
}
public MiniYaml Compile(ITerrainInfo terrainInfo)
{
// Apply the choices in their canonical order.
var layers = Options
.OrderBy(option => option.Priority)
.Select(o => o.GetSettings(terrainInfo));
return new MiniYaml(null, MiniYaml.Merge(layers));
}
}
}

View File

@@ -11,6 +11,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Mods.Common.MapGenerator;
using OpenRA.Mods.Common.Terrain;
using OpenRA.Support;
@@ -50,12 +51,13 @@ namespace OpenRA.Mods.Common.Traits
static List<string> FluentReferencesLoader(MiniYaml my)
{
return MapGeneratorSettings.DumpFluent(my.NodeWithKey("Settings").Value);
return new MapGeneratorSettings(my.NodeWithKey("Settings").Value)
.Options.SelectMany(o => o.GetFluentReferences()).ToList();
}
public MapGeneratorSettings GetSettings(ITerrainInfo terrainInfo)
public IMapGeneratorSettings GetSettings()
{
return MapGeneratorSettings.LoadSettings(Settings, terrainInfo);
return new MapGeneratorSettings(Settings);
}
public void Generate(Map map, MiniYaml settings)

View File

@@ -50,7 +50,8 @@ namespace OpenRA.Mods.Common.Traits
static List<string> FluentReferencesLoader(MiniYaml my)
{
return MapGeneratorSettings.DumpFluent(my.NodeWithKey("Settings").Value);
return new MapGeneratorSettings(my.NodeWithKey("Settings").Value)
.Options.SelectMany(o => o.GetFluentReferences()).ToList();
}
const int FractionMax = 1000;
@@ -481,9 +482,9 @@ namespace OpenRA.Mods.Common.Traits
}
}
public MapGeneratorSettings GetSettings(ITerrainInfo terrainInfo)
public IMapGeneratorSettings GetSettings()
{
return MapGeneratorSettings.LoadSettings(Settings, terrainInfo);
return new MapGeneratorSettings(Settings);
}
public void Generate(Map map, MiniYaml settings)

View File

@@ -969,16 +969,23 @@ namespace OpenRA.Mods.Common.Traits
: base(message, inner) { }
}
public interface IMapGeneratorSettings
{
/// <summary>Returns the options that allow users to customise the result.</summary>
List<MapGeneratorOption> Options { get; }
void Randomize(MersenneTwister random);
/// <summary>Merge all choices into a complete settings MiniYaml.</summary>
MiniYaml Compile(ITerrainInfo terrainInfo);
}
public interface IMapGeneratorInfo : ITraitInfoInterface
{
string Type { get; }
string Name { get; }
/// <summary>
/// Get the generator settings available for this tileset.
/// Returns null if not compatible with the given tileset.
/// </summary>
MapGeneratorSettings GetSettings(ITerrainInfo terrainInfo);
IMapGeneratorSettings GetSettings();
/// <summary>
/// Generate or manipulate a supplied map in-place.

View File

@@ -15,6 +15,7 @@ using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text;
using OpenRA.Mods.Common.MapGenerator;
using OpenRA.Mods.Common.Traits;
namespace OpenRA.Mods.Common.UtilityCommands
@@ -204,9 +205,9 @@ namespace OpenRA.Mods.Common.UtilityCommands
.Select(variable => config.Choices[variable].Length)
.ToImmutableArray();
var generator =
modData.DefaultRules.Actors[SystemActors.EditorWorld].TraitInfos<IMapGeneratorInfo>()
.FirstOrDefault(info => info.Type == config.MapGeneratorType);
var generator = modData.DefaultRules.Actors[SystemActors.EditorWorld]
.TraitInfos<IMapGeneratorInfo>()
.FirstOrDefault(info => info.Type == config.MapGeneratorType);
if (generator == null)
throw new ArgumentException($"No map generator with type `{config.MapGeneratorType}`");
@@ -246,7 +247,7 @@ namespace OpenRA.Mods.Common.UtilityCommands
.Select(kv => $"{kv.Key}={kv.Value}")
.ToHashSet();
var tileset = modData.DefaultTerrainInfo[iterationChoices[Configuration.TilesetVariable]];
var terrainInfo = modData.DefaultTerrainInfo[iterationChoices[Configuration.TilesetVariable]];
iterationChoices.Remove(Configuration.TilesetVariable);
var size = iterationChoices[Configuration.SizeVariable]
@@ -263,26 +264,31 @@ namespace OpenRA.Mods.Common.UtilityCommands
if (size.Length != 2)
throw new ArgumentException($"bad map size `{iterationChoices[Configuration.SizeVariable]}`");
var map = new Map(modData, tileset, size[0], size[1]);
var map = new Map(modData, terrainInfo, size[0], size[1]);
var maxTerrainHeight = map.Grid.MaximumTerrainHeight;
var tl = new PPos(1, 1 + maxTerrainHeight);
var br = new PPos(size[0], size[1] + maxTerrainHeight);
map.SetBounds(tl, br);
var settings = generator.GetSettings(tileset);
var choices = settings.DefaultChoices();
foreach (var option in choices.Keys)
if (iterationChoices.TryGetValue(option.Id, out var choice))
var settings = generator.GetSettings();
foreach (var o in settings.Options)
{
if (iterationChoices.TryGetValue(o.Id, out var choice))
{
choices[option] = option.IsFreeform()
? choices[option].NewValue(choice)
: option.Choices.First(c => c.Id == choice);
iterationChoices.Remove(option.Id);
if (o is MapGeneratorBooleanOption bo)
bo.Value = FieldLoader.GetValue<bool>("choice", choice);
else if (o is MapGeneratorIntegerOption io)
io.Value = FieldLoader.GetValue<int>("choice", choice);
else if (o is MapGeneratorMultiIntegerChoiceOption mio)
mio.Value = FieldLoader.GetValue<int>("choice", choice);
else if (o is MapGeneratorMultiChoiceOption mo)
mo.Value = choice;
iterationChoices.Remove(o.Id);
}
else if (config.NoDefaults)
{
throw new ArgumentException($"No choices specified for option `{option.Id}`");
}
throw new ArgumentException($"No choices specified for option `{o.Id}`");
}
if (iterationChoices.Count != 0)
throw new ArgumentException($"Unknown options: {string.Join(", ", iterationChoices.Keys)}");
@@ -292,8 +298,7 @@ namespace OpenRA.Mods.Common.UtilityCommands
tests++;
try
{
var my = settings.Compile(choices);
generator.Generate(map, my);
generator.Generate(map, settings.Compile(terrainInfo));
}
catch (Exception e) when (e is MapGenerationException || e is YamlException)
{

View File

@@ -27,24 +27,18 @@ namespace OpenRA.Mods.Common.Widgets.Logic
[FluentReference("name")]
const string StrGenerated = "notification-map-generator-generated";
[FluentReference]
const string StrBadOption = "notification-map-generator-bad-option";
[FluentReference]
const string StrFailed = "notification-map-generator-failed";
[FluentReference]
const string StrFailedCancel = "label-map-generator-failed-cancel";
readonly EditorActionManager editorActionManager;
readonly ButtonWidget generateButtonWidget;
readonly ButtonWidget generateRandomButtonWidget;
readonly World world;
readonly WorldRenderer worldRenderer;
readonly ModData modData;
// nullable
IMapGeneratorInfo selectedGenerator;
readonly Dictionary<IMapGeneratorInfo, MapGeneratorSettings> generatorsToSettings;
readonly Dictionary<IMapGeneratorInfo, Dictionary<MapGeneratorSettings.Option, MapGeneratorSettings.Choice>> generatorsToSettingsChoices;
IMapGeneratorSettings selectedSettings;
readonly ScrollPanelWidget settingsPanel;
readonly Widget checkboxSettingTemplate;
@@ -60,42 +54,25 @@ namespace OpenRA.Mods.Common.Widgets.Logic
this.worldRenderer = worldRenderer;
this.modData = modData;
selectedGenerator = null;
generatorsToSettings = [];
generatorsToSettingsChoices = [];
var mapGenerators = new List<IMapGeneratorInfo>();
var terrainInfo = modData.DefaultTerrainInfo[world.Map.Tileset];
foreach (var generator in world.Map.Rules.Actors[SystemActors.EditorWorld].TraitInfos<IMapGeneratorInfo>())
{
var settings = generator.GetSettings(terrainInfo);
if (settings == null)
continue;
var choices = settings.DefaultChoices();
mapGenerators.Add(generator);
generatorsToSettingsChoices.Add(generator, choices);
generatorsToSettings.Add(generator, settings);
}
generateButtonWidget = widget.Get<ButtonWidget>("GENERATE_BUTTON");
generateRandomButtonWidget = widget.Get<ButtonWidget>("GENERATE_RANDOM_BUTTON");
settingsPanel = widget.Get<ScrollPanelWidget>("SETTINGS_PANEL");
checkboxSettingTemplate = settingsPanel.Get<Widget>("CHECKBOX_TEMPLATE");
textSettingTemplate = settingsPanel.Get<Widget>("TEXT_TEMPLATE");
dropDownSettingTemplate = settingsPanel.Get<Widget>("DROPDOWN_TEMPLATE");
var generateButtonWidget = widget.Get<ButtonWidget>("GENERATE_BUTTON");
generateButtonWidget.OnClick = GenerateMap;
var generateRandomButtonWidget = widget.Get<ButtonWidget>("GENERATE_RANDOM_BUTTON");
generateRandomButtonWidget.OnClick = () =>
{
Randomize();
selectedSettings?.Randomize(world.LocalRandom);
UpdateSettingsUi();
GenerateMap();
};
var generatorDropDown = widget.Get<DropDownButtonWidget>("GENERATOR");
ChangeGenerator(mapGenerators.FirstOrDefault());
if (selectedGenerator != null)
var mapGenerators = world.Map.Rules.Actors[SystemActors.EditorWorld].TraitInfos<IMapGeneratorInfo>();
if (mapGenerators.Count > 0)
{
var label = new CachedTransform<IMapGeneratorInfo, string>(g => FluentProvider.GetMessage(g.Name));
generatorDropDown.GetText = () => label.Update(selectedGenerator);
@@ -104,7 +81,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
ScrollItemWidget SetupItem(IMapGeneratorInfo g, ScrollItemWidget template)
{
bool IsSelected() => g.Type == selectedGenerator.Type;
void OnClick() => ChangeGenerator(mapGenerators.First(generator => generator.Type == g.Type));
void OnClick() => ChangeGenerator(g);
var item = ScrollItemWidget.Setup(template, IsSelected, OnClick);
var label = FluentProvider.GetMessage(g.Name);
item.Get<LabelWidget>("LABEL").GetText = () => label;
@@ -120,6 +97,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
generateRandomButtonWidget.IsDisabled = () => true;
generatorDropDown.IsDisabled = () => true;
}
ChangeGenerator(mapGenerators.FirstOrDefault());
}
sealed class RandomMapEditorAction : IEditorAction
@@ -155,6 +134,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
void ChangeGenerator(IMapGeneratorInfo newGenerator)
{
selectedGenerator = newGenerator;
selectedSettings = newGenerator?.GetSettings();
UpdateSettingsUi();
}
@@ -166,89 +146,119 @@ namespace OpenRA.Mods.Common.Widgets.Logic
if (selectedGenerator == null)
return;
var settings = generatorsToSettings[selectedGenerator];
var choices = generatorsToSettingsChoices[selectedGenerator];
foreach (var option in settings.Options)
foreach (var o in selectedSettings.Options)
{
Widget settingWidget;
switch (option.Ui)
Widget settingWidget = null;
switch (o)
{
case MapGeneratorSettings.UiType.Hidden:
continue;
case MapGeneratorBooleanOption bo:
{
settingWidget = checkboxSettingTemplate.Clone();
var checkboxWidget = settingWidget.Get<CheckboxWidget>("CHECKBOX");
var label = FluentProvider.GetMessage(bo.Label);
checkboxWidget.GetText = () => label;
checkboxWidget.IsChecked = () => bo.Value;
checkboxWidget.OnClick = () => bo.Value ^= true;
break;
}
case MapGeneratorSettings.UiType.DropDown:
case MapGeneratorIntegerOption io:
{
settingWidget = textSettingTemplate.Clone();
var labelWidget = settingWidget.Get<LabelWidget>("LABEL");
var label = FluentProvider.GetMessage(io.Label);
labelWidget.GetText = () => label;
var textFieldWidget = settingWidget.Get<TextFieldWidget>("INPUT");
textFieldWidget.Type = TextFieldType.Integer;
textFieldWidget.Text = FieldSaver.FormatValue(io.Value);
textFieldWidget.OnTextEdited = () =>
{
var valid = int.TryParse(textFieldWidget.Text, out io.Value);
textFieldWidget.IsValid = () => valid;
};
textFieldWidget.OnEscKey = _ => { textFieldWidget.YieldKeyboardFocus(); return true; };
textFieldWidget.OnEnterKey = _ => { textFieldWidget.YieldKeyboardFocus(); return true; };
break;
}
case MapGeneratorMultiIntegerChoiceOption mio:
{
settingWidget = dropDownSettingTemplate.Clone();
var label = settingWidget.Get<LabelWidget>("LABEL");
label.GetText = () => option.Label;
var dropDown = settingWidget.Get<DropDownButtonWidget>("DROPDOWN");
dropDown.GetText = () => choices[option].Label;
dropDown.OnMouseDown = _ =>
{
ScrollItemWidget SetupItem(MapGeneratorSettings.Choice choice, ScrollItemWidget template)
{
bool IsSelected() => choice == choices[option];
void OnClick() => choices[option] = choice;
var item = ScrollItemWidget.Setup(template, IsSelected, OnClick);
item.Get<LabelWidget>("LABEL").GetText = () => choice.Label;
item.GetTooltipText =
choice.Description != null
? () => choice.Description
: null;
var labelWidget = settingWidget.Get<LabelWidget>("LABEL");
var label = FluentProvider.GetMessage(mio.Label);
labelWidget.GetText = () => label;
var labelCache = new CachedTransform<int, string>(v => FieldSaver.FormatValue(v));
var dropDownWidget = settingWidget.Get<DropDownButtonWidget>("DROPDOWN");
dropDownWidget.GetText = () => labelCache.Update(mio.Value);
dropDownWidget.OnMouseDown = _ =>
{
ScrollItemWidget SetupItem(int choice, ScrollItemWidget template)
{
bool IsSelected() => choice == mio.Value;
void OnClick() => mio.Value = choice;
var item = ScrollItemWidget.Setup(template, IsSelected, OnClick);
var itemLabel = FieldSaver.FormatValue(choice);
item.Get<LabelWidget>("LABEL").GetText = () => itemLabel;
item.GetTooltipText = null;
return item;
}
dropDown.ShowDropDown("LABEL_DROPDOWN_WITH_TOOLTIP_TEMPLATE", option.Choices.Count * 30, option.Choices, SetupItem);
dropDownWidget.ShowDropDown("LABEL_DROPDOWN_WITH_TOOLTIP_TEMPLATE", mio.Choices.Length * 30, mio.Choices, SetupItem);
};
break;
}
case MapGeneratorSettings.UiType.Checkbox:
case MapGeneratorMultiChoiceOption mo:
{
if (option.Choices.Count != 2)
throw new InvalidOperationException("Checkbox option that does not have two choices");
settingWidget = checkboxSettingTemplate.Clone();
var checkbox = settingWidget.Get<CheckboxWidget>("CHECKBOX");
checkbox.GetText = () => option.Label;
checkbox.IsChecked = () => choices[option] == option.Choices[1];
checkbox.OnClick = () =>
choices[option] =
choices[option] == option.Choices[1]
? option.Choices[0]
: option.Choices[1];
break;
}
var validChoices = mo.ValidChoices(world.Map.Rules.TerrainInfo);
if (!validChoices.Contains(mo.Value))
mo.Value = mo.Default?.FirstOrDefault(validChoices.Contains) ?? validChoices.FirstOrDefault();
case MapGeneratorSettings.UiType.Integer:
case MapGeneratorSettings.UiType.Float:
case MapGeneratorSettings.UiType.String:
{
if (option.Choices.Count != 1)
throw new InvalidOperationException("Text option that does not have one choice");
settingWidget = textSettingTemplate.Clone();
var label = settingWidget.Get<LabelWidget>("LABEL");
label.GetText = () => option.Label;
var input = settingWidget.Get<TextFieldWidget>("INPUT");
input.Text = choices[option].Id;
input.OnTextEdited = () =>
if (mo.Label != null && validChoices.Count > 0)
{
var choice = choices[option].NewValue(input.Text);
choices[option] = choice;
var valid = option.ValidateChoice(choice);
input.IsValid = () => valid;
};
settingWidget = dropDownSettingTemplate.Clone();
var labelWidget = settingWidget.Get<LabelWidget>("LABEL");
var label = FluentProvider.GetMessage(mo.Label);
labelWidget.GetText = () => label;
var labelCache = new CachedTransform<string, string>(v => FluentProvider.GetMessage(mo.Choices[v].Label + ".label"));
var dropDownWidget = settingWidget.Get<DropDownButtonWidget>("DROPDOWN");
dropDownWidget.GetText = () => labelCache.Update(mo.Value);
dropDownWidget.OnMouseDown = _ =>
{
ScrollItemWidget SetupItem(string choice, ScrollItemWidget template)
{
bool IsSelected() => choice == mo.Value;
void OnClick() => mo.Value = choice;
var item = ScrollItemWidget.Setup(template, IsSelected, OnClick);
var itemLabel = FluentProvider.GetMessage(mo.Choices[choice].Label + ".label");
item.Get<LabelWidget>("LABEL").GetText = () => itemLabel;
if (FluentProvider.TryGetMessage(mo.Choices[choice].Label + ".description", out var desc))
item.GetTooltipText = () => desc;
else
item.GetTooltipText = null;
return item;
}
dropDownWidget.ShowDropDown("LABEL_DROPDOWN_WITH_TOOLTIP_TEMPLATE", validChoices.Count * 30, validChoices, SetupItem);
};
}
input.OnEscKey = _ => { input.YieldKeyboardFocus(); return true; };
input.OnEnterKey = _ => { input.YieldKeyboardFocus(); return true; };
break;
}
default:
throw new NotSupportedException("Unsupported MapGeneratorSettings.UiType");
throw new NotImplementedException($"Unhandled MapGeneratorOption type {o.GetType().Name}");
}
if (settingWidget == null)
continue;
settingWidget.IsVisible = () => true;
settingsPanel.AddChild(settingWidget);
}
@@ -266,18 +276,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic
cancelText: StrFailedCancel);
}
void Randomize()
{
var choices = generatorsToSettingsChoices[selectedGenerator];
foreach (var option in choices.Keys)
{
if (option.Random)
choices[option] = option.RandomChoice();
}
UpdateSettingsUi();
}
void GenerateMap()
{
try
@@ -293,22 +291,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic
void GenerateMapMayThrow()
{
var map = world.Map;
var tileset = modData.DefaultTerrainInfo[map.Tileset];
var generatedMap = new Map(modData, tileset, map.MapSize.X, map.MapSize.Y);
var terrainInfo = map.Rules.TerrainInfo;
var generatedMap = new Map(modData, terrainInfo, map.MapSize.X, map.MapSize.Y);
var bounds = map.Bounds;
generatedMap.SetBounds(new PPos(bounds.Left, bounds.Top), new PPos(bounds.Right - 1, bounds.Bottom - 1));
var choices = generatorsToSettingsChoices[selectedGenerator];
foreach (var optionChoice in choices)
{
var option = optionChoice.Key;
var choice = optionChoice.Value;
if (!option.ValidateChoice(choice))
throw new MapGenerationException(
FluentProvider.GetMessage(StrBadOption, "option", option.Label));
}
var settings = generatorsToSettings[selectedGenerator].Compile(choices);
var settings = selectedSettings.Compile(terrainInfo);
// Run main generator logic. May throw.
var generateStopwatch = Stopwatch.StartNew();

View File

@@ -3,7 +3,7 @@
Type: experimental
Name: map-generator-experimental
Settings:
Option@hidden_defaults:
MultiChoiceOption@hidden_defaults:
Choice@hidden_defaults:
Settings:
TerrainFeatureSize: 20480
@@ -71,7 +71,7 @@
ForestObstacles: Trees
UnplayableObstacles: Obstructions
CivilianBuildingsObstacles: CivilianBuildings
Option@hidden_tileset_overrides:
MultiChoiceOption@hidden_tileset_overrides:
Choice@common:
Tileset: DESERT,SNOW,TEMPERAT
Settings:
@@ -87,12 +87,11 @@
RepaintTiles:
255: Snow
1: Water
Option@Seed:
IntegerOption@Seed:
Label: label-cnc-map-generator-option-seed
Random: True
Parameter: Seed
Default: 0
Integer: Seed
Option@TerrainType:
MultiChoiceOption@TerrainType:
Label: label-cnc-map-generator-option-terrain-type
Priority: 2
Default: Gardens,Rocky
@@ -170,13 +169,13 @@
Mountains: 1000
Roughness: 850
MinimumTerrainContourSpacing: 5
Option@Rotations:
MultiIntegerChoiceOption@Rotations:
Label: label-cnc-map-generator-option-rotations
SimpleChoice: Rotations
Values: 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16
Parameter: Rotations
Choices: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
Default: 2
Priority: 1
Option@Mirror:
MultiChoiceOption@Mirror:
Label: label-cnc-map-generator-option-mirror
Default: None
Priority: 1
@@ -200,7 +199,7 @@
Label: label-cnc-map-generator-choice-mirror-top-right-matches-bottom-left
Settings:
Mirror: TopRightMatchesBottomLeft
Option@Shape:
MultiChoiceOption@Shape:
Label: label-cnc-map-generator-option-shape
Default: Square
Priority: 1
@@ -217,13 +216,13 @@
Tileset: DESERT
Settings:
ExternalCircularBias: -1
Option@Players:
MultiIntegerChoiceOption@Players:
Label: label-cnc-map-generator-option-players
SimpleChoice: Players
Values: 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16
Parameter: Players
Choices: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
Default: 1
Priority: 1
Option@Resources:
MultiChoiceOption@Resources:
Label: label-cnc-map-generator-option-resources
Default: Medium
Choice@None:
@@ -285,7 +284,7 @@
ResourceSpawnWeights:
MaximumExpansionResourceSpawns: 0
MaximumResourceSpawnsPerExpansion: 1
Option@Buildings:
MultiChoiceOption@Buildings:
Label: label-cnc-map-generator-option-buildings
Default: Standard
Choice@None:
@@ -327,7 +326,7 @@
MaximumBuildings: 10
BuildingWeights:
v19: 1
Option@Density:
MultiChoiceOption@Density:
Label: label-cnc-map-generator-option-density
Default: Players
Priority: 1
@@ -366,17 +365,17 @@
Settings:
AreaEntityBonus: 800
PlayerCountEntityBonus: 0
Option@DenyWalledArea:
BooleanOption@DenyWalledArea:
Label: label-cnc-map-generator-option-deny-walled-areas
Checkbox: DenyWalledAreas
Parameter: DenyWalledAreas
Default: True
Priority: 1
Option@Roads:
BooleanOption@Roads:
Label: label-cnc-map-generator-option-roads
Checkbox: Roads
Parameter: Roads
Default: True
Priority: 1
Option@CivilianDensity:
MultiChoiceOption@CivilianDensity:
Label: label-cnc-map-generator-option-civilian-density
Default: Default
Priority: 3
@@ -411,7 +410,7 @@
Type: clear
Name: map-generator-clear
Settings:
Option@Tile:
MultiChoiceOption@Tile:
Label: label-clear-map-generator-option-tile
Choice@CommonClear:
Label: label-clear-map-generator-choice-tile-clear

View File

@@ -1142,5 +1142,4 @@ keycode =
## MapGeneratorToolLogic
label-map-generator-failed-cancel = Dismiss
notification-map-generator-generated = Generated using { $name }
notification-map-generator-bad-option = Option "{ $option }" is invalid
notification-map-generator-failed = Map generation failed

View File

@@ -3,7 +3,7 @@
Type: experimental
Name: map-generator-experimental
Settings:
Option@hidden_defaults:
MultiChoiceOption@hidden_defaults:
Choice@hidden_defaults:
Settings:
TerrainFeatureSize: 20480
@@ -70,7 +70,7 @@
ForestObstacles: Trees
UnplayableObstacles: Obstructions
CivilianBuildingsObstacles: CivilianBuildings
Option@hidden_tileset_overrides:
MultiChoiceOption@hidden_tileset_overrides:
Choice@desert:
Tileset: DESERT
Settings:
@@ -86,12 +86,11 @@
Settings:
LandTile: 255
WaterTile: 1
Option@Seed:
IntegerOption@Seed:
Label: label-ra-map-generator-option-seed
Random: True
Parameter: Seed
Default: 0
Integer: Seed
Option@TerrainType:
MultiChoiceOption@TerrainType:
Label: label-ra-map-generator-option-terrain-type
Priority: 2
Default: Gardens
@@ -193,13 +192,13 @@
Forests: 0
SpawnBuildSize: 6
MinimumSpawnRadius: 4
Option@Rotations:
MultiIntegerChoiceOption@Rotations:
Label: label-ra-map-generator-option-rotations
SimpleChoice: Rotations
Values: 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16
Parameter: Rotations
Choices: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
Default: 2
Priority: 1
Option@Mirror:
MultiChoiceOption@Mirror:
Label: label-ra-map-generator-option-mirror
Default: None
Priority: 1
@@ -223,7 +222,7 @@
Label: label-ra-map-generator-choice-mirror-top-right-matches-bottom-left
Settings:
Mirror: TopRightMatchesBottomLeft
Option@Shape:
MultiChoiceOption@Shape:
Label: label-ra-map-generator-option-shape
Default: Square
Priority: 1
@@ -239,13 +238,13 @@
Label: label-ra-map-generator-choice-shape-circle-water
Settings:
ExternalCircularBias: -1
Option@Players:
MultiIntegerChoiceOption@Players:
Label: label-ra-map-generator-option-players
SimpleChoice: Players
Values: 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16
Parameter: Players
Choices: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
Default: 1
Priority: 1
Option@Resources:
MultiChoiceOption@Resources:
Label: label-ra-map-generator-option-resources
Default: Medium
Choice@None:
@@ -303,7 +302,7 @@
ResourceSpawnWeights:
MaximumExpansionResourceSpawns: 0
MaximumResourceSpawnsPerExpansion: 1
Option@Buildings:
MultiChoiceOption@Buildings:
Label: label-ra-map-generator-option-buildings
Default: Standard
Choice@None:
@@ -346,7 +345,7 @@
MaximumBuildings: 10
BuildingWeights:
oilb: 1
Option@Density:
MultiChoiceOption@Density:
Label: label-ra-map-generator-option-density
Default: Players
Priority: 1
@@ -385,17 +384,17 @@
Settings:
AreaEntityBonus: 800
PlayerCountEntityBonus: 0
Option@DenyWalledArea:
BooleanOption@DenyWalledArea:
Label: label-ra-map-generator-option-deny-walled-areas
Checkbox: DenyWalledAreas
Parameter: DenyWalledAreas
Default: True
Priority: 1
Option@Roads:
BooleanOption@Roads:
Label: label-ra-map-generator-option-roads
Checkbox: Roads
Parameter: Roads
Default: True
Priority: 1
Option@CivilianDensity:
MultiChoiceOption@CivilianDensity:
Label: label-ra-map-generator-option-civilian-density
Default: Default
Priority: 3
@@ -430,7 +429,7 @@
Type: clear
Name: map-generator-clear
Settings:
Option@Tile:
MultiChoiceOption@Tile:
Label: label-clear-map-generator-option-tile
Choice@DesertClear:
Label: label-clear-map-generator-choice-tile-clear