#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.Primitives; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets.Logic { public class MapChooserLogic : ChromeLogic { [FluentReference] const string AllMaps = "label-all-maps"; [FluentReference] const string NoMatches = "label-no-matches"; [FluentReference("players")] const string Players = "label-player-count"; [FluentReference("author")] const string CreatedBy = "label-created-by"; [FluentReference] const string MapSizeHuge = "label-map-size-huge"; [FluentReference] const string MapSizeLarge = "label-map-size-large"; [FluentReference] const string MapSizeMedium = "label-map-size-medium"; [FluentReference] const string MapSizeSmall = "label-map-size-small"; [FluentReference("count")] const string MapSearchingCount = "label-map-searching-count"; [FluentReference("count")] const string MapUnavailableCount = "label-map-unavailable-count"; [FluentReference("map")] const string MapDeletionFailed = "notification-map-deletion-failed"; [FluentReference] const string DeleteMapTitle = "dialog-delete-map.title"; [FluentReference("title")] const string DeleteMapPrompt = "dialog-delete-map.prompt"; [FluentReference] const string DeleteMapAccept = "dialog-delete-map.confirm"; [FluentReference] const string DeleteAllMapsTitle = "dialog-delete-all-maps.title"; [FluentReference] const string DeleteAllMapsPrompt = "dialog-delete-all-maps.prompt"; [FluentReference] const string DeleteAllMapsAccept = "dialog-delete-all-maps.confirm"; [FluentReference] const string OrderMapsByPlayers = "options-order-maps.player-count"; [FluentReference] const string OrderMapsByTitle = "options-order-maps.title"; [FluentReference] const string OrderMapsByDate = "options-order-maps.date"; [FluentReference] const string OrderMapsBySize = "options-order-maps.size"; readonly string allMaps; readonly Widget widget; readonly DropDownButtonWidget gameModeDropdown; readonly ModData modData; readonly HashSet remoteMapPool; readonly ScrollItemWidget itemTemplate; MapClassification currentTab; bool disposed; int remoteSearching = 0; int remoteUnavailable = 0; readonly Dictionary scrollpanels = new(); readonly Dictionary tabMaps = new(); string[] visibleMaps; string selectedUid; readonly Action onSelect; string category; string mapFilter; Func orderByFunc; [ObjectCreator.UseCtor] internal MapChooserLogic(Widget widget, ModData modData, string initialMap, HashSet remoteMapPool, MapClassification initialTab, Action onExit, Action onSelect, MapVisibility filter) { this.widget = widget; this.modData = modData; this.onSelect = onSelect; this.remoteMapPool = remoteMapPool; allMaps = FluentProvider.GetMessage(AllMaps); var approving = new Action(() => { Ui.CloseWindow(); onSelect?.Invoke(selectedUid); }); var canceling = new Action(() => { Ui.CloseWindow(); onExit(); }); var okButton = widget.Get("BUTTON_OK"); okButton.Disabled = this.onSelect == null; okButton.OnClick = approving; widget.Get("BUTTON_CANCEL").OnClick = canceling; gameModeDropdown = widget.GetOrNull("GAMEMODE_FILTER"); itemTemplate = widget.Get("MAP_TEMPLATE"); widget.RemoveChild(itemTemplate); SetupOrderByDropdown(); var mapFilterInput = widget.GetOrNull("MAPFILTER_INPUT"); if (mapFilterInput != null) { mapFilterInput.TakeKeyboardFocus(); mapFilterInput.OnEscKey = _ => { if (mapFilterInput.Text.Length == 0) canceling(); else { mapFilter = mapFilterInput.Text = null; EnumerateMaps(currentTab); } return true; }; mapFilterInput.OnEnterKey = _ => { approving(); return true; }; mapFilterInput.OnTextEdited = () => { mapFilter = mapFilterInput.Text; EnumerateMaps(currentTab); }; } var randomMapButton = widget.GetOrNull("RANDOMMAP_BUTTON"); if (randomMapButton != null) { randomMapButton.OnClick = () => { var uid = visibleMaps.Random(Game.CosmeticRandom); selectedUid = uid; scrollpanels[currentTab].ScrollToItem(uid, smooth: true); }; randomMapButton.IsDisabled = () => visibleMaps == null || visibleMaps.Length == 0; } var deleteMapButton = widget.Get("DELETE_MAP_BUTTON"); deleteMapButton.IsDisabled = () => currentTab != MapClassification.User; deleteMapButton.IsVisible = () => currentTab == MapClassification.User; deleteMapButton.OnClick = () => { DeleteOneMap(selectedUid, newUid => { RefreshMaps(currentTab, filter); EnumerateMaps(currentTab); if (tabMaps[currentTab].Length == 0) SwitchTab(modData.MapCache[newUid].Class); }); }; var deleteAllMapsButton = widget.Get("DELETE_ALL_MAPS_BUTTON"); deleteAllMapsButton.IsVisible = () => currentTab == MapClassification.User; deleteAllMapsButton.OnClick = () => { DeleteAllMaps(visibleMaps, (string newUid) => { RefreshMaps(currentTab, filter); EnumerateMaps(currentTab); SwitchTab(modData.MapCache[newUid].Class); }); }; var remoteMapLabel = widget.Get("REMOTE_MAP_LABEL"); var remoteMapText = new CachedTransform<(int Searching, int Unavailable), string>(counts => { if (counts.Searching > 0) return FluentProvider.GetMessage(MapSearchingCount, "count", counts.Searching); return FluentProvider.GetMessage(MapUnavailableCount, "count", counts.Unavailable); }); remoteMapLabel.IsVisible = () => remoteMapPool != null && (remoteSearching > 0 || remoteUnavailable > 0); remoteMapLabel.GetText = () => remoteMapText.Update((remoteSearching, remoteUnavailable)); // SetupMapTab (through RefreshMap) depends on the map search having already started if (remoteMapPool != null && Game.Settings.Game.AllowDownloading) { var services = modData.Manifest.Get(); modData.MapCache.QueryRemoteMapDetails(services.MapRepository, remoteMapPool); } SetupMapTab(MapClassification.User, filter, "USER_MAPS_TAB_BUTTON", "USER_MAPS_TAB"); SetupMapTab(MapClassification.System, filter, "SYSTEM_MAPS_TAB_BUTTON", "SYSTEM_MAPS_TAB"); SetupMapTab(MapClassification.Remote, filter, "REMOTE_MAPS_TAB_BUTTON", "REMOTE_MAPS_TAB"); // System and user map tabs are hidden when the server forces a restricted pool if (remoteMapPool != null) { currentTab = MapClassification.Remote; selectedUid = initialMap; } else if (initialMap == null && tabMaps.TryGetValue(initialTab, out var map) && map.Length > 0) { selectedUid = Game.ModData.MapCache.ChooseInitialMap(map.Select(mp => mp.Uid).First(), Game.CosmeticRandom); currentTab = initialTab; } else { selectedUid = Game.ModData.MapCache.ChooseInitialMap(initialMap, Game.CosmeticRandom); currentTab = tabMaps.Keys.FirstOrDefault(k => tabMaps[k].Select(mp => mp.Uid).Contains(selectedUid)); } EnumerateMaps(currentTab); } void SwitchTab(MapClassification tab) { currentTab = tab; EnumerateMaps(tab); } void RefreshMaps(MapClassification tab, MapVisibility filter) { if (tab != MapClassification.Remote) tabMaps[tab] = modData.MapCache.Where(m => m.Status == MapStatus.Available && m.Class == tab && (m.Visibility & filter) != 0).ToArray(); else if (remoteMapPool != null) { var loaded = new List(); remoteSearching = 0; remoteUnavailable = 0; foreach (var uid in remoteMapPool) { var preview = modData.MapCache[uid]; var status = preview.Status; if (status == MapStatus.Searching) remoteSearching++; else if (status == MapStatus.Unavailable) remoteUnavailable++; else loaded.Add(preview); } tabMaps[tab] = loaded.ToArray(); if (remoteSearching > 0) { Game.RunAfterDelay(1000, () => { if (disposed) return; var missingBefore = remoteSearching + remoteUnavailable; RefreshMaps(MapClassification.Remote, filter); var missingAfter = remoteSearching + remoteUnavailable; if (currentTab == MapClassification.Remote && missingBefore != missingAfter) EnumerateMaps(MapClassification.Remote); }); } } else tabMaps[tab] = Array.Empty(); } void SetupMapTab(MapClassification tab, MapVisibility filter, string tabButtonName, string tabContainerName) { var tabContainer = widget.Get(tabContainerName); tabContainer.IsVisible = () => currentTab == tab; var tabScrollpanel = tabContainer.Get("MAP_LIST"); tabScrollpanel.Layout = new GridLayout(tabScrollpanel); scrollpanels.Add(tab, tabScrollpanel); var tabButton = widget.Get(tabButtonName); tabButton.IsHighlighted = () => currentTab == tab; if (remoteMapPool != null) { var isRemoteTab = tab == MapClassification.Remote; tabButton.IsVisible = () => isRemoteTab; } else tabButton.IsVisible = () => tabMaps[tab].Length > 0; tabButton.OnClick = () => SwitchTab(tab); RefreshMaps(tab, filter); } void SetupGameModeDropdown(MapClassification tab, DropDownButtonWidget gameModeDropdown) { if (gameModeDropdown != null) { var categoryDict = new Dictionary(); foreach (var map in tabMaps[tab]) { foreach (var category in map.Categories) { categoryDict.TryGetValue(category, out var count); categoryDict[category] = count + 1; } } // Order categories alphabetically var categories = categoryDict .Select(kv => (Category: kv.Key, Count: kv.Value)) .OrderBy(p => p.Category) .ToList(); // 'all game types' extra item categories.Insert(0, (null, tabMaps[tab].Length)); string ShowItem((string Category, int Count) x) => (x.Category ?? allMaps) + $" ({x.Count})"; ScrollItemWidget SetupItem((string Category, int Count) ii, ScrollItemWidget template) { var item = ScrollItemWidget.Setup(template, () => category == ii.Category, () => { category = ii.Category; EnumerateMaps(tab); }); item.Get("LABEL").GetText = () => ShowItem(ii); return item; } gameModeDropdown.OnClick = () => gameModeDropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 210, categories, SetupItem); gameModeDropdown.GetText = () => { var item = categories.FirstOrDefault(m => m.Category == category); if (item == default((string, int))) item.Category = FluentProvider.GetMessage(NoMatches); return ShowItem(item); }; } } void SetupOrderByDropdown() { var orderByDropdown = widget.GetOrNull("ORDERBY"); if (orderByDropdown == null) return; var orderByPlayer = FluentProvider.GetMessage(OrderMapsByPlayers); var orderByDict = new Dictionary>() { { orderByPlayer, m => m.PlayerCount }, { FluentProvider.GetMessage(OrderMapsByTitle), null }, { FluentProvider.GetMessage(OrderMapsByDate), m => -m.ModifiedDate.Ticks }, { FluentProvider.GetMessage(OrderMapsBySize), m => m.Bounds.Width * m.Bounds.Height }, }; orderByFunc = orderByDict[orderByPlayer]; ScrollItemWidget SetupItem(string o, ScrollItemWidget template) { var item = ScrollItemWidget.Setup(template, () => orderByFunc == orderByDict[o], () => { orderByFunc = orderByDict[o]; EnumerateMaps(currentTab); }); item.Get("LABEL").GetText = () => o; return item; } orderByDropdown.OnClick = () => orderByDropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 500, orderByDict.Keys, SetupItem); orderByDropdown.GetText = () => orderByDict.FirstOrDefault(m => m.Value == orderByFunc).Key; } void EnumerateMaps(MapClassification tab) { if (!int.TryParse(mapFilter, out var playerCountFilter)) playerCountFilter = -1; var maps = tabMaps[tab] .Where(m => (category == null || m.Categories.Contains(category)) && (mapFilter == null || (m.Title != null && m.Title.Contains(mapFilter, StringComparison.CurrentCultureIgnoreCase)) || (m.Author != null && m.Author.Contains(mapFilter, StringComparison.CurrentCultureIgnoreCase)) || m.PlayerCount == playerCountFilter)); if (orderByFunc == null) maps = maps.OrderBy(m => m.Title); else maps = maps.OrderBy(orderByFunc).ThenBy(m => m.Title); maps = maps.ToList(); scrollpanels[tab].RemoveChildren(); foreach (var loop in maps) { var preview = loop; // Access the minimap to trigger async generation of the minimap. preview.GetMinimap(); void DblClick() { if (onSelect != null) { Ui.CloseWindow(); onSelect(preview.Uid); } } var item = ScrollItemWidget.Setup(preview.Uid, itemTemplate, () => selectedUid == preview.Uid, () => selectedUid = preview.Uid, DblClick); item.IsVisible = () => item.RenderBounds.IntersectsWith(scrollpanels[tab].RenderBounds); var titleLabel = item.Get("TITLE"); if (titleLabel != null) { WidgetUtils.TruncateLabelToTooltip(titleLabel, preview.Title); } var previewWidget = item.Get("PREVIEW"); previewWidget.Preview = () => preview; var detailsWidget = item.GetOrNull("DETAILS"); if (detailsWidget != null) { var type = preview.Categories.FirstOrDefault(); var details = ""; if (type != null) details = type + " "; details += FluentProvider.GetMessage(Players, "players", preview.PlayerCount); detailsWidget.GetText = () => details; } var authorWidget = item.GetOrNull("AUTHOR"); if (authorWidget != null && !string.IsNullOrEmpty(preview.Author)) WidgetUtils.TruncateLabelToTooltip(authorWidget, FluentProvider.GetMessage(CreatedBy, "author", preview.Author)); var sizeWidget = item.GetOrNull("SIZE"); if (sizeWidget != null) { var size = preview.Bounds.Width + "x" + preview.Bounds.Height; var numberPlayableCells = preview.Bounds.Width * preview.Bounds.Height; if (numberPlayableCells >= 120 * 120) size += " " + FluentProvider.GetMessage(MapSizeHuge); else if (numberPlayableCells >= 90 * 90) size += " " + FluentProvider.GetMessage(MapSizeLarge); else if (numberPlayableCells >= 60 * 60) size += " " + FluentProvider.GetMessage(MapSizeMedium); else size += " " + FluentProvider.GetMessage(MapSizeSmall); sizeWidget.GetText = () => size; } scrollpanels[tab].AddChild(item); } if (tab == currentTab) { visibleMaps = maps.Select(m => m.Uid).ToArray(); SetupGameModeDropdown(currentTab, gameModeDropdown); } if (visibleMaps.Contains(selectedUid)) scrollpanels[tab].ScrollToItem(selectedUid); } string DeleteMap(string map) { try { modData.MapCache[map].Delete(); if (selectedUid == map) selectedUid = Game.ModData.MapCache.ChooseInitialMap(tabMaps[currentTab].Select(mp => mp.Uid).FirstOrDefault(), Game.CosmeticRandom); } catch (Exception ex) { TextNotificationsManager.Debug(FluentProvider.GetMessage(MapDeletionFailed, "map", map)); Log.Write("debug", ex.ToString()); } return selectedUid; } void DeleteOneMap(string map, Action after) { ConfirmationDialogs.ButtonPrompt(modData, title: DeleteMapTitle, text: DeleteMapPrompt, textArguments: new object[] { "title", modData.MapCache[map].Title }, onConfirm: () => { var newUid = DeleteMap(map); after?.Invoke(newUid); }, confirmText: DeleteMapAccept, onCancel: () => { }); } void DeleteAllMaps(string[] maps, Action after) { ConfirmationDialogs.ButtonPrompt(modData, title: DeleteAllMapsTitle, text: DeleteAllMapsPrompt, onConfirm: () => { foreach (var map in maps) DeleteMap(map); after?.Invoke(Game.ModData.MapCache.ChooseInitialMap(null, Game.CosmeticRandom)); }, confirmText: DeleteAllMapsAccept, onCancel: () => { }); } protected override void Dispose(bool disposing) { disposed = true; base.Dispose(disposing); } } }