Files
OpenRA/OpenRA.Mods.Common/Widgets/Logic/MapGeneratorLogic.cs
let5sne.win10 9cf6ebb986
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled
Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:46:54 +08:00

464 lines
15 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using OpenRA.FileSystem;
using OpenRA.Mods.Common.MapGenerator;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets.Logic
{
public class MapGeneratorLogic : ChromeLogic
{
[FluentReference]
const string Tileset = "label-mapchooser-random-map-tileset";
[FluentReference]
const string MapSize = "label-mapchooser-random-map-size";
[FluentReference]
const string RandomMap = "label-mapchooser-random-map-title";
[FluentReference]
const string Generating = "label-mapchooser-random-map-generating";
[FluentReference]
const string GenerationFailed = "label-mapchooser-random-map-error";
[FluentReference("players")]
const string Players = "label-player-count";
[FluentReference("author")]
const string CreatedBy = "label-created-by";
[FluentReference]
const string MapSizeSmall = "label-map-size-small";
[FluentReference]
const string MapSizeMedium = "label-map-size-medium";
[FluentReference]
const string MapSizeLarge = "label-map-size-large";
[FluentReference]
const string MapSizeHuge = "label-map-size-huge";
public static readonly IReadOnlyDictionary<string, int2> MapSizes = new Dictionary<string, int2>()
{
{ MapSizeSmall, new int2(48, 60) },
{ MapSizeMedium, new int2(60, 90) },
{ MapSizeLarge, new int2(90, 120) },
{ MapSizeHuge, new int2(120, 160) },
};
readonly ModData modData;
readonly IEditorMapGeneratorInfo generator;
readonly IMapGeneratorSettings settings;
readonly Action<MapGenerationArgs, IReadWritePackage> onGenerate;
readonly GeneratedMapPreviewWidget preview;
readonly ScrollPanelWidget settingsPanel;
readonly Widget checkboxSettingTemplate;
readonly Widget textSettingTemplate;
readonly Widget dropdownSettingTemplate;
readonly Widget tilesetSetting;
readonly Widget sizeSetting;
readonly Widget parentWidget;
ITerrainInfo selectedTerrain;
string selectedSize;
Size size;
bool initialGenerationDone;
volatile bool failed;
volatile uint generationCounter = 0;
volatile uint lastGeneration = 0;
bool IsGenerating => lastGeneration != generationCounter;
[ObjectCreator.UseCtor]
internal MapGeneratorLogic(Widget widget, ModData modData, MapGenerationArgs initialSettings, Action<MapGenerationArgs, IReadWritePackage> onGenerate)
{
this.modData = modData;
this.onGenerate = onGenerate;
parentWidget = widget.Parent;
generator = modData.DefaultRules.Actors[SystemActors.EditorWorld].TraitInfos<IEditorMapGeneratorInfo>().First();
settings = generator.GetSettings();
preview = widget.Get<GeneratedMapPreviewWidget>("PREVIEW");
widget.Get("ERROR").IsVisible = () => failed;
var title = new CachedTransform<string, string>(id => FluentProvider.GetMessage(id));
var previewTitleLabel = widget.Get<LabelWidget>("TITLE");
previewTitleLabel.GetText = () => title.Update(IsGenerating ? Generating : failed ? GenerationFailed : RandomMap);
var previewDetailsLabel = widget.GetOrNull<LabelWidget>("DETAILS");
if (previewDetailsLabel != null)
{
// The default "Conquest" label is hardcoded in Map.cs
var desc = new CachedTransform<int, string>(p => "Conquest " + FluentProvider.GetMessage(Players, "players", p));
var playersOption = settings.Options.FirstOrDefault(o => o.Id == "Players") as MapGeneratorMultiIntegerChoiceOption;
previewDetailsLabel.GetText = () => desc.Update(playersOption?.Value ?? 0);
previewDetailsLabel.IsVisible = () => !failed;
}
var previewAuthorLabel = widget.GetOrNull<LabelWithTooltipWidget>("AUTHOR");
if (previewAuthorLabel != null)
{
var desc = FluentProvider.GetMessage(CreatedBy, "author", FluentProvider.GetMessage(generator.Name));
previewAuthorLabel.GetText = () => desc;
previewAuthorLabel.IsVisible = () => !failed;
}
var previewSizeLabel = widget.GetOrNull<LabelWidget>("SIZE");
if (previewSizeLabel != null)
{
var desc = new CachedTransform<Size, string>(MapChooserLogic.MapSizeLabel);
previewSizeLabel.IsVisible = () => !failed;
previewSizeLabel.GetText = () => desc.Update(size);
}
settingsPanel = widget.Get<ScrollPanelWidget>("SETTINGS_PANEL");
checkboxSettingTemplate = settingsPanel.Get<Widget>("CHECKBOX_TEMPLATE");
textSettingTemplate = settingsPanel.Get<Widget>("TEXT_TEMPLATE");
dropdownSettingTemplate = settingsPanel.Get<Widget>("DROPDOWN_TEMPLATE");
settingsPanel.Layout = new GridLayout(settingsPanel);
// Tileset and map size are handled outside the generator logic so must be created manually
var validTerrainInfos = generator.Tilesets.Select(t => modData.DefaultTerrainInfo[t]).ToList();
var tilesetLabel = FluentProvider.GetMessage(Tileset);
tilesetSetting = dropdownSettingTemplate.Clone();
tilesetSetting.Get<LabelWidget>("LABEL").GetText = () => tilesetLabel;
var label = new CachedTransform<ITerrainInfo, string>(ti => FluentProvider.GetMessage(ti.Name));
var tilesetDropdown = tilesetSetting.Get<DropDownButtonWidget>("DROPDOWN");
tilesetDropdown.GetText = () => label.Update(selectedTerrain);
tilesetDropdown.OnMouseDown = _ =>
{
ScrollItemWidget SetupItem(ITerrainInfo terrainInfo, ScrollItemWidget template)
{
bool IsSelected() => terrainInfo == selectedTerrain;
void OnClick()
{
selectedTerrain = terrainInfo;
RefreshSettings();
GenerateMap();
}
var item = ScrollItemWidget.Setup(template, IsSelected, OnClick);
var itemLabel = FluentProvider.GetMessage(terrainInfo.Name);
item.Get<LabelWidget>("LABEL").GetText = () => itemLabel;
return item;
}
tilesetDropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", validTerrainInfos.Count * 30, validTerrainInfos, SetupItem);
};
var sizeLabel = FluentProvider.GetMessage(MapSize);
sizeSetting = dropdownSettingTemplate.Clone();
sizeSetting.Get<LabelWidget>("LABEL").GetText = () => sizeLabel;
var sizeDropdown = sizeSetting.Get<DropDownButtonWidget>("DROPDOWN");
var sizeDropdownLabel = new CachedTransform<string, string>(s => FluentProvider.GetMessage(s));
sizeDropdown.GetText = () => sizeDropdownLabel.Update(selectedSize);
sizeDropdown.OnMouseDown = _ =>
{
ScrollItemWidget SetupItem(string size, ScrollItemWidget template)
{
bool IsSelected() => size == selectedSize;
void OnClick()
{
selectedSize = size;
RandomizeSize();
GenerateMap();
}
var item = ScrollItemWidget.Setup(template, IsSelected, OnClick);
var label = FluentProvider.GetMessage(size);
item.Get<LabelWidget>("LABEL").GetText = () => label;
return item;
}
sizeDropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", MapSizes.Count * 30, MapSizes.Keys, SetupItem);
};
var generateButton = widget.Get<ButtonWidget>("BUTTON_GENERATE");
generateButton.IsDisabled = () => IsGenerating;
generateButton.OnClick = () =>
{
settings.Randomize(Game.CosmeticRandom);
RandomizeSize();
GenerateMap();
};
selectedSize = MapSizes.Keys.Skip(1).First();
if (initialSettings != null)
{
selectedTerrain = modData.DefaultTerrainInfo[initialSettings.Tileset];
size = initialSettings.Size;
foreach (var kv in MapSizes)
if (kv.Value.X > size.Width && kv.Value.Y <= size.Width)
selectedSize = kv.Key;
settings.Initialize(initialSettings);
RefreshSettings();
var map = modData.MapCache[initialSettings.Uid];
if (map.Status == MapStatus.Available)
{
preview.Update(map);
initialGenerationDone = true;
onGenerate(initialSettings, null);
}
}
else
{
selectedTerrain = validTerrainInfos[0];
settings.Randomize(Game.CosmeticRandom);
RandomizeSize();
RefreshSettings();
}
}
public override void Tick()
{
if (!initialGenerationDone && !IsGenerating && parentWidget.IsVisible())
{
initialGenerationDone = true;
GenerateMap();
}
}
void RandomizeSize()
{
var mapGrid = modData.GetOrCreate<MapGrid>();
var sizeRange = MapSizes[selectedSize];
var width = Game.CosmeticRandom.Next(sizeRange.X, sizeRange.Y);
var height =
mapGrid.Type == MapGridType.RectangularIsometric
? width * 2
: width;
size = new Size(width + 2, height + mapGrid.MaximumTerrainHeight * 2 + 2);
}
void RefreshSettings()
{
settingsPanel.RemoveChildren();
tilesetSetting.Bounds = sizeSetting.Bounds = dropdownSettingTemplate.Bounds;
settingsPanel.AddChild(tilesetSetting);
settingsPanel.AddChild(sizeSetting);
var playerCount = settings.PlayerCount;
foreach (var o in settings.Options)
{
if (o.Id == "Seed")
continue;
Widget settingWidget = null;
switch (o)
{
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;
GenerateMap();
};
break;
}
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; };
textFieldWidget.OnLoseFocus = GenerateMap;
break;
}
case MapGeneratorMultiIntegerChoiceOption mio:
{
settingWidget = dropdownSettingTemplate.Clone();
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;
if (o.Id == "Players")
RefreshSettings();
GenerateMap();
}
var item = ScrollItemWidget.Setup(template, IsSelected, OnClick);
var itemLabel = FieldSaver.FormatValue(choice);
item.Get<LabelWidget>("LABEL").GetText = () => itemLabel;
item.GetTooltipText = null;
return item;
}
dropDownWidget.ShowDropDown("LABEL_DROPDOWN_WITH_TOOLTIP_TEMPLATE", mio.Choices.Length * 30, mio.Choices, SetupItem);
};
break;
}
case MapGeneratorMultiChoiceOption mo:
{
var validChoices = mo.ValidChoices(selectedTerrain, playerCount);
if (!validChoices.Contains(mo.Value))
{
if (mo.Default != null)
mo.Value = mo.Default.FirstOrDefault(validChoices.Contains);
mo.Value ??= validChoices.FirstOrDefault();
}
if (mo.Label != null && validChoices.Count > 0)
{
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;
GenerateMap();
}
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);
};
}
break;
}
default:
throw new NotImplementedException($"Unhandled MapGeneratorOption type {o.GetType().Name}");
}
if (settingWidget == null)
continue;
settingWidget.IsVisible = () => true;
settingsPanel.AddChild(settingWidget);
}
}
void GenerateMap()
{
var currentGeneration = Interlocked.Increment(ref generationCounter);
failed = false;
onGenerate(null, null);
preview.Clear();
Task.Run(() =>
{
// Tasks don't run in parallel, so we may be able to cancel some outdated requests here.
if (currentGeneration != generationCounter)
return;
MapGenerationArgs args;
Map map;
try
{
args = settings.Compile(selectedTerrain, size);
map = generator.Generate(modData, args);
}
catch (MapGenerationException)
{
// We are the lastest generation request, mark as failed.
if (currentGeneration == generationCounter)
{
lastGeneration = currentGeneration;
failed = true;
}
return;
}
// Need to invoke widgets from the main thread.
Game.RunAfterTick(() =>
{
// A newer generation will be set after us, discard.
if (currentGeneration == generationCounter)
{
var package = new ZipFileLoader.ReadWriteZipFile();
map.Save(package);
args.Uid = map.Uid;
preview.Update(map);
lastGeneration = currentGeneration;
// `onGenerate` assumed to take ownership of package here.
onGenerate(args, package);
}
});
});
}
}
}