From 2e5ef7f05935d98e57868183ae2eae95d21a8d64 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Tue, 31 Oct 2023 19:20:00 +0000 Subject: [PATCH] Show the server map pool in the client map chooser. Maps that aren't installed are queried from the resource center. --- OpenRA.Game/Network/OrderManager.cs | 3 + OpenRA.Game/Network/UnitOrders.cs | 6 + .../ServerTraits/LobbyCommands.cs | 3 + .../Widgets/Logic/Lobby/LobbyLogic.cs | 4 +- .../Widgets/Logic/MainMenuLogic.cs | 1 + .../Widgets/Logic/MapChooserLogic.cs | 145 ++++++++++++++---- .../Widgets/Logic/ServerCreationLogic.cs | 1 + mods/cnc/chrome/mapchooser.yaml | 21 +++ mods/cnc/languages/chrome/en.ftl | 1 + mods/common/chrome/map-chooser.yaml | 21 +++ mods/common/languages/chrome/en.ftl | 1 + mods/common/languages/en.ftl | 10 ++ 12 files changed, 188 insertions(+), 29 deletions(-) diff --git a/OpenRA.Game/Network/OrderManager.cs b/OpenRA.Game/Network/OrderManager.cs index c2e0096635..7c504e88e4 100644 --- a/OpenRA.Game/Network/OrderManager.cs +++ b/OpenRA.Game/Network/OrderManager.cs @@ -39,6 +39,9 @@ namespace OpenRA.Network public string ServerError = null; public bool AuthenticationFailed = false; + // The default null means "no map restriction" while an empty set means "all maps restricted" + public HashSet ServerMapPool = null; + public int NetFrameNumber { get; private set; } public int LocalFrameNumber; diff --git a/OpenRA.Game/Network/UnitOrders.cs b/OpenRA.Game/Network/UnitOrders.cs index 7d1bf0a211..a84111212a 100644 --- a/OpenRA.Game/Network/UnitOrders.cs +++ b/OpenRA.Game/Network/UnitOrders.cs @@ -383,6 +383,12 @@ namespace OpenRA.Network break; } + case "SyncMapPool": + { + orderManager.ServerMapPool = FieldLoader.GetValue>("SyncMapPool", order.TargetString); + break; + } + default: { if (world == null) diff --git a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs index 2f67c42007..67706e5c1c 100644 --- a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs +++ b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs @@ -1396,6 +1396,9 @@ namespace OpenRA.Mods.Common.Server { lock (server.LobbyInfo) { + if (server.MapPool != null) + server.SendOrderTo(conn, "SyncMapPool", FieldSaver.FormatValue(server.MapPool)); + var client = server.GetClient(conn); // Validate whether color is allowed and get an alternative if it isn't diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs index b9e9e0cc17..248567fe01 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs @@ -236,7 +236,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic var onSelect = new Action(uid => { // Don't select the same map again, and handle map becoming unavailable - if (uid == map.Uid || modData.MapCache[uid].Status != MapStatus.Available) + var status = modData.MapCache[uid].Status; + if (uid == map.Uid || (status != MapStatus.Available && status != MapStatus.DownloadAvailable)) return; orderManager.IssueOrder(Order.Command("map " + uid)); @@ -250,6 +251,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic Ui.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs() { { "initialMap", modData.MapCache.PickLastModifiedMap(MapVisibility.Lobby) ?? map.Uid }, + { "remoteMapPool", orderManager.ServerMapPool }, { "initialTab", MapClassification.System }, { "onExit", modData.MapCache.UpdateMaps }, { "onSelect", Game.IsHost ? onSelect : null }, diff --git a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs index b7c63c5694..899afcded0 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs @@ -204,6 +204,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic Game.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs() { { "initialMap", null }, + { "remoteMapPool", null }, { "initialTab", MapClassification.User }, { "onExit", () => SwitchMenu(MenuType.MapEditor) }, { "onSelect", onSelect }, diff --git a/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs index 61f6db1bd9..2b68c50ab7 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs @@ -42,6 +42,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic [TranslationReference] const string MapSizeSmall = "label-map-size-small"; + [TranslationReference("count")] + const string MapSearchingCount = "label-map-searching-count"; + + [TranslationReference("count")] + const string MapUnavailableCount = "label-map-unavailable-count"; + [TranslationReference("map")] const string MapDeletionFailed = "notification-map-deletion-failed"; @@ -80,8 +86,13 @@ namespace OpenRA.Mods.Common.Widgets.Logic 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(); @@ -97,12 +108,13 @@ namespace OpenRA.Mods.Common.Widgets.Logic Func orderByFunc; [ObjectCreator.UseCtor] - internal MapChooserLogic(Widget widget, ModData modData, string initialMap, + 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 = TranslationProvider.GetString(AllMaps); @@ -121,10 +133,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic gameModeDropdown = widget.GetOrNull("GAMEMODE_FILTER"); - var itemTemplate = widget.Get("MAP_TEMPLATE"); + itemTemplate = widget.Get("MAP_TEMPLATE"); widget.RemoveChild(itemTemplate); - SetupOrderByDropdown(itemTemplate); + SetupOrderByDropdown(); var mapFilterInput = widget.GetOrNull("MAPFILTER_INPUT"); if (mapFilterInput != null) @@ -137,7 +149,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic else { mapFilter = mapFilterInput.Text = null; - EnumerateMaps(currentTab, itemTemplate); + EnumerateMaps(currentTab); } return true; @@ -146,7 +158,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic mapFilterInput.OnTextEdited = () => { mapFilter = mapFilterInput.Text; - EnumerateMaps(currentTab, itemTemplate); + EnumerateMaps(currentTab); }; } @@ -167,12 +179,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic deleteMapButton.IsVisible = () => currentTab == MapClassification.User; deleteMapButton.OnClick = () => { - DeleteOneMap(selectedUid, (string newUid) => + DeleteOneMap(selectedUid, newUid => { RefreshMaps(currentTab, filter); - EnumerateMaps(currentTab, itemTemplate); + EnumerateMaps(currentTab); if (tabMaps[currentTab].Length == 0) - SwitchTab(modData.MapCache[newUid].Class, itemTemplate); + SwitchTab(modData.MapCache[newUid].Class); }); }; @@ -183,15 +195,41 @@ namespace OpenRA.Mods.Common.Widgets.Logic DeleteAllMaps(visibleMaps, (string newUid) => { RefreshMaps(currentTab, filter); - EnumerateMaps(currentTab, itemTemplate); - SwitchTab(modData.MapCache[newUid].Class, itemTemplate); + EnumerateMaps(currentTab); + SwitchTab(modData.MapCache[newUid].Class); }); }; - SetupMapTab(MapClassification.User, filter, "USER_MAPS_TAB_BUTTON", "USER_MAPS_TAB", itemTemplate); - SetupMapTab(MapClassification.System, filter, "SYSTEM_MAPS_TAB_BUTTON", "SYSTEM_MAPS_TAB", itemTemplate); + var remoteMapLabel = widget.Get("REMOTE_MAP_LABEL"); + var remoteMapText = new CachedTransform<(int Searching, int Unavailable), string>(counts => + { + if (counts.Searching > 0) + return TranslationProvider.GetString(MapSearchingCount, Translation.Arguments("count", counts.Searching)); - if (initialMap == null && tabMaps.TryGetValue(initialTab, out var map) && map.Length > 0) + return TranslationProvider.GetString(MapUnavailableCount, Translation.Arguments("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); @@ -203,22 +241,59 @@ namespace OpenRA.Mods.Common.Widgets.Logic currentTab = tabMaps.Keys.FirstOrDefault(k => tabMaps[k].Select(mp => mp.Uid).Contains(selectedUid)); } - SwitchTab(currentTab, itemTemplate); + EnumerateMaps(currentTab); } - void SwitchTab(MapClassification tab, ScrollItemWidget itemTemplate) + void SwitchTab(MapClassification tab) { currentTab = tab; - EnumerateMaps(tab, itemTemplate); + EnumerateMaps(tab); } void RefreshMaps(MapClassification tab, MapVisibility filter) { - tabMaps[tab] = modData.MapCache.Where(m => m.Status == MapStatus.Available && - m.Class == tab && (m.Visibility & filter) != 0).ToArray(); + 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, ScrollItemWidget itemTemplate) + void SetupMapTab(MapClassification tab, MapVisibility filter, string tabButtonName, string tabContainerName) { var tabContainer = widget.Get(tabContainerName); tabContainer.IsVisible = () => currentTab == tab; @@ -228,13 +303,21 @@ namespace OpenRA.Mods.Common.Widgets.Logic var tabButton = widget.Get(tabButtonName); tabButton.IsHighlighted = () => currentTab == tab; - tabButton.IsVisible = () => tabMaps[tab].Length > 0; - tabButton.OnClick = () => SwitchTab(tab, itemTemplate); + + 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, ScrollItemWidget itemTemplate) + void SetupGameModeDropdown(MapClassification tab, DropDownButtonWidget gameModeDropdown) { if (gameModeDropdown != null) { @@ -263,7 +346,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic { var item = ScrollItemWidget.Setup(template, () => category == ii.Category, - () => { category = ii.Category; EnumerateMaps(tab, itemTemplate); }); + () => { category = ii.Category; EnumerateMaps(tab); }); item.Get("LABEL").GetText = () => ShowItem(ii); return item; } @@ -282,7 +365,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic } } - void SetupOrderByDropdown(ScrollItemWidget itemTemplate) + void SetupOrderByDropdown() { var orderByDropdown = widget.GetOrNull("ORDERBY"); if (orderByDropdown == null) @@ -304,7 +387,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic { var item = ScrollItemWidget.Setup(template, () => orderByFunc == orderByDict[o], - () => { orderByFunc = orderByDict[o]; EnumerateMaps(currentTab, itemTemplate); }); + () => { orderByFunc = orderByDict[o]; EnumerateMaps(currentTab); }); item.Get("LABEL").GetText = () => o; return item; @@ -317,7 +400,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic orderByDict.FirstOrDefault(m => m.Value == orderByFunc).Key; } - void EnumerateMaps(MapClassification tab, ScrollItemWidget template) + void EnumerateMaps(MapClassification tab) { if (!int.TryParse(mapFilter, out var playerCountFilter)) playerCountFilter = -1; @@ -353,7 +436,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic } } - var item = ScrollItemWidget.Setup(preview.Uid, template, () => selectedUid == preview.Uid, + var item = ScrollItemWidget.Setup(preview.Uid, itemTemplate, () => selectedUid == preview.Uid, () => selectedUid = preview.Uid, DblClick); item.IsVisible = () => item.RenderBounds.IntersectsWith(scrollpanels[tab].RenderBounds); @@ -400,7 +483,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (tab == currentTab) { visibleMaps = maps.Select(m => m.Uid).ToArray(); - SetupGameModeDropdown(currentTab, gameModeDropdown, template); + SetupGameModeDropdown(currentTab, gameModeDropdown); } if (visibleMaps.Contains(selectedUid)) @@ -455,5 +538,11 @@ namespace OpenRA.Mods.Common.Widgets.Logic confirmText: DeleteAllMapsAccept, onCancel: () => { }); } + + protected override void Dispose(bool disposing) + { + disposed = true; + base.Dispose(disposing); + } } } diff --git a/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs index 074450242f..e393516866 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ServerCreationLogic.cs @@ -95,6 +95,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic Ui.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs() { { "initialMap", map.Uid }, + { "remoteMapPool", null }, { "initialTab", MapClassification.System }, { "onExit", () => modData.MapCache.UpdateMaps() }, { "onSelect", (Action)(uid => map = modData.MapCache[uid]) }, diff --git a/mods/cnc/chrome/mapchooser.yaml b/mods/cnc/chrome/mapchooser.yaml index 2d294bed1f..5d16c0f167 100644 --- a/mods/cnc/chrome/mapchooser.yaml +++ b/mods/cnc/chrome/mapchooser.yaml @@ -23,6 +23,12 @@ Container@MAPCHOOSER_PANEL: Height: 31 Width: 135 Text: button-bg-system-maps-tab + Button@REMOTE_MAPS_TAB_BUTTON: + X: 15 + Y: 15 + Height: 31 + Width: 135 + Text: button-bg-remote-maps-tab Button@USER_MAPS_TAB_BUTTON: X: 155 Y: 15 @@ -43,6 +49,14 @@ Container@MAPCHOOSER_PANEL: Width: PARENT_RIGHT Height: PARENT_BOTTOM ItemSpacing: 1 + Container@REMOTE_MAPS_TAB: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Children: + ScrollPanel@MAP_LIST: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + ItemSpacing: 1 Container@USER_MAPS_TAB: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -134,6 +148,13 @@ Container@MAPCHOOSER_PANEL: X: PARENT_RIGHT - WIDTH Width: 200 Height: 25 + Label@REMOTE_MAP_LABEL: + X: 140 + Y: 539 + Width: PARENT_RIGHT - 430 + Height: 35 + Align: Center + Font: Bold Button@BUTTON_CANCEL: Key: escape Y: PARENT_BOTTOM - 1 diff --git a/mods/cnc/languages/chrome/en.ftl b/mods/cnc/languages/chrome/en.ftl index 4fc2ce4090..9d727b4d0f 100644 --- a/mods/cnc/languages/chrome/en.ftl +++ b/mods/cnc/languages/chrome/en.ftl @@ -499,6 +499,7 @@ label-update-notice-b = Download the latest version from www.openra.net ## mapchooser.yaml label-mapchooser-panel-title = Select Map button-bg-system-maps-tab = Official Maps +button-bg-remote-maps-tab = Server Maps button-bg-user-maps-tab = Custom Maps label-filter-order-controls-desc = Filter: label-filter-order-controls-desc-joiner = in diff --git a/mods/common/chrome/map-chooser.yaml b/mods/common/chrome/map-chooser.yaml index e7fdd66854..ec0daaa308 100644 --- a/mods/common/chrome/map-chooser.yaml +++ b/mods/common/chrome/map-chooser.yaml @@ -19,6 +19,13 @@ Background@MAPCHOOSER_PANEL: Width: 140 Text: button-mapchooser-panel-system-maps-tab Font: Bold + Button@REMOTE_MAPS_TAB_BUTTON: + X: 20 + Y: 48 + Height: 31 + Width: 140 + Text: button-mapchooser-panel-remote-maps-tab + Font: Bold Button@USER_MAPS_TAB_BUTTON: X: 160 Y: 48 @@ -39,6 +46,13 @@ Background@MAPCHOOSER_PANEL: ScrollPanel@MAP_LIST: Width: PARENT_RIGHT Height: PARENT_BOTTOM + Container@REMOTE_MAPS_TAB: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Children: + ScrollPanel@MAP_LIST: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM Container@USER_MAPS_TAB: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -150,6 +164,13 @@ Background@MAPCHOOSER_PANEL: Height: 25 Text: button-mapchooser-panel-delete-all-maps Font: Bold + Label@REMOTE_MAP_LABEL: + X: 140 + Y: PARENT_BOTTOM - HEIGHT - 20 + Width: PARENT_RIGHT - 410 + Height: 25 + Align: Center + Font: Bold Button@BUTTON_OK: X: PARENT_RIGHT - 270 Y: PARENT_BOTTOM - 45 diff --git a/mods/common/languages/chrome/en.ftl b/mods/common/languages/chrome/en.ftl index e7dbfbde0b..5ae2dce5b1 100644 --- a/mods/common/languages/chrome/en.ftl +++ b/mods/common/languages/chrome/en.ftl @@ -350,6 +350,7 @@ label-update-notice-b = Download the latest version from www.openra.net ## map-chooser.yaml label-mapchooser-panel-title = Choose Map button-mapchooser-panel-system-maps-tab = Official Maps +button-mapchooser-panel-remote-maps-tab = Server Maps button-mapchooser-panel-user-maps-tab = Custom Maps label-filter-order-controls-desc = Filter: label-filter-order-controls-desc-joiner = in diff --git a/mods/common/languages/en.ftl b/mods/common/languages/en.ftl index c96683536f..94bffa25d1 100644 --- a/mods/common/languages/en.ftl +++ b/mods/common/languages/en.ftl @@ -500,6 +500,16 @@ label-map-size-huge = (Huge) label-map-size-large = (Large) label-map-size-medium = (Medium) label-map-size-small = (Small) +label-map-searching-count = + { $count -> + [one] Searching the OpenRA Resource Center for { $count } map... + *[other] Searching the OpenRA Resource Center for { $count } maps... + } +label-map-unavailable-count = + { $count -> + [one] { $count } map was not found on the OpenRA Resource Center + *[other] { $count } maps were not found on the OpenRA Resource Center + } notification-map-deletion-failed = Failed to delete map '{ $map }'. See the debug.log file for details.