#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 MapSizes = new Dictionary() { { 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 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 onGenerate) { this.modData = modData; this.onGenerate = onGenerate; parentWidget = widget.Parent; generator = modData.DefaultRules.Actors[SystemActors.EditorWorld].TraitInfos().First(); settings = generator.GetSettings(); preview = widget.Get("PREVIEW"); widget.Get("ERROR").IsVisible = () => failed; var title = new CachedTransform(id => FluentProvider.GetMessage(id)); var previewTitleLabel = widget.Get("TITLE"); previewTitleLabel.GetText = () => title.Update(IsGenerating ? Generating : failed ? GenerationFailed : RandomMap); var previewDetailsLabel = widget.GetOrNull("DETAILS"); if (previewDetailsLabel != null) { // The default "Conquest" label is hardcoded in Map.cs var desc = new CachedTransform(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("AUTHOR"); if (previewAuthorLabel != null) { var desc = FluentProvider.GetMessage(CreatedBy, "author", FluentProvider.GetMessage(generator.Name)); previewAuthorLabel.GetText = () => desc; previewAuthorLabel.IsVisible = () => !failed; } var previewSizeLabel = widget.GetOrNull("SIZE"); if (previewSizeLabel != null) { var desc = new CachedTransform(MapChooserLogic.MapSizeLabel); previewSizeLabel.IsVisible = () => !failed; previewSizeLabel.GetText = () => desc.Update(size); } settingsPanel = widget.Get("SETTINGS_PANEL"); checkboxSettingTemplate = settingsPanel.Get("CHECKBOX_TEMPLATE"); textSettingTemplate = settingsPanel.Get("TEXT_TEMPLATE"); dropdownSettingTemplate = settingsPanel.Get("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("LABEL").GetText = () => tilesetLabel; var label = new CachedTransform(ti => FluentProvider.GetMessage(ti.Name)); var tilesetDropdown = tilesetSetting.Get("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("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("LABEL").GetText = () => sizeLabel; var sizeDropdown = sizeSetting.Get("DROPDOWN"); var sizeDropdownLabel = new CachedTransform(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("LABEL").GetText = () => label; return item; } sizeDropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", MapSizes.Count * 30, MapSizes.Keys, SetupItem); }; var generateButton = widget.Get("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(); 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("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("LABEL"); var label = FluentProvider.GetMessage(io.Label); labelWidget.GetText = () => label; var textFieldWidget = settingWidget.Get("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("LABEL"); var label = FluentProvider.GetMessage(mio.Label); labelWidget.GetText = () => label; var labelCache = new CachedTransform(v => FieldSaver.FormatValue(v)); var dropDownWidget = settingWidget.Get("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("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("LABEL"); var label = FluentProvider.GetMessage(mo.Label); labelWidget.GetText = () => label; var labelCache = new CachedTransform(v => FluentProvider.GetMessage(mo.Choices[v].Label + ".label")); var dropDownWidget = settingWidget.Get("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("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); } }); }); } } }