diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index e385e98739..be4b56cb58 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -370,7 +370,7 @@ namespace OpenRA modData = new ModData(mod); Renderer.InitializeFonts(modData.Manifest); modData.InitializeLoaders(); - modData.LoadMaps(); + modData.MapCache.LoadMaps(); PerfHistory.items["render"].hasNormalTick = false; PerfHistory.items["batches"].hasNormalTick = false; @@ -401,7 +401,7 @@ namespace OpenRA if (Settings.Server.DedicatedLoop) { Console.WriteLine("Starting a new server instance..."); - modData.LoadMaps(); + modData.MapCache.LoadMaps(); continue; } @@ -426,13 +426,14 @@ namespace OpenRA static string ChooseShellmap() { - var shellmaps = modData.AvailableMaps - .Where(m => m.Value.UseAsShellmap); + var shellmaps = modData.MapCache + .Where(m => m.Status == MapStatus.Available && m.Map.UseAsShellmap) + .Select(m => m.Uid); if (!shellmaps.Any()) throw new InvalidDataException("No valid shellmaps available"); - return shellmaps.Random(CosmeticRandom).Key; + return shellmaps.Random(CosmeticRandom); } static bool quit; @@ -553,7 +554,7 @@ namespace OpenRA WebClient webClient = new WebClient(); webClient.DownloadFile(url, tempFile); File.Move(tempFile, mapPath); - Game.modData.AvailableMaps.Add(mapHash, new Map(mapPath)); + Game.modData.MapCache[mapHash].UpdateFromMap(new Map(mapPath)); Log.Write("debug", "New map has been downloaded to '{0}'", mapPath); return true; diff --git a/OpenRA.Game/Graphics/SheetBuilder.cs b/OpenRA.Game/Graphics/SheetBuilder.cs index 0cee8661ef..7915f4fd8d 100644 --- a/OpenRA.Game/Graphics/SheetBuilder.cs +++ b/OpenRA.Game/Graphics/SheetBuilder.cs @@ -67,6 +67,14 @@ namespace OpenRA.Graphics return rect; } + public Sprite Add(Bitmap src) + { + var rect = Allocate(src.Size); + Util.FastCopyIntoSprite(rect, src); + current.CommitData(); + return rect; + } + public Sprite Add(Size size, byte paletteIndex) { var data = new byte[size.Width * size.Height]; diff --git a/OpenRA.Game/Graphics/Util.cs b/OpenRA.Game/Graphics/Util.cs index a997d1b6fa..4f179834c8 100644 --- a/OpenRA.Game/Graphics/Util.cs +++ b/OpenRA.Game/Graphics/Util.cs @@ -9,6 +9,8 @@ #endregion using System; +using System.Drawing; +using System.Drawing.Imaging; using OpenRA.FileFormats.Graphics; namespace OpenRA.Graphics @@ -59,6 +61,36 @@ namespace OpenRA.Graphics } } + public static void FastCopyIntoSprite(Sprite dest, Bitmap src) + { + var destStride = dest.sheet.Size.Width; + var width = dest.bounds.Width; + var height = dest.bounds.Height; + + var srcData = src.LockBits(src.Bounds(), + ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); + + unsafe + { + var c = (int*)srcData.Scan0; + + // Cast the data to an int array so we can copy the src data directly + fixed (byte* bd = &dest.sheet.Data[0]) + { + var data = (int*)bd; + var x = dest.bounds.Left; + var y = dest.bounds.Top; + + for (var j = 0; j < height; j++) + for (var i = 0; i < width; i++) + data[(y + j) * destStride + x + i] = *(c + (j * srcData.Stride >> 2) + i); + } + } + + src.UnlockBits(srcData); + + } + public static float[] IdentityMatrix() { return Exts.MakeArray(16, j => (j % 5 == 0) ? 1.0f : 0); diff --git a/OpenRA.Game/MapCache.cs b/OpenRA.Game/MapCache.cs new file mode 100755 index 0000000000..fa751824f1 --- /dev/null +++ b/OpenRA.Game/MapCache.cs @@ -0,0 +1,148 @@ +#region Copyright & License Information +/* + * Copyright 2007-2014 The OpenRA Developers (see AUTHORS) + * 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Threading; +using OpenRA.FileFormats; +using OpenRA.Graphics; +using OpenRA.Widgets; + +namespace OpenRA +{ + public class MapCache : IEnumerable + { + public static readonly MapPreview UnknownMap = new MapPreview(null, null); + readonly Cache previews; + readonly Manifest manifest; + readonly SheetBuilder sheetBuilder; + Thread previewLoaderThread; + object syncRoot = new object(); + Queue generateMinimap = new Queue(); + + public MapCache(Manifest m) + { + manifest = m; + previews = new Cache(uid => new MapPreview(uid, this)); + sheetBuilder = new SheetBuilder(SheetType.BGRA); + } + + public void LoadMaps() + { + var paths = manifest.MapFolders.SelectMany(f => FindMapsIn(f)); + foreach (var path in paths) + { + try + { + var map = new Map(path, manifest.Mod.Id); + if (manifest.MapCompatibility.Contains(map.RequiresMod)) + previews[map.Uid].UpdateFromMap(map); + } + catch (Exception e) + { + Console.WriteLine("Failed to load map: {0}", path); + Console.WriteLine("Details: {0}", e); + } + } + } + + static IEnumerable FindMapsIn(string dir) + { + string[] noMaps = { }; + + // Ignore optional flag + if (dir.StartsWith("~")) + dir = dir.Substring(1); + + // Paths starting with ^ are relative to the user directory + if (dir.StartsWith("^")) + dir = Platform.SupportDir + dir.Substring(1); + + if (!Directory.Exists(dir)) + return noMaps; + + var dirsWithMaps = Directory.GetDirectories(dir) + .Where(d => Directory.GetFiles(d, "map.yaml").Any() && Directory.GetFiles(d, "map.bin").Any()); + + return dirsWithMaps.Concat(Directory.GetFiles(dir, "*.oramap")); + } + + void LoadAsyncInternal() + { + for (;;) + { + MapPreview p; + lock (syncRoot) + { + if (generateMinimap.Count == 0) + break; + + p = generateMinimap.Peek(); + + // Preview already exists + if (p.Minimap != null) + { + generateMinimap.Dequeue(); + continue; + } + } + + // Render the minimap into the shared sheet + // Note: this is not generally thread-safe, but it works here because: + // (a) This worker is the only thread writing to this sheet + // (b) The main thread is the only thread reading this sheet + // (c) The sheet is marked dirty after the write is completed, + // which causes the main thread to copy this to the texture during + // the next render cycle. + // (d) Any partially written bytes from the next minimap is in an + // unallocated area, and will be committed in the next cycle. + var bitmap = Minimap.RenderMapPreview(p.Map, true); + p.Minimap = sheetBuilder.Add(bitmap); + + lock (syncRoot) + generateMinimap.Dequeue(); + + // Yuck... But this helps the UI Jank when opening the map selector significantly. + Thread.Sleep(50); + } + } + + public void CacheMinimap(MapPreview preview) + { + lock (syncRoot) + generateMinimap.Enqueue(preview); + + if (previewLoaderThread == null || !previewLoaderThread.IsAlive) + { + previewLoaderThread = new Thread(LoadAsyncInternal); + previewLoaderThread.Start(); + } + } + + public MapPreview this[string key] + { + get { return previews[key]; } + } + + public IEnumerator GetEnumerator() + { + return previews.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/OpenRA.Game/MapPreview.cs b/OpenRA.Game/MapPreview.cs new file mode 100755 index 0000000000..1bb8352d89 --- /dev/null +++ b/OpenRA.Game/MapPreview.cs @@ -0,0 +1,91 @@ +#region Copyright & License Information +/* + * Copyright 2007-2014 The OpenRA Developers (see AUTHORS) + * 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Threading; +using OpenRA.FileFormats; +using OpenRA.Graphics; +using OpenRA.Widgets; + +namespace OpenRA +{ + public enum MapStatus { Available, Unavailable } + public class MapPreview + { + static readonly List NoSpawns = new List(); + + public readonly string Uid; + public string Title { get; private set; } + public string Type { get; private set; } + public string Author { get; private set; } + public int PlayerCount { get; private set; } + public List SpawnPoints { get; private set; } + public Rectangle Bounds { get; private set; } + public Map Map { get; private set; } + public MapStatus Status { get; private set; } + + Sprite minimap; + bool generatingMinimap; + public Sprite Minimap + { + get + { + if (minimap != null) + return minimap; + + if (!generatingMinimap && Status == MapStatus.Available) + { + generatingMinimap = true; + cache.CacheMinimap(this); + } + + return null; + } + + set + { + minimap = value; + generatingMinimap = false; + } + } + + MapCache cache; + public MapPreview(string uid, MapCache cache) + { + this.cache = cache; + Uid = uid; + Title = "Unknown Map"; + Type = "Unknown"; + Author = "Unknown Author"; + PlayerCount = 0; + Bounds = Rectangle.Empty; + SpawnPoints = NoSpawns; + Status = MapStatus.Unavailable; + } + + public void UpdateFromMap(Map m) + { + Map = m; + Title = m.Title; + Type = m.Type; + Type = m.Type; + Author = m.Author; + PlayerCount = m.Players.Count(x => x.Value.Playable); + Bounds = m.Bounds; + SpawnPoints = m.GetSpawnPoints().ToList(); + Status = MapStatus.Available; + } + } +} diff --git a/OpenRA.Game/ModData.cs b/OpenRA.Game/ModData.cs index 55ad688f56..f5a67f5b5a 100755 --- a/OpenRA.Game/ModData.cs +++ b/OpenRA.Game/ModData.cs @@ -22,34 +22,13 @@ namespace OpenRA { public readonly Manifest Manifest; public readonly ObjectCreator ObjectCreator; - public Dictionary AvailableMaps { get; private set; } public readonly WidgetLoader WidgetLoader; + public readonly MapCache MapCache; public ILoadScreen LoadScreen = null; public SheetBuilder SheetBuilder; public SpriteLoader SpriteLoader; public VoxelLoader VoxelLoader; - public static IEnumerable FindMapsIn(string dir) - { - string[] noMaps = { }; - - // ignore optional flag - if (dir.StartsWith("~")) - dir = dir.Substring(1); - - // paths starting with ^ are relative to the user directory - if (dir.StartsWith("^")) - dir = Platform.SupportDir + dir.Substring(1); - - if (!Directory.Exists(dir)) - return noMaps; - - var dirsWithMaps = Directory.GetDirectories(dir) - .Where(d => Directory.GetFiles(d, "map.yaml").Any() && Directory.GetFiles(d, "map.bin").Any()); - - return dirsWithMaps.Concat(Directory.GetFiles(dir, "*.oramap")); - } - public ModData(string mod) { Languages = new string[0]; @@ -59,6 +38,7 @@ namespace OpenRA LoadScreen.Init(Manifest, Manifest.LoadScreen.NodesDict.ToDictionary(x => x.Key, x => x.Value.Value)); LoadScreen.Display(); WidgetLoader = new WidgetLoader(this); + MapCache = new MapCache(Manifest); // HACK: Mount only local folders so we have a half-working environment for the asset installer FileSystem.UnmountAll(); @@ -119,17 +99,13 @@ namespace OpenRA FieldLoader.Translations = translations; } - public void LoadMaps() - { - AvailableMaps = FindMaps(); - } - public Map PrepareMap(string uid) { LoadScreen.Display(); - if (!AvailableMaps.ContainsKey(uid)) + + var map = MapCache[uid].Map; + if (map == null) throw new InvalidDataException("Invalid map uid: {0}".F(uid)); - var map = new Map(AvailableMaps[uid].Path); LoadTranslations(map); @@ -148,33 +124,6 @@ namespace OpenRA VoxelProvider.Initialize(Manifest.VoxelSequences, map.VoxelSequences); return map; } - - public Dictionary FindMaps() - { - var paths = Manifest.MapFolders.SelectMany(f => FindMapsIn(f)); - var ret = new Dictionary(); - foreach (var path in paths) - { - try - { - var map = new Map(path, Manifest.Mod.Id); - if (Manifest.MapCompatibility.Contains(map.RequiresMod)) - ret.Add(map.Uid, map); - } - catch (Exception e) - { - Console.WriteLine("Failed to load map: {0}", path); - Console.WriteLine("Details: {0}", e); - } - } - - return ret; - } - - public Map FindMapByUid(string uid) - { - return AvailableMaps.ContainsKey(uid) ? AvailableMaps[uid] : null; - } } public interface ILoadScreen diff --git a/OpenRA.Game/Network/GameServer.cs b/OpenRA.Game/Network/GameServer.cs index 2cd7eb7754..e289325511 100644 --- a/OpenRA.Game/Network/GameServer.cs +++ b/OpenRA.Game/Network/GameServer.cs @@ -37,7 +37,7 @@ namespace OpenRA.Network // Don't have the map locally // TODO: We allow joining, then drop on game start if the map isn't available - if (!Game.modData.AvailableMaps.ContainsKey(Map) && !Game.Settings.Game.AllowDownloading) + if (Game.modData.MapCache[Map].Status != MapStatus.Available && !Game.Settings.Game.AllowDownloading) return false; return true; diff --git a/OpenRA.Game/Network/Replay.cs b/OpenRA.Game/Network/Replay.cs index 155489bcc4..a62f628860 100644 --- a/OpenRA.Game/Network/Replay.cs +++ b/OpenRA.Game/Network/Replay.cs @@ -35,10 +35,7 @@ namespace OpenRA.Network return null; var map = LobbyInfo.GlobalSettings.Map; - if (!Game.modData.AvailableMaps.ContainsKey(map)) - return null; - - return Game.modData.AvailableMaps[map]; + return Game.modData.MapCache[map].Map; } } } diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 331c6c1829..0085f53d30 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -232,6 +232,8 @@ + + diff --git a/OpenRA.Game/Widgets/MapPreviewWidget.cs b/OpenRA.Game/Widgets/MapPreviewWidget.cs index cecd76880e..dd4eb9e876 100644 --- a/OpenRA.Game/Widgets/MapPreviewWidget.cs +++ b/OpenRA.Game/Widgets/MapPreviewWidget.cs @@ -21,7 +21,7 @@ namespace OpenRA.Widgets { public class MapPreviewWidget : Widget { - public Func Map = () => null; + public Func Preview = () => null; public Func> SpawnClients = () => new Dictionary(); public Action OnMouseDown = _ => {}; public bool IgnoreMouseInput = false; @@ -32,6 +32,9 @@ namespace OpenRA.Widgets Lazy tooltipContainer; public int TooltipSpawnIndex = -1; + Rectangle MapRect; + float PreviewScale = 0; + public MapPreviewWidget() { tooltipContainer = Lazy.New(() => Ui.Root.Get(TooltipContainer)); @@ -40,8 +43,7 @@ namespace OpenRA.Widgets protected MapPreviewWidget(MapPreviewWidget other) : base(other) { - lastMap = other.lastMap; - Map = other.Map; + Preview = other.Preview; SpawnClients = other.SpawnClients; ShowSpawnPoints = other.ShowSpawnPoints; TooltipTemplate = other.TooltipTemplate; @@ -65,74 +67,51 @@ namespace OpenRA.Widgets public override void MouseEntered() { - if (TooltipContainer == null) return; - tooltipContainer.Value.SetTooltip(TooltipTemplate, new WidgetArgs() {{ "preview", this }}); + if (TooltipContainer != null) + tooltipContainer.Value.SetTooltip(TooltipTemplate, new WidgetArgs() {{ "preview", this }}); } public override void MouseExited() { - if (TooltipContainer == null) return; - tooltipContainer.Value.RemoveTooltip(); + if (TooltipContainer != null) + tooltipContainer.Value.RemoveTooltip(); } public int2 ConvertToPreview(CPos point) { - var map = Map(); - return new int2(MapRect.X + (int)(PreviewScale*(point.X - map.Bounds.Left)) , MapRect.Y + (int)(PreviewScale*(point.Y - map.Bounds.Top))); + var preview = Preview(); + return new int2(MapRect.X + (int)(PreviewScale*(point.X - preview.Bounds.Left)) , MapRect.Y + (int)(PreviewScale*(point.Y - preview.Bounds.Top))); } - Sheet mapChooserSheet; - Sprite mapChooserSprite; - Map lastMap; - Rectangle MapRect; - float PreviewScale = 0; - + Sprite minimap; public override void Draw() { - var map = Map(); - if (map == null) + var preview = Preview(); + if (preview == null) return; - // Preview unavailable - if (!Loaded) - { - GeneratePreview(); + // Stash a copy of the minimap to ensure consistency + // (it may be modified by another thread) + minimap = preview.Minimap; + if (minimap == null) return; - } - - if (lastMap != map) - { - lastMap = map; - - // Update image data - Bitmap preview; - lock (syncRoot) - preview = Previews[map.Uid]; - - if (mapChooserSheet == null || mapChooserSheet.Size.Width != preview.Width || mapChooserSheet.Size.Height != preview.Height) - mapChooserSheet = new Sheet(new Size(preview.Width, preview.Height)); - - mapChooserSheet.Texture.SetData(preview); - mapChooserSprite = new Sprite(mapChooserSheet, new Rectangle(0, 0, map.Bounds.Width, map.Bounds.Height), TextureChannel.Alpha); - } // Update map rect - PreviewScale = Math.Min(RenderBounds.Width * 1.0f / map.Bounds.Width, RenderBounds.Height * 1.0f / map.Bounds.Height); - var size = Math.Max(map.Bounds.Width, map.Bounds.Height); - var dw = (int)(PreviewScale * (size - map.Bounds.Width)) / 2; - var dh = (int)(PreviewScale * (size - map.Bounds.Height)) / 2; - MapRect = new Rectangle(RenderBounds.X + dw, RenderBounds.Y + dh, (int)(map.Bounds.Width * PreviewScale), (int)(map.Bounds.Height * PreviewScale)); + PreviewScale = Math.Min(RenderBounds.Width / minimap.size.X, RenderBounds.Height / minimap.size.Y); + var w = (int)(PreviewScale * minimap.size.X); + var h = (int)(PreviewScale * minimap.size.Y); + var x = RenderBounds.X + (RenderBounds.Width - w) / 2; + var y = RenderBounds.Y + (RenderBounds.Height - h) / 2; + MapRect = new Rectangle(x, y, w, h); - Game.Renderer.RgbaSpriteRenderer.DrawSprite(mapChooserSprite, - new float2(MapRect.Location), - new float2(MapRect.Size)); + Game.Renderer.RgbaSpriteRenderer.DrawSprite(minimap, new float2(MapRect.Location), new float2(MapRect.Size)); TooltipSpawnIndex = -1; if (ShowSpawnPoints) { var colors = SpawnClients().ToDictionary(c => c.Key, c => c.Value.Color.RGB); - var spawnPoints = map.GetSpawnPoints().ToList(); + var spawnPoints = preview.SpawnPoints; foreach (var p in spawnPoints) { var owned = colors.ContainsKey(p); @@ -151,79 +130,6 @@ namespace OpenRA.Widgets } } - // Async map preview generation bits - enum PreviewStatus { Invalid, Uncached, Generating, Cached } - static Thread previewLoaderThread; - static object syncRoot = new object(); - static Queue cacheUids = new Queue(); - static readonly Dictionary Previews = new Dictionary(); - - void LoadAsyncInternal() - { - for (;;) - { - string uid; - lock (syncRoot) - { - if (cacheUids.Count == 0) - break; - uid = cacheUids.Peek(); - } - - var bitmap = Minimap.RenderMapPreview(Game.modData.AvailableMaps[uid], false); - lock (syncRoot) - { - // TODO: We should add previews to a sheet here (with multiple previews per sheet) - Previews.Add(uid, bitmap); - cacheUids.Dequeue(); - } - - // Yuck... But this helps the UI Jank when opening the map selector significantly. - Thread.Sleep(50); - } - } - - bool compatibleTileset; - - void GeneratePreview() - { - var m = Map(); - if (m == null) - return; - - compatibleTileset = Rules.TileSets.Values.Any(t => t.Id == m.Tileset); - - var status = Status(m); - if (status == PreviewStatus.Uncached) - lock (syncRoot) - cacheUids.Enqueue(m.Uid); - - if (previewLoaderThread == null || !previewLoaderThread.IsAlive) - { - previewLoaderThread = new Thread(LoadAsyncInternal); - previewLoaderThread.Start(); - } - } - - PreviewStatus Status(Map m) - { - if (m == null) - return PreviewStatus.Invalid; - - lock (syncRoot) - { - if (Previews.ContainsKey(m.Uid)) - return PreviewStatus.Cached; - - if (cacheUids.Contains(m.Uid)) - return PreviewStatus.Generating; - - if (!compatibleTileset) - return PreviewStatus.Invalid; - } - return PreviewStatus.Uncached; - } - - public bool Loaded { get { return Status(Map()) == PreviewStatus.Cached; } } + public bool Loaded { get { return minimap != null; } } } } diff --git a/OpenRA.Game/Widgets/WidgetUtils.cs b/OpenRA.Game/Widgets/WidgetUtils.cs index 63f49a0955..f4f70cd0f9 100644 --- a/OpenRA.Game/Widgets/WidgetUtils.cs +++ b/OpenRA.Game/Widgets/WidgetUtils.cs @@ -225,14 +225,13 @@ namespace OpenRA.Widgets public static Action Once( Action a ) { return () => { if (a != null) { a(); a = null; } }; } - public static string ChooseInitialMap(string map) + public static string ChooseInitialMap(string initialUid) { - var availableMaps = Game.modData.AvailableMaps; - if (string.IsNullOrEmpty(map) || !availableMaps.ContainsKey(map)) + if (string.IsNullOrEmpty(initialUid) || Game.modData.MapCache[initialUid].Status != MapStatus.Available) { - Func isIdealMap = m => + Func isIdealMap = m => { - if (!m.Selectable) + if (m.Status != MapStatus.Available || !m.Map.Selectable) return false; // Other map types may have confusing settings or gameplay @@ -240,22 +239,22 @@ namespace OpenRA.Widgets return false; // Maps with bots disabled confuse new players - if (m.Players.Any(s => !s.Value.AllowBots)) + if (m.Map.Players.Any(s => !s.Value.AllowBots)) return false; // Large maps expose unfortunate performance problems - if (m.MapSize.X > 128 || m.MapSize.Y > 128) + if (m.Bounds.Width > 128 || m.Bounds.Height > 128) return false; return true; }; - var selected = availableMaps.Values.Where(m => isIdealMap(m)).RandomOrDefault(Game.CosmeticRandom) ?? - availableMaps.Values.First(m => m.Selectable); + var selected = Game.modData.MapCache.Where(m => isIdealMap(m)).RandomOrDefault(Game.CosmeticRandom) ?? + Game.modData.MapCache.First(m => m.Status == MapStatus.Available && m.Map.Selectable); return selected.Uid; } - return map; + return initialUid; } } diff --git a/OpenRA.Lint/YamlChecker.cs b/OpenRA.Lint/YamlChecker.cs index 679f7fbe8c..e35fd3fa3d 100644 --- a/OpenRA.Lint/YamlChecker.cs +++ b/OpenRA.Lint/YamlChecker.cs @@ -9,6 +9,7 @@ #endregion using System; +using System.Collections.Generic; using System.Linq; using OpenRA.FileFormats; using OpenRA.Traits; @@ -52,14 +53,16 @@ namespace OpenRA.Lint AppDomain.CurrentDomain.AssemblyResolve += FileSystem.ResolveAssembly; Game.modData = new ModData(mod); - var maps = new Map[] { new Map() }; - if (!string.IsNullOrEmpty(map)) - maps = new Map[] { new Map(map) }; - else + IEnumerable maps; + if (string.IsNullOrEmpty(map)) { - Game.modData.LoadMaps(); - maps = Game.modData.AvailableMaps.Values.ToArray(); + Game.modData.MapCache.LoadMaps(); + maps = Game.modData.MapCache + .Where(m => m.Status == MapStatus.Available) + .Select(m => m.Map); } + else + maps = new [] { new Map(map) }; foreach (var testMap in maps) { diff --git a/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs b/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs index 37ef9d3b4f..6565b1b796 100644 --- a/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs +++ b/OpenRA.Mods.RA/ServerTraits/LobbyCommands.cs @@ -280,7 +280,7 @@ namespace OpenRA.Mods.RA.Server return true; } - if (!server.ModData.AvailableMaps.ContainsKey(s)) + if (server.ModData.MapCache[s].Status != MapStatus.Available) { server.SendOrderTo(conn, "Message", "Map was not found on server"); return true; @@ -720,7 +720,7 @@ namespace OpenRA.Mods.RA.Server static void LoadMap(S server) { - server.Map = new Map(server.ModData.AvailableMaps[server.LobbyInfo.GlobalSettings.Map].Path); + server.Map = server.ModData.MapCache[server.LobbyInfo.GlobalSettings.Map].Map; server.LobbyInfo.Slots = server.Map.Players .Select(p => MakeSlotFromPlayerReference(p.Value)) .Where(s => s != null) diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs index f8122710a2..7c64cbf925 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs @@ -38,8 +38,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic ScrollPanelWidget players; Dictionary countryNames; - string mapUid; - Map map; + MapPreview preview = MapCache.UnknownMap; ColorPreviewManagerWidget colorPreview; @@ -124,31 +123,21 @@ namespace OpenRA.Mods.RA.Widgets.Logic colorPreview.Color = Game.Settings.Player.Color; var mapPreview = lobby.Get("MAP_PREVIEW"); - mapPreview.IsVisible = () => map != null; - mapPreview.Map = () => map; - mapPreview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, mapPreview, map, mi); - mapPreview.SpawnClients = () => LobbyUtils.GetSpawnClients(orderManager, map); + mapPreview.Preview = () => preview; + mapPreview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, mapPreview, preview, mi); + mapPreview.SpawnClients = () => LobbyUtils.GetSpawnClients(orderManager, preview); var mapTitle = lobby.GetOrNull("MAP_TITLE"); if (mapTitle != null) - { - mapTitle.IsVisible = () => map != null; - mapTitle.GetText = () => map.Title; - } + mapTitle.GetText = () => preview.Title; var mapType = lobby.GetOrNull("MAP_TYPE"); if (mapType != null) - { - mapType.IsVisible = () => map != null; - mapType.GetText = () => map.Type; - } + mapType.GetText = () => preview.Type; var mapAuthor = lobby.GetOrNull("MAP_AUTHOR"); if (mapAuthor != null) - { - mapAuthor.IsVisible = () => map != null; - mapAuthor.GetText = () => "Created by {0}".F(map.Author); - } + mapAuthor.GetText = () => "Created by {0}".F(preview.Author); countryNames = Rules.Info["world"].Traits.WithInterface() .Where(c => c.Selectable) @@ -174,7 +163,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic Ui.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs() { - { "initialMap", map.Uid }, + { "initialMap", preview.Uid }, { "onExit", () => { } }, { "onSelect", onSelect } }); @@ -308,7 +297,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (allowCheats != null) { allowCheats.IsChecked = () => orderManager.LobbyInfo.GlobalSettings.AllowCheats; - allowCheats.IsDisabled = () => map.Options.Cheats.HasValue || configurationDisabled(); + allowCheats.IsDisabled = () => preview.Status != MapStatus.Available || preview.Map.Options.Cheats.HasValue || configurationDisabled(); allowCheats.OnClick = () => orderManager.IssueOrder(Order.Command( "allowcheats {0}".F(!orderManager.LobbyInfo.GlobalSettings.AllowCheats))); } @@ -317,7 +306,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (crates != null) { crates.IsChecked = () => orderManager.LobbyInfo.GlobalSettings.Crates; - crates.IsDisabled = () => map.Options.Crates.HasValue || configurationDisabled(); + crates.IsDisabled = () => preview.Status != MapStatus.Available || preview.Map.Options.Crates.HasValue || configurationDisabled(); crates.OnClick = () => orderManager.IssueOrder(Order.Command( "crates {0}".F(!orderManager.LobbyInfo.GlobalSettings.Crates))); } @@ -326,7 +315,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (allybuildradius != null) { allybuildradius.IsChecked = () => orderManager.LobbyInfo.GlobalSettings.AllyBuildRadius; - allybuildradius.IsDisabled = () => map.Options.AllyBuildRadius.HasValue || configurationDisabled(); + allybuildradius.IsDisabled = () => preview.Status != MapStatus.Available || preview.Map.Options.AllyBuildRadius.HasValue || configurationDisabled(); allybuildradius.OnClick = () => orderManager.IssueOrder(Order.Command( "allybuildradius {0}".F(!orderManager.LobbyInfo.GlobalSettings.AllyBuildRadius))); } @@ -335,7 +324,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (fragileAlliance != null) { fragileAlliance.IsChecked = () => orderManager.LobbyInfo.GlobalSettings.FragileAlliances; - fragileAlliance.IsDisabled = () => map.Options.FragileAlliances.HasValue || configurationDisabled(); + fragileAlliance.IsDisabled = () => preview.Status != MapStatus.Available || preview.Map.Options.FragileAlliances.HasValue || configurationDisabled(); fragileAlliance.OnClick = () => orderManager.IssueOrder(Order.Command( "fragilealliance {0}".F(!orderManager.LobbyInfo.GlobalSettings.FragileAlliances))); } @@ -343,12 +332,12 @@ namespace OpenRA.Mods.RA.Widgets.Logic var difficulty = optionsBin.GetOrNull("DIFFICULTY_DROPDOWNBUTTON"); if (difficulty != null) { - difficulty.IsVisible = () => map.Options.Difficulties.Any(); - difficulty.IsDisabled = configurationDisabled; + difficulty.IsVisible = () => preview.Status == MapStatus.Available && preview.Map.Options.Difficulties.Any(); + difficulty.IsDisabled = () => preview.Status != MapStatus.Available || configurationDisabled(); difficulty.GetText = () => orderManager.LobbyInfo.GlobalSettings.Difficulty; difficulty.OnMouseDown = _ => { - var options = map.Options.Difficulties.Select(d => new DropDownOption + var options = preview.Map.Options.Difficulties.Select(d => new DropDownOption { Title = d, IsSelected = () => orderManager.LobbyInfo.GlobalSettings.Difficulty == d, @@ -380,8 +369,8 @@ namespace OpenRA.Mods.RA.Widgets.Logic var classes = Rules.Info["world"].Traits.WithInterface() .Select(a => a.Class).Distinct(); - startingUnits.IsDisabled = () => !map.Options.ConfigurableStartingUnits || configurationDisabled(); - startingUnits.GetText = () => !map.Options.ConfigurableStartingUnits ? "Not Available" : className(orderManager.LobbyInfo.GlobalSettings.StartingUnitsClass); + startingUnits.IsDisabled = () => preview.Status != MapStatus.Available || !preview.Map.Options.ConfigurableStartingUnits || configurationDisabled(); + startingUnits.GetText = () => preview.Status != MapStatus.Available || !preview.Map.Options.ConfigurableStartingUnits ? "Not Available" : className(orderManager.LobbyInfo.GlobalSettings.StartingUnitsClass); startingUnits.OnMouseDown = _ => { var options = classes.Select(c => new DropDownOption @@ -407,8 +396,8 @@ namespace OpenRA.Mods.RA.Widgets.Logic var startingCash = optionsBin.GetOrNull("STARTINGCASH_DROPDOWNBUTTON"); if (startingCash != null) { - startingCash.IsDisabled = () => map.Options.StartingCash.HasValue || configurationDisabled(); - startingCash.GetText = () => map.Options.StartingCash.HasValue ? "Not Available" : "${0}".F(orderManager.LobbyInfo.GlobalSettings.StartingCash); + startingCash.IsDisabled = () => preview.Status != MapStatus.Available || preview.Map.Options.StartingCash.HasValue || configurationDisabled(); + startingCash.GetText = () => preview.Status != MapStatus.Available || preview.Map.Options.StartingCash.HasValue ? "Not Available" : "${0}".F(orderManager.LobbyInfo.GlobalSettings.StartingCash); startingCash.OnMouseDown = _ => { var options = Rules.Info["player"].Traits.Get().SelectableCash.Select(c => new DropDownOption @@ -433,7 +422,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (enableShroud != null) { enableShroud.IsChecked = () => orderManager.LobbyInfo.GlobalSettings.Shroud; - enableShroud.IsDisabled = () => map.Options.Shroud.HasValue || configurationDisabled(); + enableShroud.IsDisabled = () => preview.Status != MapStatus.Available || preview.Map.Options.Shroud.HasValue || configurationDisabled(); enableShroud.OnClick = () => orderManager.IssueOrder(Order.Command( "shroud {0}".F(!orderManager.LobbyInfo.GlobalSettings.Shroud))); } @@ -442,7 +431,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (enableFog != null) { enableFog.IsChecked = () => orderManager.LobbyInfo.GlobalSettings.Fog; - enableFog.IsDisabled = () => map.Options.Fog.HasValue || configurationDisabled(); + enableFog.IsDisabled = () => preview.Status != MapStatus.Available || preview.Map.Options.Fog.HasValue || configurationDisabled(); enableFog.OnClick = () => orderManager.IssueOrder(Order.Command( "fog {0}".F(!orderManager.LobbyInfo.GlobalSettings.Fog))); } @@ -529,24 +518,25 @@ namespace OpenRA.Mods.RA.Widgets.Logic void UpdateCurrentMap() { - if (mapUid == orderManager.LobbyInfo.GlobalSettings.Map) + var uid = orderManager.LobbyInfo.GlobalSettings.Map; + if (preview.Uid == uid) return; - mapUid = orderManager.LobbyInfo.GlobalSettings.Map; - - if (!Game.modData.AvailableMaps.ContainsKey(mapUid)) + preview = Game.modData.MapCache[uid]; + if (preview.Status != MapStatus.Available) + { if (Game.Settings.Game.AllowDownloading) { - Game.DownloadMap(mapUid); + Game.DownloadMap(uid); Game.Debug("A new map has been downloaded..."); } else throw new InvalidOperationException("Server's new map doesn't exist on your system and Downloading turned off"); - map = new Map(Game.modData.AvailableMaps[mapUid].Path); + } // Restore default starting cash if the last map set it to something invalid var pri = Rules.Info["player"].Traits.Get(); - if (!map.Options.StartingCash.HasValue && !pri.SelectableCash.Contains(orderManager.LobbyInfo.GlobalSettings.StartingCash)) + if (!preview.Map.Options.StartingCash.HasValue && !pri.SelectableCash.Contains(orderManager.LobbyInfo.GlobalSettings.StartingCash)) orderManager.IssueOrder(Order.Command("startingcash {0}".F(pri.DefaultCash))); } @@ -596,7 +586,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic LobbyUtils.SetupEditableColorWidget(template, slot, client, orderManager, colorPreview); LobbyUtils.SetupEditableFactionWidget(template, slot, client, orderManager, countryNames); - LobbyUtils.SetupEditableTeamWidget(template, slot, client, orderManager, map.GetSpawnPoints().Length); + LobbyUtils.SetupEditableTeamWidget(template, slot, client, orderManager, preview.SpawnPoints.Count); LobbyUtils.SetupEditableReadyWidget(template, slot, client, orderManager); } else diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs index 8ed0c7b3b5..0e5da7bdfa 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs @@ -130,26 +130,23 @@ namespace OpenRA.Mods.RA.Widgets.Logic color.AttachPanel(colorChooser, onExit); } - public static Dictionary GetSpawnClients(OrderManager orderManager, Map map) + public static Dictionary GetSpawnClients(OrderManager orderManager, MapPreview preview) { - var spawns = map.GetSpawnPoints(); + var spawns = preview.SpawnPoints; return orderManager.LobbyInfo.Clients .Where(c => c.SpawnPoint != 0) - .ToDictionary( - c => spawns[c.SpawnPoint - 1], - c => c); + .ToDictionary(c => spawns[c.SpawnPoint - 1], c => c); } - public static void SelectSpawnPoint(OrderManager orderManager, MapPreviewWidget mapPreview, Map map, MouseInput mi) + public static void SelectSpawnPoint(OrderManager orderManager, MapPreviewWidget mapPreview, MapPreview preview, MouseInput mi) { - if (map == null) - return; if (mi.Button != MouseButton.Left) return; + if (!orderManager.LocalClient.IsObserver && orderManager.LocalClient.State == Session.ClientState.Ready) return; - var selectedSpawn = map.GetSpawnPoints() + var selectedSpawn = preview.SpawnPoints .Select((sp, i) => Pair.New(mapPreview.ConvertToPreview(sp), i)) .Where(a => (a.First - mi.Location).LengthSquared < 64) .Select(a => a.Second + 1) diff --git a/OpenRA.Mods.RA/Widgets/Logic/MapChooserLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/MapChooserLogic.cs index 44628aa683..198e9b1db1 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/MapChooserLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/MapChooserLogic.cs @@ -30,7 +30,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic [ObjectCreator.UseCtor] internal MapChooserLogic(Widget widget, string initialMap, Action onExit, Action onSelect) { - map = Game.modData.AvailableMaps[WidgetUtils.ChooseInitialMap(initialMap)]; + map = Game.modData.MapCache[WidgetUtils.ChooseInitialMap(initialMap)].Map; widget.Get("BUTTON_OK").OnClick = () => { Ui.CloseWindow(); onSelect(map); }; widget.Get("BUTTON_CANCEL").OnClick = () => { Ui.CloseWindow(); onExit(); }; @@ -44,9 +44,9 @@ namespace OpenRA.Mods.RA.Widgets.Logic var gameModeDropdown = widget.GetOrNull("GAMEMODE_FILTER"); if (gameModeDropdown != null) { - var selectableMaps = Game.modData.AvailableMaps.Where(m => m.Value.Selectable).ToList(); + var selectableMaps = Game.modData.MapCache.Where(m => m.Status == MapStatus.Available && m.Map.Selectable); var gameModes = selectableMaps - .GroupBy(m => m.Value.Type) + .GroupBy(m => m.Type) .Select(g => Pair.New(g.Key, g.Count())).ToList(); // 'all game types' extra item @@ -87,8 +87,9 @@ namespace OpenRA.Mods.RA.Widgets.Logic void EnumerateMaps(Action onSelect) { - var maps = Game.modData.AvailableMaps - .Where(kv => kv.Value.Selectable) + var maps = Game.modData.MapCache + .Where(m => m.Status == MapStatus.Available && m.Map.Selectable) + .ToDictionary(m => m.Uid, m => m.Map) .Where(kv => kv.Value.Type == gameMode || gameMode == null) .OrderBy(kv => kv.Value.PlayerCount) .ThenBy(kv => kv.Value.Title); @@ -102,10 +103,11 @@ namespace OpenRA.Mods.RA.Widgets.Logic var titleLabel = item.Get("TITLE"); titleLabel.GetText = () => m.Title; + var preview = Game.modData.MapCache[m.Uid]; var previewWidget = item.Get("PREVIEW"); previewWidget.IgnoreMouseOver = true; previewWidget.IgnoreMouseInput = true; - previewWidget.Map = () => m; + previewWidget.Preview = () => preview; previewWidget.IsVisible = () => previewWidget.RenderBounds.IntersectsWith(scrollpanel.RenderBounds); var previewLoadingWidget = item.GetOrNull("PREVIEW_PLACEHOLDER"); diff --git a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs index e55fe67f53..a1a23ff6a2 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs @@ -51,7 +51,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic } Replay currentReplay; - Map currentMap; + MapPreview currentMap = MapCache.UnknownMap; void SelectReplay(string filename) { @@ -61,11 +61,11 @@ namespace OpenRA.Mods.RA.Widgets.Logic try { currentReplay = new Replay(filename); - currentMap = currentReplay.Map(); + currentMap = Game.modData.MapCache[currentReplay.LobbyInfo.GlobalSettings.Map]; panel.Get("DURATION").GetText = () => WidgetUtils.FormatTime(currentReplay.Duration); - panel.Get("MAP_PREVIEW").Map = () => currentMap; + panel.Get("MAP_PREVIEW").Preview = () => currentMap; panel.Get("MAP_TITLE").GetText = () => currentMap != null ? currentMap.Title : "(Unknown Map)"; @@ -77,7 +77,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic { Log.Write("debug", "Exception while parsing replay: {0}", e); currentReplay = null; - currentMap = null; + currentMap = MapCache.UnknownMap; } } diff --git a/OpenRA.Mods.RA/Widgets/Logic/ServerBrowserLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/ServerBrowserLogic.cs index 7787ea071d..efe0dd4371 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/ServerBrowserLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/ServerBrowserLogic.cs @@ -155,10 +155,8 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (game == null || game.Players == 0) return ""; - var map = Game.modData.FindMapByUid(game.Map); - - var maxPlayers = map == null ? "?" : (object)map.PlayerCount; - return "{0} / {1}".F(game.Players, maxPlayers); + var map = Game.modData.MapCache[game.Map]; + return "{0} / {1}".F(game.Players, map.PlayerCount == 0 ? "?" : map.PlayerCount.ToString()); } string GetStateLabel(GameServer game) @@ -176,11 +174,6 @@ namespace OpenRA.Mods.RA.Widgets.Logic return "Unknown server state"; } - Map GetMapPreview(GameServer game) - { - return (game == null) ? null : Game.modData.FindMapByUid(game.Map); - } - public static string GenerateModLabel(GameServer s) { Mod mod; @@ -241,20 +234,16 @@ namespace OpenRA.Mods.RA.Widgets.Logic var item = ScrollItemWidget.Setup(serverTemplate, () => currentServer == game, () => currentServer = game, () => Join(game)); + var map = Game.modData.MapCache[game.Map]; var preview = item.Get("MAP_PREVIEW"); - preview.Map = () => GetMapPreview(game); - preview.IsVisible = () => GetMapPreview(game) != null; + preview.Preview = () => map; var title = item.Get("TITLE"); title.GetText = () => game.Name; // TODO: Use game.MapTitle once the server supports it var maptitle = item.Get("MAP"); - maptitle.GetText = () => - { - var map = Game.modData.FindMapByUid(game.Map); - return map == null ? "Unknown Map" : map.Title; - }; + maptitle.GetText = () => map.Title; // TODO: Use game.MaxPlayers once the server supports it var players = item.Get("PLAYERS"); diff --git a/OpenRA.Mods.RA/Widgets/Logic/ServerCreationLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/ServerCreationLogic.cs index c70f39f428..c4d22eaebf 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/ServerCreationLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/ServerCreationLogic.cs @@ -20,7 +20,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic Widget panel; Action onCreate; Action onExit; - Map map; + MapPreview preview = MapCache.UnknownMap; bool advertiseOnline; bool allowPortForward; @@ -32,12 +32,11 @@ namespace OpenRA.Mods.RA.Widgets.Logic this.onExit = onExit; var settings = Game.Settings; + preview = Game.modData.MapCache[WidgetUtils.ChooseInitialMap(Game.Settings.Server.Map)]; panel.Get("BACK_BUTTON").OnClick = () => { Ui.CloseWindow(); onExit(); }; panel.Get("CREATE_BUTTON").OnClick = CreateAndJoin; - map = Game.modData.AvailableMaps[ WidgetUtils.ChooseInitialMap(Game.Settings.Server.Map) ]; - var mapButton = panel.GetOrNull("MAP_BUTTON"); if (mapButton != null) { @@ -45,14 +44,14 @@ namespace OpenRA.Mods.RA.Widgets.Logic { Ui.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs() { - { "initialMap", map.Uid }, + { "initialMap", preview.Uid }, { "onExit", () => {} }, - { "onSelect", (Action)(m => map = m) } + { "onSelect", (Action)(m => preview = Game.modData.MapCache[m.Uid]) } }); }; - panel.Get("MAP_PREVIEW").Map = () => map; - panel.Get("MAP_NAME").GetText = () => map.Title; + panel.Get("MAP_PREVIEW").Preview = () => preview; + panel.Get("MAP_NAME").GetText = () => preview.Title; } panel.Get("SERVER_NAME").Text = settings.Server.Name ?? ""; @@ -97,7 +96,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic Game.Settings.Server.ExternalPort = externalPort; Game.Settings.Server.AdvertiseOnline = advertiseOnline; Game.Settings.Server.AllowPortForward = allowPortForward; - Game.Settings.Server.Map = map.Uid; + Game.Settings.Server.Map = preview.Uid; Game.Settings.Server.Password = password; Game.Settings.Save(); diff --git a/OpenRA.Utility/UpgradeRules.cs b/OpenRA.Utility/UpgradeRules.cs index f87cbe2c13..753acd11bd 100644 --- a/OpenRA.Utility/UpgradeRules.cs +++ b/OpenRA.Utility/UpgradeRules.cs @@ -301,7 +301,11 @@ namespace OpenRA.Utility } Console.WriteLine("Processing Maps:"); - foreach (var map in Game.modData.FindMaps().Values) + var maps = Game.modData.MapCache + .Where(m => m.Status == MapStatus.Available) + .Select(m => m.Map); + + foreach (var map in maps) { Console.WriteLine("\t" + map.Path); UpgradeActorRules(engineDate, ref map.Rules, null, 0);