550 lines
17 KiB
C#
550 lines
17 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 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<string> remoteMapPool;
|
|
readonly ScrollItemWidget itemTemplate;
|
|
|
|
MapClassification currentTab;
|
|
bool disposed;
|
|
int remoteSearching = 0;
|
|
int remoteUnavailable = 0;
|
|
|
|
readonly Dictionary<MapClassification, ScrollPanelWidget> scrollpanels = new();
|
|
|
|
readonly Dictionary<MapClassification, MapPreview[]> tabMaps = new();
|
|
string[] visibleMaps;
|
|
|
|
string selectedUid;
|
|
readonly Action<string> onSelect;
|
|
|
|
string category;
|
|
string mapFilter;
|
|
|
|
Func<MapPreview, long> orderByFunc;
|
|
|
|
[ObjectCreator.UseCtor]
|
|
internal MapChooserLogic(Widget widget, ModData modData, string initialMap, HashSet<string> remoteMapPool,
|
|
MapClassification initialTab, Action onExit, Action<string> 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<ButtonWidget>("BUTTON_OK");
|
|
okButton.Disabled = this.onSelect == null;
|
|
okButton.OnClick = approving;
|
|
widget.Get<ButtonWidget>("BUTTON_CANCEL").OnClick = canceling;
|
|
|
|
gameModeDropdown = widget.GetOrNull<DropDownButtonWidget>("GAMEMODE_FILTER");
|
|
|
|
itemTemplate = widget.Get<ScrollItemWidget>("MAP_TEMPLATE");
|
|
widget.RemoveChild(itemTemplate);
|
|
|
|
SetupOrderByDropdown();
|
|
|
|
var mapFilterInput = widget.GetOrNull<TextFieldWidget>("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<ButtonWidget>("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<ButtonWidget>("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<ButtonWidget>("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<LabelWidget>("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<WebServices>();
|
|
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<MapPreview>();
|
|
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<MapPreview>();
|
|
}
|
|
|
|
void SetupMapTab(MapClassification tab, MapVisibility filter, string tabButtonName, string tabContainerName)
|
|
{
|
|
var tabContainer = widget.Get<ContainerWidget>(tabContainerName);
|
|
tabContainer.IsVisible = () => currentTab == tab;
|
|
var tabScrollpanel = tabContainer.Get<ScrollPanelWidget>("MAP_LIST");
|
|
tabScrollpanel.Layout = new GridLayout(tabScrollpanel);
|
|
scrollpanels.Add(tab, tabScrollpanel);
|
|
|
|
var tabButton = widget.Get<ButtonWidget>(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<string, int>();
|
|
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<LabelWidget>("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<DropDownButtonWidget>("ORDERBY");
|
|
if (orderByDropdown == null)
|
|
return;
|
|
|
|
var orderByPlayer = FluentProvider.GetMessage(OrderMapsByPlayers);
|
|
|
|
var orderByDict = new Dictionary<string, Func<MapPreview, long>>()
|
|
{
|
|
{ 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<LabelWidget>("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<LabelWithTooltipWidget>("TITLE");
|
|
if (titleLabel != null)
|
|
{
|
|
WidgetUtils.TruncateLabelToTooltip(titleLabel, preview.Title);
|
|
}
|
|
|
|
var previewWidget = item.Get<MapPreviewWidget>("PREVIEW");
|
|
previewWidget.Preview = () => preview;
|
|
|
|
var detailsWidget = item.GetOrNull<LabelWidget>("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<LabelWithTooltipWidget>("AUTHOR");
|
|
if (authorWidget != null && !string.IsNullOrEmpty(preview.Author))
|
|
WidgetUtils.TruncateLabelToTooltip(authorWidget, FluentProvider.GetMessage(CreatedBy, "author", preview.Author));
|
|
|
|
var sizeWidget = item.GetOrNull<LabelWidget>("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<string> 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<string> 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);
|
|
}
|
|
}
|
|
}
|