diff --git a/OpenRA.Mods.Common/Widgets/GeneratedMapPreviewWidget.cs b/OpenRA.Mods.Common/Widgets/GeneratedMapPreviewWidget.cs new file mode 100644 index 0000000000..5baa7773da --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/GeneratedMapPreviewWidget.cs @@ -0,0 +1,156 @@ +#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 OpenRA.FileFormats; +using OpenRA.Graphics; +using OpenRA.Primitives; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets +{ + public class GeneratedMapPreviewWidget : Widget + { + public readonly bool ShowSpawnPoints = true; + + readonly Sprite spawnUnclaimed; + readonly SpriteFont spawnFont; + readonly Color spawnColor, spawnContrastColor; + readonly int2 spawnLabelOffset; + + Sheet mapSheet; + Sprite mapSprite; + Rectangle mapRect; + + public GeneratedMapPreviewWidget() + { + spawnUnclaimed = ChromeProvider.GetImage("lobby-bits", "spawn-unclaimed"); + spawnFont = Game.Renderer.Fonts[ChromeMetrics.Get("SpawnFont")]; + spawnColor = ChromeMetrics.Get("SpawnColor"); + spawnContrastColor = ChromeMetrics.Get("SpawnContrastColor"); + spawnLabelOffset = ChromeMetrics.Get("SpawnLabelOffset"); + } + + protected GeneratedMapPreviewWidget(GeneratedMapPreviewWidget other) + : base(other) + { + ShowSpawnPoints = other.ShowSpawnPoints; + spawnUnclaimed = ChromeProvider.GetImage("lobby-bits", "spawn-unclaimed"); + spawnFont = Game.Renderer.Fonts[ChromeMetrics.Get("SpawnFont")]; + spawnColor = ChromeMetrics.Get("SpawnColor"); + spawnContrastColor = ChromeMetrics.Get("SpawnContrastColor"); + spawnLabelOffset = ChromeMetrics.Get("SpawnLabelOffset"); + } + + public override GeneratedMapPreviewWidget Clone() { return new GeneratedMapPreviewWidget(this); } + + (int2 Pos, string Label, int2 LabelOffset)[] spawns = []; + + public void Update(MapPreview map) + { + Update(map.SpawnPoints, map.Bounds, map.GridType, map.Preview); + } + + public void Update(Map map) + { + var spawnPoints = map.ActorDefinitions + .Where(d => d.Value.Value == "mpspawn") + .Select(kv => new ActorReference(kv.Value.Value, kv.Value.ToDictionary()).Get().Value); + + Update(spawnPoints, map.Bounds, map.Grid.Type, new Png(map.Package.GetStream("map.png"))); + } + + void Update(IEnumerable spawnPoints, Rectangle bounds, MapGridType gridType, Png preview) + { + if (mapSheet == null || mapSheet.Size.Width < preview.Width || mapSheet.Size.Height < preview.Height) + { + mapSheet?.Dispose(); + mapSheet = new Sheet(SheetType.BGRA, new Size(preview.Width, preview.Height).NextPowerOf2()); + } + + var spriteRect = new Rectangle(0, 0, preview.Width, preview.Height); + mapSprite = new Sprite(mapSheet, spriteRect, TextureChannel.RGBA); + OpenRA.Graphics.Util.FastCopyIntoSprite(mapSprite, preview); + mapSheet.CommitBufferedData(); + + // Update map rect + var previewScale = Math.Min(RenderBounds.Width * 1f / spriteRect.Width, RenderBounds.Height * 1f / spriteRect.Height); + var w = (int)(previewScale * spriteRect.Width); + var h = (int)(previewScale * spriteRect.Height); + var x = RenderBounds.X + (RenderBounds.Width - w) / 2; + var y = RenderBounds.Y + (RenderBounds.Height - h) / 2; + mapRect = new Rectangle(x, y, w, h); + + if (ShowSpawnPoints) + { + var s = new List<(int2, string, int2)>(); + foreach (var p in spawnPoints) + { + var pos = ConvertToPreview(p, bounds, gridType, previewScale); + + var sprite = spawnUnclaimed; + var offset = sprite.Size.XY.ToInt2() / 2; + WidgetUtils.DrawSprite(sprite, pos - offset); + + var number = Convert.ToChar('A' + s.Count).ToString(); + var textOffset = spawnFont.Measure(number) / 2 + spawnLabelOffset; + s.Add((pos, number, textOffset)); + } + + spawns = s.ToArray(); + } + } + + public void Clear() + { + mapSprite = null; + } + + public int2 ConvertToPreview(CPos cell, Rectangle bounds, MapGridType gridType, float previewScale) + { + var point = cell.ToMPos(gridType); + var cellWidth = gridType == MapGridType.RectangularIsometric ? 2 : 1; + var dx = (int)(previewScale * cellWidth * (point.U - bounds.Left)); + var dy = (int)(previewScale * (point.V - bounds.Top)); + + // Odd rows are shifted right by 1px + if ((point.V & 1) == 1) + dx++; + + return new int2(mapRect.X + dx, mapRect.Y + dy); + } + + public override void Draw() + { + if (mapSprite == null) + return; + + WidgetUtils.DrawSprite(mapSprite, mapRect.Location, mapRect.Size); + var offset = spawnUnclaimed.Size.XY.ToInt2() / 2; + foreach (var (pos, label, labelOffset) in spawns) + { + WidgetUtils.DrawSprite(spawnUnclaimed, pos - offset); + spawnFont.DrawTextWithContrast(label, pos - labelOffset, spawnColor, spawnContrastColor, 1); + } + } + + public override void Removed() + { + base.Removed(); + mapSheet?.Dispose(); + mapSheet = null; + } + + public bool Loaded => mapSprite != null; + } +} diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs index 9ab382b03f..158efcfc02 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs @@ -98,6 +98,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic MapPreview map; Session.MapStatus mapStatus; + MapGenerationArgs lastGeneratedMap; bool chatEnabled; bool disableTeamChat; @@ -249,16 +250,30 @@ namespace OpenRA.Mods.Common.Widgets.Logic Game.Settings.Save(); }); + var onSelectGenerated = new Action(args => + { + if (args.Uid == map.Uid) + return; + + lastGeneratedMap = args; + orderManager.IssueOrder(Order.FromTargetString("GenerateMap", args.Serialize(), true)); + orderManager.IssueOrder(Order.Command("map " + args.Uid)); + Game.Settings.Server.Map = args.Uid; + Game.Settings.Save(); + }); + // Check for updated maps, if the user has edited a map we'll preselect it for them modData.MapCache.UpdateMaps(); Ui.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs() { { "initialMap", modData.MapCache.PickLastModifiedMap(MapVisibility.Lobby) ?? map.Uid }, + { "initialGeneratedMap", lastGeneratedMap }, { "remoteMapPool", orderManager.ServerMapPool }, { "initialTab", MapClassification.System }, { "onExit", modData.MapCache.UpdateMaps }, { "onSelect", Game.IsHost ? onSelect : null }, + { "onSelectGenerated", Game.IsHost ? onSelectGenerated : null }, { "filter", MapVisibility.Lobby }, }); }; diff --git a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs index 686ee3407d..de0b7e9f7a 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs @@ -211,10 +211,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic Game.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs() { { "initialMap", null }, + { "initialGeneratedMap", (MapGenerationArgs)null }, { "remoteMapPool", null }, { "initialTab", MapClassification.User }, { "onExit", () => SwitchMenu(MenuType.MapEditor) }, { "onSelect", onSelect }, + { "onSelectGenerated", null }, { "filter", MapVisibility.Lobby | MapVisibility.Shellmap | MapVisibility.MissionSelector }, }); }; diff --git a/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs index e0524a53d3..75137fa40f 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Linq; +using OpenRA.Mods.Common.Traits; using OpenRA.Primitives; using OpenRA.Widgets; @@ -91,6 +92,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic [FluentReference] const string RemoteMapsTab = "button-mapchooser-remote-maps-tab"; + [FluentReference] + const string GeneratedMapsTab = "button-mapchooser-generated-maps-tab"; + public static string MapSizeLabel(Size size) { var area = size.Width * size.Height; @@ -124,6 +128,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic string selectedUid; readonly Action onSelect; + MapGenerationArgs generatedMap; string category; string mapFilter; @@ -131,8 +136,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic Func orderByFunc; [ObjectCreator.UseCtor] - internal MapChooserLogic(Widget widget, ModData modData, string initialMap, HashSet remoteMapPool, - MapClassification initialTab, Action onExit, Action onSelect, MapVisibility filter) + internal MapChooserLogic(Widget widget, ModData modData, string initialMap, MapGenerationArgs initialGeneratedMap, HashSet remoteMapPool, + MapClassification initialTab, Action onExit, Action onSelect, Action onSelectGenerated, MapVisibility filter) { this.widget = widget; this.modData = modData; @@ -145,13 +150,20 @@ namespace OpenRA.Mods.Common.Widgets.Logic var approving = new Action(() => { Ui.CloseWindow(); - onSelect?.Invoke(selectedUid); + if (currentTab == MapClassification.Generated && generatedMap != null) + onSelectGenerated?.Invoke(generatedMap); + else + onSelect?.Invoke(selectedUid); }); var canceling = new Action(() => { Ui.CloseWindow(); onExit(); }); var okButton = widget.Get("BUTTON_OK"); - okButton.Disabled = this.onSelect == null; + if (onSelect != null) + okButton.IsDisabled = () => currentTab == MapClassification.Generated && generatedMap == null; + else + okButton.Disabled = true; + okButton.OnClick = approving; widget.Get("BUTTON_CANCEL").OnClick = canceling; @@ -162,6 +174,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic SetupOrderByDropdown(); + var filterContainer = widget.GetOrNull("FILTER_ORDER_CONTROLS"); + if (filterContainer != null) + filterContainer.IsVisible = () => currentTab != MapClassification.Generated; + var mapFilterInput = widget.GetOrNull("MAPFILTER_INPUT"); if (mapFilterInput != null) { @@ -196,6 +212,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic scrollpanels[currentTab].ScrollToItem(uid, smooth: true); }; randomMapButton.IsDisabled = () => visibleMaps == null || visibleMaps.Length == 0; + randomMapButton.IsVisible = () => currentTab != MapClassification.Generated; } var deleteMapButton = widget.Get("DELETE_MAP_BUTTON"); @@ -249,6 +266,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic SetupMapPanel(MapClassification.System, "SYSTEM_MAPS_TAB"); SetupMapPanel(MapClassification.Remote, "REMOTE_MAPS_TAB"); + var hasGenerator = modData.DefaultRules.Actors[SystemActors.EditorWorld].HasTraitInfo(); + if (onSelectGenerated != null && hasGenerator) + SetupGenerateMapPanel(MapClassification.Generated, "GENERATE_MAP_TAB", initialGeneratedMap); + // System and user map tabs are hidden when the server forces a restricted pool if (remoteMapPool != null) { @@ -260,8 +281,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic { tabLabels[MapClassification.System] = SystemMapsTab; tabLabels[MapClassification.User] = UserMapsTab; + if (onSelectGenerated != null && hasGenerator) + tabLabels[MapClassification.Generated] = GeneratedMapsTab; - if (initialMap == null && tabMaps.TryGetValue(initialTab, out var map) && map.Length > 0) + if (initialMap != null && modData.MapCache[initialMap].Class == MapClassification.Generated && onSelectGenerated != null && hasGenerator) + { + currentTab = MapClassification.Generated; + selectedUid = modData.MapCache.ChooseInitialMap(null, Game.CosmeticRandom); + } + else if (initialMap == null && tabMaps.TryGetValue(initialTab, out var map) && map.Length > 0) { var uid = map.Select(mp => mp.Uid).First(); selectedUid = Game.ModData.MapCache.ChooseInitialMap(uid, Game.CosmeticRandom); @@ -286,7 +314,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic void RefreshMaps(MapClassification tab) { - if (tab != MapClassification.Remote) + if (tab == MapClassification.System || tab == MapClassification.User) tabMaps[tab] = modData.MapCache.Where(m => m.Status == MapStatus.Available && m.Class == tab && (m.Visibility & filter) != 0).ToArray(); else if (remoteMapPool != null) @@ -358,6 +386,18 @@ namespace OpenRA.Mods.Common.Widgets.Logic RefreshMaps(tab); } + void SetupGenerateMapPanel(MapClassification tab, string tabContainerName, MapGenerationArgs initialSettings) + { + var tabContainer = widget.Get(tabContainerName); + tabContainer.IsVisible = () => currentTab == tab; + Ui.LoadWidget("MAPCHOOSER_GENERATE_PANEL", tabContainer, new WidgetArgs + { + { "modData", modData }, + { "initialSettings", initialSettings }, + { "onGenerate", (Action)(data => generatedMap = data) } + }); + } + void SetupGameModeDropdown(MapClassification tab, DropDownButtonWidget gameModeDropdown) { if (gameModeDropdown != null) @@ -443,6 +483,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic void EnumerateMaps(MapClassification tab) { + if (tab == MapClassification.Generated) + return; + if (!int.TryParse(mapFilter, out var playerCountFilter)) playerCountFilter = -1; @@ -532,7 +575,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic { modData.MapCache[map].Delete(); if (selectedUid == map) - selectedUid = Game.ModData.MapCache.ChooseInitialMap(tabMaps[currentTab].Select(mp => mp.Uid).FirstOrDefault(), + selectedUid = modData.MapCache.ChooseInitialMap(tabMaps[currentTab].Select(mp => mp.Uid).FirstOrDefault(), Game.CosmeticRandom); } catch (Exception ex) @@ -569,7 +612,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic foreach (var map in maps) DeleteMap(map); - after?.Invoke(Game.ModData.MapCache.ChooseInitialMap(null, Game.CosmeticRandom)); + after?.Invoke(modData.MapCache.ChooseInitialMap(null, Game.CosmeticRandom)); }, confirmText: DeleteAllMapsAccept, onCancel: () => { }); diff --git a/OpenRA.Mods.Common/Widgets/Logic/MapGeneratorLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MapGeneratorLogic.cs new file mode 100644 index 0000000000..8981b48056 --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/Logic/MapGeneratorLogic.cs @@ -0,0 +1,424 @@ +#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.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; + + ITerrainInfo selectedTerrain; + string selectedSize; + Size size; + + volatile bool generating; + volatile bool failed; + + [ObjectCreator.UseCtor] + internal MapGeneratorLogic(Widget widget, ModData modData, MapGenerationArgs initialSettings, Action onGenerate) + { + this.modData = modData; + this.onGenerate = onGenerate; + + 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(generating ? 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 = () => generating; + 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); + onGenerate(initialSettings); + } + else + GenerateMap(); + } + else + { + selectedTerrain = validTerrainInfos[0]; + settings.Randomize(Game.CosmeticRandom); + RandomizeSize(); + RefreshSettings(); + GenerateMap(); + } + } + + void RandomizeSize() + { + var sizeRange = MapSizes[selectedSize]; + var width = Game.CosmeticRandom.Next(sizeRange.X, sizeRange.Y); + size = new Size(width + 2, width + 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)) + mo.Value = mo.Default?.FirstOrDefault(validChoices.Contains) ?? 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() + { + generating = true; + onGenerate(null); + failed = false; + preview.Clear(); + Task.Run(() => + { + for (var i = 0; i < 5; i++) + { + try + { + var args = settings.Compile(selectedTerrain, size); + var map = generator.Generate(modData, args); + + // Map UID and preview image are generated on save + var package = new ZipFileLoader.ReadWriteZipFile(); + map.Save(package); + args.Uid = map.Uid; + + Game.RunAfterTick(() => + { + preview.Update(map); + onGenerate(args); + generating = false; + }); + return; + } + catch (Exception) + { + // Ignore the exception + } + } + + failed = true; + generating = false; + }); + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs index 85e17be189..60d3f3a953 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs @@ -98,10 +98,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic Ui.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs() { { "initialMap", map.Uid }, + { "initialGeneratedMap", (MapGenerationArgs)null }, { "remoteMapPool", null }, { "initialTab", MapClassification.System }, { "onExit", () => modData.MapCache.UpdateMaps() }, { "onSelect", (Action)(uid => map = modData.MapCache[uid]) }, + { "onSelectGenerated", null }, { "filter", MapVisibility.Lobby }, { "onStart", () => { } } }); diff --git a/mods/cnc/chrome/mapchooser.yaml b/mods/cnc/chrome/mapchooser.yaml index a2a153c991..31e4bcd0ea 100644 --- a/mods/cnc/chrome/mapchooser.yaml +++ b/mods/cnc/chrome/mapchooser.yaml @@ -63,6 +63,9 @@ Container@MAPCHOOSER_PANEL: Width: PARENT_WIDTH Height: PARENT_HEIGHT ItemSpacing: 1 + Container@GENERATE_MAP_TAB: + Width: PARENT_WIDTH + Height: PARENT_HEIGHT + 30 ScrollItem@MAP_TEMPLATE: Width: 210 Height: 262 @@ -186,3 +189,103 @@ Container@MAPCHOOSER_PANEL: Height: 35 Text: button-bg-ok TooltipContainer@TOOLTIP_CONTAINER: + +Container@MAPCHOOSER_GENERATE_PANEL: + Logic: MapGeneratorLogic + Width: PARENT_WIDTH + Height: PARENT_HEIGHT + Children: + Background@MAP_BG: + X: PARENT_WIDTH - WIDTH + Width: 370 + Height: 370 + Background: panel-gray + Children: + GeneratedMapPreview@PREVIEW: + X: 1 + Y: 1 + Width: PARENT_WIDTH - 2 + Height: PARENT_HEIGHT - 2 + Label@TITLE: + X: PARENT_WIDTH - WIDTH + Y: 375 + Width: 370 + Height: 24 + Align: Center + Text: label-mapchooser-random-map-title + Font: Bold + Label@ERROR: + X: PARENT_WIDTH - WIDTH + Y: 405 + Width: 370 + Height: 24 + Align: Center + Text: label-mapchooser-random-map-error-desc + Label@DETAILS: + X: PARENT_WIDTH - WIDTH + Y: 405 + Width: 370 + Height: 24 + Align: Center + LabelWithTooltip@AUTHOR: + X: PARENT_WIDTH - WIDTH + Y: 425 + Width: 370 + Height: 24 + Align: Center + Label@SIZE: + X: PARENT_WIDTH - WIDTH + Y: 445 + Width: 370 + Height: 24 + Align: Center + Button@BUTTON_GENERATE: + X: PARENT_WIDTH - 135 - WIDTH + Y: PARENT_HEIGHT + 14 + Width: 140 + Height: 35 + Text: button-mapchooser-random-map-generate + ScrollPanel@SETTINGS_PANEL: + TopBottomSpacing: 5 + Width: 485 + Height: PARENT_HEIGHT + Children: + Container@CHECKBOX_TEMPLATE: + Width: 230 + Height: 30 + Children: + Checkbox@CHECKBOX: + X: 10 + Y: 5 + Width: PARENT_WIDTH - 20 + Height: 20 + Container@TEXT_TEMPLATE: + Width: 230 + Height: 50 + Children: + LabelForInput@LABEL: + X: 10 + Width: PARENT_WIDTH - 20 + Height: 20 + For: INPUT + TextField@INPUT: + X: 10 + Y: 20 + Width: PARENT_WIDTH - 20 + Height: 25 + Container@DROPDOWN_TEMPLATE: + Width: 230 + Height: 50 + Children: + LabelForInput@LABEL: + X: 10 + Width: PARENT_WIDTH - 20 + Height: 20 + For: DROPDOWN + DropDownButton@DROPDOWN: + X: 10 + Y: 20 + Width: PARENT_WIDTH - 20 + Height: 25 + PanelRoot: GENERATOR_DROPDOWN_PANEL_ROOT # ensure that tooltips for the options are on top of the dropdown panel + Container@GENERATOR_DROPDOWN_PANEL_ROOT: diff --git a/mods/cnc/fluent/chrome.ftl b/mods/cnc/fluent/chrome.ftl index ffefefae2b..1b04b4138b 100644 --- a/mods/cnc/fluent/chrome.ftl +++ b/mods/cnc/fluent/chrome.ftl @@ -534,6 +534,14 @@ button-bg-delete-map = Delete Map button-bg-delete-all-maps = Delete All Maps button-bg-ok = Ok +label-mapchooser-random-map-title = Random Map +label-mapchooser-random-map-generating = Generating... +label-mapchooser-random-map-error = Map Generation Failed +button-mapchooser-random-map-generate = Generate +label-mapchooser-random-map-tileset = Environment: +label-mapchooser-random-map-size = Map Size: +label-mapchooser-random-map-error-desc = Adjust the settings to try again. + ## missionbrowser.yaml button-missionbrowser-panel-mission-info = Mission Info button-missionbrowser-panel-mission-options = Options diff --git a/mods/common/chrome/map-chooser.yaml b/mods/common/chrome/map-chooser.yaml index 45886e9596..bb09bbabb2 100644 --- a/mods/common/chrome/map-chooser.yaml +++ b/mods/common/chrome/map-chooser.yaml @@ -57,6 +57,9 @@ Background@MAPCHOOSER_PANEL: ScrollPanel@MAP_LIST: Width: PARENT_WIDTH Height: PARENT_HEIGHT + Container@GENERATE_MAP_TAB: + Width: PARENT_WIDTH + Height: PARENT_HEIGHT ScrollItem@MAP_TEMPLATE: Width: 208 Height: 266 @@ -185,3 +188,105 @@ Background@MAPCHOOSER_PANEL: Font: Bold Key: escape TooltipContainer@TOOLTIP_CONTAINER: + + +Container@MAPCHOOSER_GENERATE_PANEL: + Logic: MapGeneratorLogic + Width: PARENT_WIDTH + Height: PARENT_HEIGHT + Children: + Background@MAP_BG: + X: PARENT_WIDTH - WIDTH + Width: 370 + Height: 370 + Background: dialog3 + Children: + GeneratedMapPreview@PREVIEW: + X: 1 + Y: 1 + Width: PARENT_WIDTH - 2 + Height: PARENT_HEIGHT - 2 + Label@TITLE: + X: PARENT_WIDTH - WIDTH + Y: 375 + Width: 370 + Height: 24 + Align: Center + Text: label-mapchooser-random-map-title + Font: Bold + Label@ERROR: + X: PARENT_WIDTH - WIDTH + Y: 405 + Width: 370 + Height: 24 + Align: Center + Text: label-mapchooser-random-map-error-desc + Label@DETAILS: + X: PARENT_WIDTH - WIDTH + Y: 405 + Width: 370 + Height: 24 + Align: Center + LabelWithTooltip@AUTHOR: + X: PARENT_WIDTH - WIDTH + Y: 425 + Width: 370 + Height: 24 + Align: Center + Label@SIZE: + X: PARENT_WIDTH - WIDTH + Y: 445 + Width: 370 + Height: 24 + Align: Center + Button@BUTTON_GENERATE: + X: PARENT_WIDTH - 380 + Y: PARENT_HEIGHT + 40 + Width: 120 + Height: 25 + Text: button-mapchooser-random-map-generate + Font: Bold + ScrollPanel@SETTINGS_PANEL: + TopBottomSpacing: 5 + Width: 485 + Height: PARENT_HEIGHT + 30 + Children: + Container@CHECKBOX_TEMPLATE: + Width: 230 + Height: 30 + Children: + Checkbox@CHECKBOX: + X: 10 + Y: 5 + Width: PARENT_WIDTH - 20 + Height: 20 + Container@TEXT_TEMPLATE: + Width: 230 + Height: 50 + Children: + LabelForInput@LABEL: + X: 10 + Width: PARENT_WIDTH - 20 + Height: 20 + For: INPUT + TextField@INPUT: + X: 10 + Y: 20 + Width: PARENT_WIDTH - 20 + Height: 25 + Container@DROPDOWN_TEMPLATE: + Width: 230 + Height: 50 + Children: + LabelForInput@LABEL: + X: 10 + Width: PARENT_WIDTH - 20 + Height: 20 + For: DROPDOWN + DropDownButton@DROPDOWN: + X: 10 + Y: 20 + Width: PARENT_WIDTH - 20 + Height: 25 + PanelRoot: GENERATOR_DROPDOWN_PANEL_ROOT # ensure that tooltips for the options are on top of the dropdown panel + Container@GENERATOR_DROPDOWN_PANEL_ROOT: diff --git a/mods/common/fluent/chrome.ftl b/mods/common/fluent/chrome.ftl index f1cc937a49..02dbc2b79b 100644 --- a/mods/common/fluent/chrome.ftl +++ b/mods/common/fluent/chrome.ftl @@ -353,6 +353,14 @@ button-mapchooser-panel-delete-map = Delete Map button-mapchooser-panel-delete-all-maps = Delete All Maps button-mapchooser-panel-ok = Ok +label-mapchooser-random-map-title = Random Map +label-mapchooser-random-map-generating = Generating... +label-mapchooser-random-map-error = Map Generation Failed +button-mapchooser-random-map-generate = Generate +label-mapchooser-random-map-tileset = Environment: +label-mapchooser-random-map-size = Map Size: +label-mapchooser-random-map-error-desc = Adjust the settings to try again. + ## missionbrowser.yaml button-missionbrowser-panel-start-briefing-video = Watch Briefing button-missionbrowser-panel-stop-briefing-video = Stop Briefing diff --git a/mods/common/fluent/common.ftl b/mods/common/fluent/common.ftl index 4b326162be..21daa3fe62 100644 --- a/mods/common/fluent/common.ftl +++ b/mods/common/fluent/common.ftl @@ -514,6 +514,7 @@ options-order-maps = button-mapchooser-system-maps-tab = Official Maps button-mapchooser-remote-maps-tab = Server Maps button-mapchooser-user-maps-tab = Custom Maps +button-mapchooser-generated-maps-tab = Generate Map ## MissionBrowserLogic dialog-no-video =