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>
This commit is contained in:
463
OpenRA.Mods.Common/Widgets/Logic/MapGeneratorLogic.cs
Normal file
463
OpenRA.Mods.Common/Widgets/Logic/MapGeneratorLogic.cs
Normal file
@@ -0,0 +1,463 @@
|
||||
#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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user