diff --git a/OpenRA.Game/Manifest.cs b/OpenRA.Game/Manifest.cs index 6f2b45007d..6734adb179 100644 --- a/OpenRA.Game/Manifest.cs +++ b/OpenRA.Game/Manifest.cs @@ -21,12 +21,13 @@ namespace OpenRA { public readonly ModMetadata Mod; public readonly string[] - Folders, MapFolders, Rules, ServerTraits, + Folders, Rules, ServerTraits, Sequences, VoxelSequences, Cursors, Chrome, Assemblies, ChromeLayout, Weapons, Voices, Notifications, Music, Movies, Translations, TileSets, ChromeMetrics, PackageContents, LuaScripts, MapCompatibility, Missions; - public readonly Dictionary Packages; + public readonly IReadOnlyDictionary Packages; + public readonly IReadOnlyDictionary MapFolders; public readonly MiniYaml LoadScreen; public readonly MiniYaml LobbyDefaults; public readonly Dictionary> Fonts; @@ -43,8 +44,8 @@ namespace OpenRA // TODO: Use fieldloader Folders = YamlList(yaml, "Folders"); - MapFolders = YamlList(yaml, "MapFolders"); - Packages = yaml["Packages"].NodesDict.ToDictionary(x => x.Key, x => x.Value.Value); + MapFolders = YamlDictionary(yaml, "MapFolders"); + Packages = YamlDictionary(yaml, "Packages"); Rules = YamlList(yaml, "Rules"); ServerTraits = YamlList(yaml, "ServerTraits"); Sequences = YamlList(yaml, "Sequences"); @@ -95,5 +96,14 @@ namespace OpenRA return yaml[key].NodesDict.Keys.ToArray(); } + + static IReadOnlyDictionary YamlDictionary(Dictionary yaml, string key) + { + if (!yaml.ContainsKey(key)) + return new ReadOnlyDictionary(); + + var inner = yaml[key].NodesDict.ToDictionary(x => x.Key, x => x.Value.Value); + return new ReadOnlyDictionary(inner); + } } } diff --git a/OpenRA.Game/Map/MapCache.cs b/OpenRA.Game/Map/MapCache.cs index 449bc6996c..0a7ea1d6cd 100644 --- a/OpenRA.Game/Map/MapCache.cs +++ b/OpenRA.Game/Map/MapCache.cs @@ -42,16 +42,19 @@ namespace OpenRA public void LoadMaps() { - var paths = modData.Manifest.MapFolders.SelectMany(f => FindMapsIn(f)); - foreach (var path in paths) + // Expand the dictionary (dir path, dir type) to a dictionary of (map path, dir type) + var mapPaths = modData.Manifest.MapFolders.SelectMany(kv => + FindMapsIn(kv.Key).ToDictionary(p => p, p => string.IsNullOrEmpty(kv.Value) ? MapClassification.Unknown : Enum.Parse(kv.Value))); + + foreach (var path in mapPaths) { try { - using (new Support.PerfTimer(path)) + using (new Support.PerfTimer(path.Key)) { - var map = new Map(path, modData.Manifest.Mod.Id); + var map = new Map(path.Key, modData.Manifest.Mod.Id); if (modData.Manifest.MapCompatibility.Contains(map.RequiresMod)) - previews[map.Uid].UpdateFromMap(map); + previews[map.Uid].UpdateFromMap(map, path.Value); } } catch (Exception e) diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs index cef9ac9b17..5207b1327d 100755 --- a/OpenRA.Game/Map/MapPreview.cs +++ b/OpenRA.Game/Map/MapPreview.cs @@ -24,6 +24,12 @@ namespace OpenRA { public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError } + // Used for grouping maps in the UI + public enum MapClassification { Unknown, System, User, Remote } + + // Used for verifying map availability in the lobby + public enum MapRuleStatus { Unknown, Cached, Invalid } + // Fields names must match the with the remote API public class RemoteMapData { @@ -53,6 +59,9 @@ namespace OpenRA public Bitmap CustomPreview { get; private set; } public Map Map { get; private set; } public MapStatus Status { get; private set; } + public MapClassification Class { get; private set; } + + public MapRuleStatus RuleStatus { get; private set; } Download download; public long DownloadBytes { get; private set; } @@ -94,9 +103,10 @@ namespace OpenRA Bounds = Rectangle.Empty; SpawnPoints = NoSpawns; Status = MapStatus.Unavailable; + Class = MapClassification.Unknown; } - public void UpdateFromMap(Map m) + public void UpdateFromMap(Map m, MapClassification classification) { Map = m; Title = m.Title; @@ -108,6 +118,7 @@ namespace OpenRA SpawnPoints = m.GetSpawnPoints().ToList(); CustomPreview = m.CustomPreview; Status = MapStatus.Available; + Class = classification; } public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml) @@ -146,8 +157,9 @@ namespace OpenRA if (CustomPreview != null) cache.CacheMinimap(this); } - Status = status; + Status = status; + Class = MapClassification.Remote; }); } @@ -199,7 +211,7 @@ namespace OpenRA } Log.Write("debug", "Downloaded map to '{0}'", mapPath); - Game.RunAfterTick(() => UpdateFromMap(new Map(mapPath))); + Game.RunAfterTick(() => UpdateFromMap(new Map(mapPath), MapClassification.User)); }; download = new Download(mapUrl, mapPath, onDownloadProgress, onDownloadComplete); @@ -220,5 +232,22 @@ namespace OpenRA download.Cancel(); download = null; } + + public void CacheRules() + { + if (RuleStatus != MapRuleStatus.Unknown) + return; + + try + { + Map.PreloadRules(); + RuleStatus = MapRuleStatus.Cached; + } + catch (Exception e) + { + Log.Write("debug", "Map {0} failed validation with an exception:\n{1}", Uid, e.Message); + RuleStatus = MapRuleStatus.Invalid; + } + } } } diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs index 9a144ce9fa..da5d814ce8 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs @@ -12,6 +12,7 @@ using System; using System.Collections.Generic; using System.Drawing; using System.Linq; +using System.Threading; using OpenRA.Graphics; using OpenRA.Network; using OpenRA.Traits; @@ -170,7 +171,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (slotsButton != null) { slotsButton.IsDisabled = () => configurationDisabled() || panel != PanelType.Players || - !orderManager.LobbyInfo.Slots.Values.Any(s => s.AllowBots || !s.LockTeam); + Map.RuleStatus != MapRuleStatus.Cached || !orderManager.LobbyInfo.Slots.Values.Any(s => s.AllowBots || !s.LockTeam); var botNames = modRules.Actors["player"].Traits.WithInterface().Select(t => t.Name); slotsButton.OnMouseDown = _ => @@ -264,7 +265,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic optionsBin.IsVisible = () => panel == PanelType.Options; var optionsButton = lobby.Get("OPTIONS_BUTTON"); - optionsButton.IsDisabled = () => panel == PanelType.Kick || panel == PanelType.ForceStart; + optionsButton.IsDisabled = () => Map.RuleStatus != MapRuleStatus.Cached || panel == PanelType.Kick || panel == PanelType.ForceStart; optionsButton.GetText = () => panel == PanelType.Options ? "Players" : "Options"; optionsButton.OnClick = () => panel = (panel == PanelType.Options) ? PanelType.Players : PanelType.Options; @@ -278,7 +279,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic var startGameButton = lobby.GetOrNull("START_GAME_BUTTON"); if (startGameButton != null) { - startGameButton.IsDisabled = () => configurationDisabled() || + startGameButton.IsDisabled = () => configurationDisabled() || Map.RuleStatus != MapRuleStatus.Cached || orderManager.LobbyInfo.Slots.Any(sl => sl.Value.Required && orderManager.LobbyInfo.ClientInSlot(sl.Key) == null); startGameButton.OnClick = () => { @@ -582,16 +583,31 @@ namespace OpenRA.Mods.RA.Widgets.Logic return; Map = Game.modData.MapCache[uid]; - if (Map.Status == MapStatus.Available) { - // Tell the server that we have the map - orderManager.IssueOrder(Order.Command("state {0}".F(Session.ClientState.NotReady))); + // Maps need to be validated and pre-loaded before they can be accessed + new Thread(_ => + { + var map = Map; + map.CacheRules(); + Game.RunAfterTick(() => + { + // Map may have changed in the meantime + if (map != Map) + return; - // Restore default starting cash if the last map set it to something invalid - var pri = modRules.Actors["player"].Traits.Get(); - if (!Map.Map.Options.StartingCash.HasValue && !pri.SelectableCash.Contains(orderManager.LobbyInfo.GlobalSettings.StartingCash)) - orderManager.IssueOrder(Order.Command("startingcash {0}".F(pri.DefaultCash))); + if (map.RuleStatus != MapRuleStatus.Invalid) + { + // Tell the server that we have the map + orderManager.IssueOrder(Order.Command("state {0}".F(Session.ClientState.NotReady))); + + // Restore default starting cash if the last map set it to something invalid + var pri = modRules.Actors["player"].Traits.Get(); + if (!Map.Map.Options.StartingCash.HasValue && !pri.SelectableCash.Contains(orderManager.LobbyInfo.GlobalSettings.StartingCash)) + orderManager.IssueOrder(Order.Command("startingcash {0}".F(pri.DefaultCash))); + } + }); + }).Start(); } else if (Game.Settings.Game.AllowDownloading) Game.modData.MapCache.QueryRemoteMapDetails(new [] { uid }); diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs index a7562a511d..42181fceeb 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs @@ -26,7 +26,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic var available = widget.GetOrNull("MAP_AVAILABLE"); if (available != null) { - available.IsVisible = () => lobby.Map.Status == MapStatus.Available; + available.IsVisible = () => lobby.Map.Status == MapStatus.Available && lobby.Map.RuleStatus == MapRuleStatus.Cached; var preview = available.Get("MAP_PREVIEW"); preview.Preview = () => lobby.Map; @@ -46,6 +46,25 @@ namespace OpenRA.Mods.RA.Widgets.Logic author.GetText = () => "Created by {0}".F(lobby.Map.Author); } + var invalid = widget.GetOrNull("MAP_INVALID"); + if (invalid != null) + { + invalid.IsVisible = () => lobby.Map.Status == MapStatus.Available && lobby.Map.RuleStatus == MapRuleStatus.Invalid; + + var preview = invalid.Get("MAP_PREVIEW"); + preview.Preview = () => lobby.Map; + preview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, preview, lobby.Map, mi); + preview.SpawnClients = () => LobbyUtils.GetSpawnClients(orderManager.LobbyInfo, lobby.Map); + + var title = invalid.GetOrNull("MAP_TITLE"); + if (title != null) + title.GetText = () => lobby.Map.Title; + + var type = invalid.GetOrNull("MAP_TYPE"); + if (type != null) + type.GetText = () => lobby.Map.Type; + } + var download = widget.GetOrNull("MAP_DOWNLOADABLE"); if (download != null) { @@ -76,7 +95,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic var progress = widget.GetOrNull("MAP_PROGRESS"); if (progress != null) { - progress.IsVisible = () => lobby.Map.Status != MapStatus.Available && lobby.Map.Status != MapStatus.DownloadAvailable; + progress.IsVisible = () => (lobby.Map.Status != MapStatus.Available || lobby.Map.RuleStatus == MapRuleStatus.Unknown) && lobby.Map.Status != MapStatus.DownloadAvailable; var preview = progress.Get("MAP_PREVIEW"); preview.Preview = () => lobby.Map; diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs index cba373dcce..6a36afcdca 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs @@ -430,7 +430,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic var status = parent.Get("STATUS_CHECKBOX"); status.IsChecked = () => orderManager.LocalClient.IsReady || c.Bot != null; status.IsVisible = () => true; - status.IsDisabled = () => c.Bot != null || map.Status != MapStatus.Available; + status.IsDisabled = () => c.Bot != null || map.Status != MapStatus.Available || map.RuleStatus != MapRuleStatus.Cached; var state = orderManager.LocalClient.IsReady ? Session.ClientState.NotReady : Session.ClientState.Ready; status.OnClick = () => orderManager.IssueOrder(Order.Command("state {0}".F(state))); diff --git a/mods/cnc/chrome/lobby-mappreview.yaml b/mods/cnc/chrome/lobby-mappreview.yaml index e23c093db7..a6de873ac6 100644 --- a/mods/cnc/chrome/lobby-mappreview.yaml +++ b/mods/cnc/chrome/lobby-mappreview.yaml @@ -38,6 +38,41 @@ Container@LOBBY_MAP_PREVIEW: Height: 25 Font: Tiny Align: Center + Container@MAP_INVALID: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Children: + Background@MAP_BG: + Width: PARENT_RIGHT + Height: 194 + Background: panel-gray + Children: + MapPreview@MAP_PREVIEW: + X: 1 + Y: 1 + Width: PARENT_RIGHT-2 + Height: PARENT_BOTTOM-2 + TooltipContainer: TOOLTIP_CONTAINER + Label@MAP_TITLE: + Y: 197 + Width: PARENT_RIGHT + Height: 25 + Font: Bold + Align: Center + Label@MAP_STATUS_A: + Y: 212 + Width: PARENT_RIGHT + Height: 25 + Font: Tiny + Align: Center + Text: This map is not compatible + Label@MAP_STATUS_B: + Y: 225 + Width: PARENT_RIGHT + Height: 25 + Font: Tiny + Align: Center + Text: with this version of OpenRA Container@MAP_DOWNLOADABLE: Width: PARENT_RIGHT Height: PARENT_BOTTOM diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index f9d83ecc00..70ba4737af 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -13,8 +13,8 @@ Folders: ~^Content/cnc MapFolders: - ./mods/cnc/maps - ~^maps/cnc + ./mods/cnc/maps: System + ~^maps/cnc: User Packages: bluetib.mix diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index 6f4ebedb6c..4e43d74261 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -17,8 +17,8 @@ Folders: ~^Content/d2k/Music MapFolders: - ./mods/d2k/maps - ~^maps/d2k + ./mods/d2k/maps: System + ~^maps/d2k: User Packages: SOUND.RS diff --git a/mods/modchooser/mod.yaml b/mods/modchooser/mod.yaml index f397b519c8..4a290c6bac 100644 --- a/mods/modchooser/mod.yaml +++ b/mods/modchooser/mod.yaml @@ -47,5 +47,4 @@ Fonts: TinyBold: Font:FreeSansBold.ttf Size:10 -Packages: LobbyDefaults: \ No newline at end of file diff --git a/mods/ra/chrome/lobby-mappreview.yaml b/mods/ra/chrome/lobby-mappreview.yaml index dcffed4b27..7990a72bef 100644 --- a/mods/ra/chrome/lobby-mappreview.yaml +++ b/mods/ra/chrome/lobby-mappreview.yaml @@ -38,6 +38,41 @@ Container@LOBBY_MAP_PREVIEW: Height: 25 Font: Tiny Align: Center + Container@MAP_INVALID: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Children: + Background@MAP_BG: + Width: PARENT_RIGHT + Height: 214 + Background: dialog3 + Children: + MapPreview@MAP_PREVIEW: + X: 1 + Y: 1 + Width: PARENT_RIGHT-2 + Height: PARENT_BOTTOM-2 + TooltipContainer: TOOLTIP_CONTAINER + Label@MAP_TITLE: + Y: 215 + Width: PARENT_RIGHT + Height: 25 + Font: Bold + Align: Center + Label@MAP_STATUS_A: + Y:232 + Width: PARENT_RIGHT + Height: 25 + Font: Tiny + Align: Center + Text: This map is not compatible + Label@MAP_STATUS_B: + Y:245 + Width: PARENT_RIGHT + Height: 25 + Font: Tiny + Align: Center + Text: with this version of OpenRA Container@MAP_DOWNLOADABLE: Width: PARENT_RIGHT Height: PARENT_BOTTOM diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index dd2e5b3e74..3055ff3e36 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -13,8 +13,8 @@ Folders: ~^Content/ra MapFolders: - ./mods/ra/maps - ~^maps/ra + ./mods/ra/maps: System + ~^maps/ra: User Packages: ~main.mix diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index 313607952e..d31c956c95 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -15,8 +15,8 @@ Folders: ./mods/ra/uibits MapFolders: - ./mods/ts/maps - ~^maps/ts + ./mods/ts/maps: System + ~^maps/ts: User Packages: # Tiberian Sun