Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
464 lines
15 KiB
C#
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);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|