diff --git a/CHANGELOG b/CHANGELOG index d21165516a..8e40dd1188 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -117,6 +117,7 @@ NEW: Updated RenderBuildingCharge so you can set a custom charge sequence name (default is active). Added IEffectiveOwner interface for traits/logic that temporarily alter an actor's apparent owner. Renamed Spy trait to Disguise, SpyToolTip trait to DisguiseToolTip, and RenderSpy trait to RenderDisguise. + Overhauled the internal map management and preview generation code. Server: Message of the day is now shared between all mods and a default motd.txt gets created in the user directory. Asset Browser: @@ -137,6 +138,7 @@ NEW: If you spot black tiles in your Dune 2000 ARRAKIS maps, replace them with the remaining sand and rock tiles. Go to Map → Fix Open Areas to randomize them. The TestFile check in mod.yaml has been renamed to TestFiles (plural!) and now supports a comma-separated list of assets that are required to load the game. DisabledOverlay has been split from RenderBuilding. Use it together with RequiresPower and CanPowerDown for buildings or directly with Husk. + Added support for custom map previews that replace the minimap in the game browser and lobby. Add map.png inside the map package. Packaging: Removed portable install option from Windows installer as the game left without write access breaks content download and error log generation. Added HTML documentation to the Windows installer. diff --git a/OpenRA.Editor/MapSelect.cs b/OpenRA.Editor/MapSelect.cs index 68d1514497..4f2c3613b0 100644 --- a/OpenRA.Editor/MapSelect.cs +++ b/OpenRA.Editor/MapSelect.cs @@ -45,7 +45,7 @@ namespace OpenRA.Editor if (DirectoryIsEmpty(MapFolderPath)) return; - foreach (var map in ModData.FindMapsIn(MapFolderPath)) + foreach (var map in MapCache.FindMapsIn(MapFolderPath)) { ListViewItem map1 = new ListViewItem(); map1.Tag = map; diff --git a/OpenRA.Game/Download.cs b/OpenRA.Game/Download.cs index 5955d3b588..4cd997ae7d 100644 --- a/OpenRA.Game/Download.cs +++ b/OpenRA.Game/Download.cs @@ -52,6 +52,20 @@ namespace OpenRA wc.DownloadFileAsync(new Uri(url), path); } + public Download(string url, Action onProgress, Action onComplete) + { + wc = new WebClient(); + wc.Proxy = null; + + wc.DownloadProgressChanged += (_, a) => onProgress(a); + wc.DownloadDataCompleted += (_, a) => onComplete(a, cancelled); + + Game.OnQuit += Cancel; + wc.DownloadDataCompleted += (_, a) => { Game.OnQuit -= Cancel; }; + + wc.DownloadDataAsync(new Uri(url)); + } + public void Cancel() { Game.OnQuit -= Cancel; 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/Map.cs b/OpenRA.Game/Map.cs index f2ec745328..bf1f26d78a 100644 --- a/OpenRA.Game/Map.cs +++ b/OpenRA.Game/Map.cs @@ -70,6 +70,7 @@ namespace OpenRA public string Author; public string Tileset; public bool AllowStartUnitConfig = true; + public Bitmap CustomPreview; [FieldLoader.LoadUsing("LoadOptions")] public MapOptions Options; @@ -226,14 +227,17 @@ namespace OpenRA Save(path); Uid = ComputeHash(); + + if (Container.Exists("map.png")) + CustomPreview = new Bitmap(Container.GetContent("map.png")); } - public int2[] GetSpawnPoints() + public CPos[] GetSpawnPoints() { return Actors.Value.Values .Where(a => a.Type == "mpspawn") - .Select(a => a.InitDict.Get().value) - .ToArray(); + .Select(a => (CPos)a.InitDict.Get().value) + .ToArray(); } public void Save(string toPath) diff --git a/OpenRA.Game/MapCache.cs b/OpenRA.Game/MapCache.cs new file mode 100755 index 0000000000..cd0beec736 --- /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); + } + } + } + + 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")); + } + + 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 = p.CustomPreview ?? 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..de0e3d1425 --- /dev/null +++ b/OpenRA.Game/MapPreview.cs @@ -0,0 +1,93 @@ +#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 Bitmap CustomPreview { 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(); + CustomPreview = m.CustomPreview; + 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 deleted file mode 100644 index 155489bcc4..0000000000 --- a/OpenRA.Game/Network/Replay.cs +++ /dev/null @@ -1,44 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2011 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; - -namespace OpenRA.Network -{ - public class Replay - { - public readonly string Filename; - public readonly int Duration; - public readonly Session LobbyInfo; - - public Replay(string filename) - { - Filename = filename; - - using (var conn = new ReplayConnection(filename)) - { - Duration = conn.TickCount * Game.NetTickScale; - LobbyInfo = conn.LobbyInfo; - } - } - - public Map Map() - { - if (LobbyInfo == null) - return null; - - var map = LobbyInfo.GlobalSettings.Map; - if (!Game.modData.AvailableMaps.ContainsKey(map)) - return null; - - return Game.modData.AvailableMaps[map]; - } - } -} diff --git a/OpenRA.Game/Network/ServerList.cs b/OpenRA.Game/Network/ServerList.cs deleted file mode 100644 index 55cff8dc47..0000000000 --- a/OpenRA.Game/Network/ServerList.cs +++ /dev/null @@ -1,52 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2011 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.Linq; -using System.Net; -using System.Text; -using System.Threading; -using OpenRA.FileFormats; - -namespace OpenRA.Network -{ - public static class ServerList - { - public static void Query(Action onComplete) - { - var masterServerUrl = Game.Settings.Server.MasterServer; - - new Thread(() => - { - GameServer[] games = null; - try - { - var str = GetData(new Uri(masterServerUrl + "list.php")); - - var yaml = MiniYaml.FromString(str); - - games = yaml.Select(a => FieldLoader.Load(a.Value)) - .Where(gs => gs.Address != null).ToArray(); - } - catch { } - - Game.RunAfterTick(() => onComplete(games)); - }) { IsBackground = true }.Start(); - } - - static string GetData(Uri uri) - { - var wc = new WebClient(); - wc.Proxy = null; - var data = wc.DownloadData(uri); - return Encoding.UTF8.GetString(data); - } - } -} diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index 331c6c1829..6d13a70cd9 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -129,9 +129,7 @@ - - @@ -232,6 +230,8 @@ + + diff --git a/OpenRA.Game/Widgets/MapPreviewWidget.cs b/OpenRA.Game/Widgets/MapPreviewWidget.cs index 538ac9453a..dd4eb9e876 100644 --- a/OpenRA.Game/Widgets/MapPreviewWidget.cs +++ b/OpenRA.Game/Widgets/MapPreviewWidget.cs @@ -21,8 +21,8 @@ namespace OpenRA.Widgets { public class MapPreviewWidget : Widget { - public Func Map = () => null; - public Func> SpawnClients = () => new Dictionary(); + public Func Preview = () => null; + public Func> SpawnClients = () => new Dictionary(); public Action OnMouseDown = _ => {}; public bool IgnoreMouseInput = false; public bool ShowSpawnPoints = true; @@ -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(int2 point) + 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/MPStartLocations.cs b/OpenRA.Mods.RA/MPStartLocations.cs index e2f8c15a4f..2be4f1b75e 100755 --- a/OpenRA.Mods.RA/MPStartLocations.cs +++ b/OpenRA.Mods.RA/MPStartLocations.cs @@ -28,9 +28,10 @@ namespace OpenRA.Mods.RA public void WorldLoaded(World world, WorldRenderer wr) { + var spawns = world.Map.GetSpawnPoints(); var taken = world.LobbyInfo.Clients.Where(c => c.SpawnPoint != 0 && c.Slot != null) - .Select(c => (CPos) world.Map.GetSpawnPoints()[c.SpawnPoint-1]).ToList(); - var available = world.Map.GetSpawnPoints().Select(c => (CPos)c).Except(taken).ToList(); + .Select(c => spawns[c.SpawnPoint-1]).ToList(); + var available = spawns.Except(taken).ToList(); // Set spawn foreach (var kv in world.LobbyInfo.Slots) @@ -41,7 +42,7 @@ namespace OpenRA.Mods.RA var client = world.LobbyInfo.ClientInSlot(kv.Key); var spid = (client == null || client.SpawnPoint == 0) ? ChooseSpawnPoint(world, available, taken) - : (CPos)world.Map.GetSpawnPoints()[client.SpawnPoint-1]; + : world.Map.GetSpawnPoints()[client.SpawnPoint-1]; Start.Add(player, spid); } 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 5d3314dd29..e72f2272bd 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs @@ -20,27 +20,28 @@ namespace OpenRA.Mods.RA.Widgets.Logic { public class LobbyLogic { + readonly Action onStart; + readonly Action onExit; + readonly OrderManager orderManager; + readonly bool skirmishMode; + enum PanelType { Players, Options, Kick } PanelType panel = PanelType.Players; Widget lobby; - Widget EditablePlayerTemplate, NonEditablePlayerTemplate, EmptySlotTemplate, - EditableSpectatorTemplate, NonEditableSpectatorTemplate, NewSpectatorTemplate; + + Widget editablePlayerTemplate, nonEditablePlayerTemplate, emptySlotTemplate, + editableSpectatorTemplate, nonEditableSpectatorTemplate, newSpectatorTemplate; + ScrollPanelWidget chatPanel; Widget chatTemplate; - ScrollPanelWidget Players; - Dictionary CountryNames; - string MapUid; - Map Map; + ScrollPanelWidget players; + Dictionary countryNames; + MapPreview preview = MapCache.UnknownMap; ColorPreviewManagerWidget colorPreview; - readonly Action OnGameStart; - readonly Action onExit; - readonly OrderManager orderManager; - readonly bool skirmishMode; - // Listen for connection failures void ConnectionStateChanged(OrderManager om) { @@ -54,7 +55,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic Game.OpenWindow("SERVER_LOBBY", new WidgetArgs() { { "onExit", onExit }, - { "onStart", OnGameStart }, + { "onStart", onStart }, { "skirmishMode", false } }); }; @@ -90,8 +91,8 @@ namespace OpenRA.Mods.RA.Widgets.Logic { lobby = widget; this.orderManager = orderManager; - this.OnGameStart = () => { CloseWindow(); onStart(); }; - this.onExit = onExit; + this.onStart = onStart; + this.onExit = onExit; this.skirmishMode = skirmishMode; Game.LobbyInfoChanged += UpdateCurrentMap; @@ -105,49 +106,43 @@ namespace OpenRA.Mods.RA.Widgets.Logic name.GetText = () => orderManager.LobbyInfo.GlobalSettings.ServerName; UpdateCurrentMap(); - Players = Ui.LoadWidget("LOBBY_PLAYER_BIN", lobby.Get("PLAYER_BIN_ROOT"), new WidgetArgs()); - Players.IsVisible = () => panel == PanelType.Players; + players = Ui.LoadWidget("LOBBY_PLAYER_BIN", lobby.Get("PLAYER_BIN_ROOT"), new WidgetArgs()); + players.IsVisible = () => panel == PanelType.Players; - EditablePlayerTemplate = Players.Get("TEMPLATE_EDITABLE_PLAYER"); - NonEditablePlayerTemplate = Players.Get("TEMPLATE_NONEDITABLE_PLAYER"); - EmptySlotTemplate = Players.Get("TEMPLATE_EMPTY"); - EditableSpectatorTemplate = Players.Get("TEMPLATE_EDITABLE_SPECTATOR"); - NonEditableSpectatorTemplate = Players.Get("TEMPLATE_NONEDITABLE_SPECTATOR"); - NewSpectatorTemplate = Players.Get("TEMPLATE_NEW_SPECTATOR"); + var playerBinHeaders = lobby.GetOrNull("LABEL_CONTAINER"); + if (playerBinHeaders != null) + playerBinHeaders.IsVisible = () => panel == PanelType.Players; + + editablePlayerTemplate = players.Get("TEMPLATE_EDITABLE_PLAYER"); + nonEditablePlayerTemplate = players.Get("TEMPLATE_NONEDITABLE_PLAYER"); + emptySlotTemplate = players.Get("TEMPLATE_EMPTY"); + editableSpectatorTemplate = players.Get("TEMPLATE_EDITABLE_SPECTATOR"); + nonEditableSpectatorTemplate = players.Get("TEMPLATE_NONEDITABLE_SPECTATOR"); + newSpectatorTemplate = players.Get("TEMPLATE_NEW_SPECTATOR"); colorPreview = lobby.Get("COLOR_MANAGER"); 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() + countryNames = Rules.Info["world"].Traits.WithInterface() .Where(c => c.Selectable) .ToDictionary(a => a.Race, a => a.Name); - CountryNames.Add("random", "Any"); + countryNames.Add("random", "Any"); var gameStarting = false; Func configurationDisabled = () => !Game.IsHost || gameStarting || panel == PanelType.Kick || @@ -159,17 +154,17 @@ namespace OpenRA.Mods.RA.Widgets.Logic mapButton.IsDisabled = configurationDisabled; mapButton.OnClick = () => { - var onSelect = new Action(m => + var onSelect = new Action(uid => { - orderManager.IssueOrder(Order.Command("map " + m.Uid)); - Game.Settings.Server.Map = m.Uid; + orderManager.IssueOrder(Order.Command("map " + uid)); + Game.Settings.Server.Map = uid; Game.Settings.Save(); }); Ui.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs() { - { "initialMap", Map.Uid }, - { "onExit", () => {} }, + { "initialMap", preview.Uid }, + { "onExit", () => { } }, { "onSelect", onSelect } }); }; @@ -181,7 +176,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic slotsButton.IsDisabled = () => configurationDisabled() || panel != PanelType.Players || !orderManager.LobbyInfo.Slots.Values.Any(s => s.AllowBots || !s.LockTeam); - var aiModes = Rules.Info["player"].Traits.WithInterface().Select(t => t.Name); + var botNames = Rules.Info["player"].Traits.WithInterface().Select(t => t.Name); slotsButton.OnMouseDown = _ => { var options = new Dictionary>(); @@ -189,21 +184,24 @@ namespace OpenRA.Mods.RA.Widgets.Logic var botController = orderManager.LobbyInfo.Clients.FirstOrDefault(c => c.IsAdmin); if (orderManager.LobbyInfo.Slots.Values.Any(s => s.AllowBots)) { - var botOptions = new List(){ new DropDownOption() + var botOptions = new List() { - Title = "Add", - IsSelected = () => false, - OnClick = () => + new DropDownOption() { - foreach (var slot in orderManager.LobbyInfo.Slots) + Title = "Add", + IsSelected = () => false, + OnClick = () => { - var bot = aiModes.Random(Game.CosmeticRandom); - var c = orderManager.LobbyInfo.ClientInSlot(slot.Key); - if (slot.Value.AllowBots == true && (c == null || c.Bot != null)) - orderManager.IssueOrder(Order.Command("slot_bot {0} {1} {2}".F(slot.Key, botController.Index, bot))); + foreach (var slot in orderManager.LobbyInfo.Slots) + { + var bot = botNames.Random(Game.CosmeticRandom); + var c = orderManager.LobbyInfo.ClientInSlot(slot.Key); + if (slot.Value.AllowBots == true && (c == null || c.Bot != null)) + orderManager.IssueOrder(Order.Command("slot_bot {0} {1} {2}".F(slot.Key, botController.Index, bot))); + } } } - }}; + }; if (orderManager.LobbyInfo.Clients.Any(c => c.Bot != null)) { @@ -217,7 +215,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic { var c = orderManager.LobbyInfo.ClientInSlot(slot.Key); if (c != null && c.Bot != null) - orderManager.IssueOrder(Order.Command("slot_open "+slot.Value.PlayerReference)); + orderManager.IssueOrder(Order.Command("slot_open " + slot.Value.PlayerReference)); } } }); @@ -299,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))); } @@ -308,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))); } @@ -317,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))); } @@ -326,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))); } @@ -334,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, @@ -360,19 +358,19 @@ namespace OpenRA.Mods.RA.Widgets.Logic var startingUnits = optionsBin.GetOrNull("STARTINGUNITS_DROPDOWNBUTTON"); if (startingUnits != null) { - var classNames = new Dictionary() + var classNames = new Dictionary() { - {"none", "MCV Only"}, - {"light", "Light Support"}, - {"heavy", "Heavy Support"}, + { "none", "MCV Only" }, + { "light", "Light Support" }, + { "heavy", "Heavy Support" }, }; Func className = c => classNames.ContainsKey(c) ? classNames[c] : c; 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 @@ -398,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 @@ -424,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))); } @@ -433,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))); } @@ -461,7 +459,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic chatTextField.OnTabKey = () => { teamChat ^= true; - chatLabel.Text = (teamChat) ? "Team:" : "Chat:"; + chatLabel.Text = teamChat ? "Team:" : "Chat:"; return true; }; @@ -471,8 +469,8 @@ namespace OpenRA.Mods.RA.Widgets.Logic var musicButton = lobby.GetOrNull("MUSIC_BUTTON"); if (musicButton != null) - musicButton.OnClick = () => Ui.OpenWindow("MUSIC_PANEL", new WidgetArgs - { { "onExit", () => {} } }); + musicButton.OnClick = () => Ui.OpenWindow("MUSIC_PANEL", + new WidgetArgs { { "onExit", () => { } } }); // Add a bot on the first lobbyinfo update if (this.skirmishMode) @@ -523,23 +521,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))); } @@ -554,14 +554,14 @@ namespace OpenRA.Mods.RA.Widgets.Logic Widget template = null; // get template for possible reuse - if (idx < Players.Children.Count) - template = Players.Children[idx]; + if (idx < players.Children.Count) + template = players.Children[idx]; - // Empty slot if (client == null) { - if (template == null || template.Id != EmptySlotTemplate.Id) - template = EmptySlotTemplate.Clone(); + // Empty slot + if (template == null || template.Id != emptySlotTemplate.Id) + template = emptySlotTemplate.Clone(); if (Game.IsHost) LobbyUtils.SetupEditableSlotWidget(template, slot, client, orderManager); @@ -573,13 +573,12 @@ namespace OpenRA.Mods.RA.Widgets.Logic join.IsDisabled = () => orderManager.LocalClient.IsReady; join.OnClick = () => orderManager.IssueOrder(Order.Command("slot " + key)); } - - // Editable player in slot else if ((client.Index == orderManager.LocalClient.Index) || (client.Bot != null && Game.IsHost)) { - if (template == null || template.Id != EditablePlayerTemplate.Id) - template = EditablePlayerTemplate.Clone(); + // Editable player in slot + if (template == null || template.Id != editablePlayerTemplate.Id) + template = editablePlayerTemplate.Clone(); LobbyUtils.SetupClientWidget(template, slot, client, orderManager, client.Bot == null); @@ -589,31 +588,32 @@ namespace OpenRA.Mods.RA.Widgets.Logic LobbyUtils.SetupEditableNameWidget(template, slot, client, orderManager); LobbyUtils.SetupEditableColorWidget(template, slot, client, orderManager, colorPreview); - LobbyUtils.SetupEditableFactionWidget(template, slot, client, orderManager, CountryNames); - LobbyUtils.SetupEditableTeamWidget(template, slot, client, orderManager, Map.GetSpawnPoints().Length); + LobbyUtils.SetupEditableFactionWidget(template, slot, client, orderManager, countryNames); + LobbyUtils.SetupEditableTeamWidget(template, slot, client, orderManager, preview.SpawnPoints.Count); LobbyUtils.SetupEditableReadyWidget(template, slot, client, orderManager); } else - { // Non-editable player in slot - if (template == null || template.Id != NonEditablePlayerTemplate.Id) - template = NonEditablePlayerTemplate.Clone(); + { + // Non-editable player in slot + if (template == null || template.Id != nonEditablePlayerTemplate.Id) + template = nonEditablePlayerTemplate.Clone(); LobbyUtils.SetupClientWidget(template, slot, client, orderManager, client.Bot == null); LobbyUtils.SetupNameWidget(template, slot, client); LobbyUtils.SetupKickWidget(template, slot, client, orderManager, lobby, () => panel = PanelType.Kick, () => panel = PanelType.Players); LobbyUtils.SetupColorWidget(template, slot, client); - LobbyUtils.SetupFactionWidget(template, slot, client, CountryNames); + LobbyUtils.SetupFactionWidget(template, slot, client, countryNames); LobbyUtils.SetupTeamWidget(template, slot, client); LobbyUtils.SetupReadyWidget(template, slot, client); } template.IsVisible = () => true; - if (idx >= Players.Children.Count) - Players.AddChild(template); - else if (Players.Children[idx].Id != template.Id) - Players.ReplaceChild(Players.Children[idx], template); + if (idx >= players.Children.Count) + players.AddChild(template); + else if (players.Children[idx].Id != template.Id) + players.ReplaceChild(players.Children[idx], template); idx++; } @@ -625,22 +625,22 @@ namespace OpenRA.Mods.RA.Widgets.Logic var c = client; // get template for possible reuse - if (idx < Players.Children.Count) - template = Players.Children[idx]; + if (idx < players.Children.Count) + template = players.Children[idx]; // Editable spectator if (c.Index == orderManager.LocalClient.Index) { - if (template == null || template.Id != EditableSpectatorTemplate.Id) - template = EditableSpectatorTemplate.Clone(); + if (template == null || template.Id != editableSpectatorTemplate.Id) + template = editableSpectatorTemplate.Clone(); LobbyUtils.SetupEditableNameWidget(template, null, c, orderManager); } - // Non-editable spectator else { - if (template == null || template.Id != NonEditableSpectatorTemplate.Id) - template = NonEditableSpectatorTemplate.Clone(); + // Non-editable spectator + if (template == null || template.Id != nonEditableSpectatorTemplate.Id) + template = nonEditableSpectatorTemplate.Clone(); LobbyUtils.SetupNameWidget(template, null, client); LobbyUtils.SetupKickWidget(template, null, client, orderManager, lobby, @@ -650,10 +650,10 @@ namespace OpenRA.Mods.RA.Widgets.Logic LobbyUtils.SetupClientWidget(template, null, c, orderManager, true); template.IsVisible = () => true; - if (idx >= Players.Children.Count) - Players.AddChild(template); - else if (Players.Children[idx].Id != template.Id) - Players.ReplaceChild(Players.Children[idx], template); + if (idx >= players.Children.Count) + players.AddChild(template); + else if (players.Children[idx].Id != template.Id) + players.ReplaceChild(players.Children[idx], template); idx++; } @@ -662,10 +662,10 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (orderManager.LocalClient.Slot != null) { Widget spec = null; - if (idx < Players.Children.Count) - spec = Players.Children[idx]; - if (spec == null || spec.Id != NewSpectatorTemplate.Id) - spec = NewSpectatorTemplate.Clone(); + if (idx < players.Children.Count) + spec = players.Children[idx]; + if (spec == null || spec.Id != newSpectatorTemplate.Id) + spec = newSpectatorTemplate.Clone(); LobbyUtils.SetupKickSpectatorsWidget(spec, orderManager, lobby, () => panel = PanelType.Kick, () => panel = PanelType.Players, this.skirmishMode); @@ -677,17 +677,23 @@ namespace OpenRA.Mods.RA.Widgets.Logic || orderManager.LocalClient.IsAdmin; spec.IsVisible = () => true; - - if (idx >= Players.Children.Count) - Players.AddChild(spec); - else if (Players.Children[idx].Id != spec.Id) - Players.ReplaceChild(Players.Children[idx], spec); + + if (idx >= players.Children.Count) + players.AddChild(spec); + else if (players.Children[idx].Id != spec.Id) + players.ReplaceChild(players.Children[idx], spec); idx++; } - while (Players.Children.Count > idx) - Players.RemoveChild(Players.Children[idx]); + while (players.Children.Count > idx) + players.RemoveChild(players.Children[idx]); + } + + void OnGameStart() + { + CloseWindow(); + onStart(); } class DropDownOption diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyUtils.cs index 76f3e5fa13..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..9e4b829bfc 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/MapChooserLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/MapChooserLogic.cs @@ -18,21 +18,21 @@ namespace OpenRA.Mods.RA.Widgets.Logic { public class MapChooserLogic { - Map map; + string selectedUid; // May be a subset of available maps if a mode filter is active - Dictionary visibleMaps; + List visibleMaps; ScrollPanelWidget scrollpanel; ScrollItemWidget itemTemplate; string gameMode; [ObjectCreator.UseCtor] - internal MapChooserLogic(Widget widget, string initialMap, Action onExit, Action onSelect) + internal MapChooserLogic(Widget widget, string initialMap, Action onExit, Action onSelect) { - map = Game.modData.AvailableMaps[WidgetUtils.ChooseInitialMap(initialMap)]; + selectedUid = WidgetUtils.ChooseInitialMap(initialMap); - widget.Get("BUTTON_OK").OnClick = () => { Ui.CloseWindow(); onSelect(map); }; + widget.Get("BUTTON_OK").OnClick = () => { Ui.CloseWindow(); onSelect(selectedUid); }; widget.Get("BUTTON_CANCEL").OnClick = () => { Ui.CloseWindow(); onExit(); }; scrollpanel = widget.Get("MAP_LIST"); @@ -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 @@ -75,9 +75,9 @@ namespace OpenRA.Mods.RA.Widgets.Logic { randomMapButton.OnClick = () => { - var kv = visibleMaps.Random(Game.CosmeticRandom); - map = kv.Value; - scrollpanel.ScrollToItem(kv.Key); + var uid = visibleMaps.Random(Game.CosmeticRandom); + selectedUid = uid; + scrollpanel.ScrollToItem(uid); }; randomMapButton.IsDisabled = () => visibleMaps == null || visibleMaps.Count == 0; } @@ -85,27 +85,27 @@ namespace OpenRA.Mods.RA.Widgets.Logic EnumerateMaps(onSelect); } - void EnumerateMaps(Action onSelect) + void EnumerateMaps(Action onSelect) { - var maps = Game.modData.AvailableMaps - .Where(kv => kv.Value.Selectable) - .Where(kv => kv.Value.Type == gameMode || gameMode == null) - .OrderBy(kv => kv.Value.PlayerCount) - .ThenBy(kv => kv.Value.Title); + var maps = Game.modData.MapCache + .Where(m => m.Status == MapStatus.Available && m.Map.Selectable) + .Where(m => m.Type == gameMode || gameMode == null) + .OrderBy(m => m.PlayerCount) + .ThenBy(m => m.Title); scrollpanel.RemoveChildren(); - foreach (var kv in maps) + foreach (var loop in maps) { - var m = kv.Value; - var item = ScrollItemWidget.Setup(kv.Key, itemTemplate, () => m == map, () => map = m, () => { Ui.CloseWindow(); onSelect(m); }); + var preview = loop; + var item = ScrollItemWidget.Setup(preview.Uid, itemTemplate, () => selectedUid == preview.Uid, () => selectedUid = preview.Uid, () => { Ui.CloseWindow(); onSelect(preview.Uid); }); var titleLabel = item.Get("TITLE"); - titleLabel.GetText = () => m.Title; + titleLabel.GetText = () => preview.Title; 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"); @@ -114,17 +114,17 @@ namespace OpenRA.Mods.RA.Widgets.Logic var detailsWidget = item.GetOrNull("DETAILS"); if (detailsWidget != null) - detailsWidget.GetText = () => "{0} ({1} players)".F(m.Type, m.PlayerCount); + detailsWidget.GetText = () => "{0} ({1} players)".F(preview.Type, preview.PlayerCount); var authorWidget = item.GetOrNull("AUTHOR"); if (authorWidget != null) - authorWidget.GetText = () => "Created by {0}".F(m.Author); + authorWidget.GetText = () => "Created by {0}".F(preview.Author); var sizeWidget = item.GetOrNull("SIZE"); if (sizeWidget != null) { - var size = m.Bounds.Width + "x" + m.Bounds.Height; - var numberPlayableCells = m.Bounds.Width * m.Bounds.Height; + var size = preview.Bounds.Width + "x" + preview.Bounds.Height; + var numberPlayableCells = preview.Bounds.Width * preview.Bounds.Height; if (numberPlayableCells >= 120 * 120) size += " (Huge)"; else if (numberPlayableCells >= 90 * 90) size += " (Large)"; else if (numberPlayableCells >= 60 * 60) size += " (Medium)"; @@ -135,9 +135,9 @@ namespace OpenRA.Mods.RA.Widgets.Logic scrollpanel.AddChild(item); } - visibleMaps = maps.ToDictionary(kv => kv.Key, kv => kv.Value); - if (visibleMaps.ContainsValue(map)) - scrollpanel.ScrollToItem(visibleMaps.First(m => m.Value == map).Key); + visibleMaps = maps.Select(m => m.Uid).ToList(); + if (visibleMaps.Contains(selectedUid)) + scrollpanel.ScrollToItem(selectedUid); } } } diff --git a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs index e55fe67f53..38d1c009bb 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/ReplayBrowserLogic.cs @@ -19,6 +19,11 @@ namespace OpenRA.Mods.RA.Widgets.Logic public class ReplayBrowserLogic { Widget panel; + MapPreview selectedMap = MapCache.UnknownMap; + string selectedFilename; + string selectedDuration; + string selectedPlayers; + bool selectedValid; [ObjectCreator.UseCtor] public ReplayBrowserLogic(Widget widget, Action onExit, Action onStart) @@ -44,15 +49,16 @@ namespace OpenRA.Mods.RA.Widgets.Logic } var watch = panel.Get("WATCH_BUTTON"); - watch.IsDisabled = () => currentReplay == null || currentMap == null || currentReplay.Duration == 0; + watch.IsDisabled = () => !selectedValid || selectedMap.Status != MapStatus.Available; watch.OnClick = () => { WatchReplay(); onStart(); }; - panel.Get("REPLAY_INFO").IsVisible = () => currentReplay != null; + panel.Get("REPLAY_INFO").IsVisible = () => selectedFilename != null;; + panel.Get("DURATION").GetText = () => selectedDuration; + panel.Get("MAP_PREVIEW").Preview = () => selectedMap; + panel.Get("MAP_TITLE").GetText = () => selectedMap.Title; + panel.Get("PLAYERS").GetText = () => selectedPlayers; } - Replay currentReplay; - Map currentMap; - void SelectReplay(string filename) { if (filename == null) @@ -60,32 +66,31 @@ namespace OpenRA.Mods.RA.Widgets.Logic try { - currentReplay = new Replay(filename); - currentMap = currentReplay.Map(); - - panel.Get("DURATION").GetText = - () => WidgetUtils.FormatTime(currentReplay.Duration); - panel.Get("MAP_PREVIEW").Map = () => currentMap; - panel.Get("MAP_TITLE").GetText = - () => currentMap != null ? currentMap.Title : "(Unknown Map)"; - - var players = currentReplay.LobbyInfo.Slots - .Count(s => currentReplay.LobbyInfo.ClientInSlot(s.Key) != null); - panel.Get("PLAYERS").GetText = () => players.ToString(); + using (var conn = new ReplayConnection(filename)) + { + selectedFilename = filename; + selectedMap = Game.modData.MapCache[conn.LobbyInfo.GlobalSettings.Map]; + selectedDuration = WidgetUtils.FormatTime(conn.TickCount * Game.NetTickScale); + selectedPlayers = conn.LobbyInfo.Slots + .Count(s => conn.LobbyInfo.ClientInSlot(s.Key) != null) + .ToString(); + selectedValid = conn.TickCount > 0; + } } catch (Exception e) { Log.Write("debug", "Exception while parsing replay: {0}", e); - currentReplay = null; - currentMap = null; + selectedFilename = null; + selectedValid = false; + selectedMap = MapCache.UnknownMap; } } void WatchReplay() { - if (currentReplay != null) + if (selectedFilename != null) { - Game.JoinReplay(currentReplay.Filename); + Game.JoinReplay(selectedFilename); Ui.CloseWindow(); } } @@ -93,7 +98,7 @@ namespace OpenRA.Mods.RA.Widgets.Logic void AddReplay(ScrollPanelWidget list, string filename, ScrollItemWidget template) { var item = ScrollItemWidget.Setup(template, - () => currentReplay != null && currentReplay.Filename == filename, + () => selectedFilename == filename, () => SelectReplay(filename), () => WatchReplay()); var f = Path.GetFileName(filename); diff --git a/OpenRA.Mods.RA/Widgets/Logic/ServerBrowserLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/ServerBrowserLogic.cs index 7787ea071d..7e09dc7ccd 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/ServerBrowserLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/ServerBrowserLogic.cs @@ -12,6 +12,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Drawing; +using System.Net; +using System.Text; using OpenRA.FileFormats; using OpenRA.Network; using OpenRA.Server; @@ -29,6 +31,8 @@ namespace OpenRA.Mods.RA.Widgets.Logic enum SearchStatus { Fetching, Failed, NoGames, Hidden } SearchStatus searchStatus = SearchStatus.Fetching; + Download currentQuery; + Widget panel, serverList; bool showWaiting = true; bool showEmpty = true; @@ -39,7 +43,6 @@ namespace OpenRA.Mods.RA.Widgets.Logic { switch (searchStatus) { - case SearchStatus.Fetching: return "Fetching game list..."; case SearchStatus.Failed: return "Failed to contact master server."; case SearchStatus.NoGames: return "No games found."; default: return ""; @@ -49,16 +52,18 @@ namespace OpenRA.Mods.RA.Widgets.Logic [ObjectCreator.UseCtor] public ServerBrowserLogic(Widget widget, Action onStart, Action onExit) { - var panel = widget; + panel = widget; this.onStart = onStart; this.onExit = onExit; - var sl = panel.Get("SERVER_LIST"); + serverList = panel.Get("SERVER_LIST"); + serverTemplate = serverList.Get("SERVER_TEMPLATE"); // Menu buttons var refreshButton = panel.Get("REFRESH_BUTTON"); refreshButton.IsDisabled = () => searchStatus == SearchStatus.Fetching; - refreshButton.OnClick = () => ServerList.Query(games => RefreshServerList(panel, games)); + refreshButton.GetText = () => searchStatus == SearchStatus.Fetching ? "Refreshing..." : "Refresh"; + refreshButton.OnClick = RefreshServerList; panel.Get("DIRECTCONNECT_BUTTON").OnClick = OpenDirectConnectPanel; panel.Get("CREATE_BUTTON").OnClick = OpenCreateServerPanel; @@ -69,9 +74,6 @@ namespace OpenRA.Mods.RA.Widgets.Logic panel.Get("BACK_BUTTON").OnClick = () => { Ui.CloseWindow(); onExit(); }; - // Server list - serverTemplate = sl.Get("SERVER_TEMPLATE"); - // Display the progress label over the server list // The text is only visible when the list is empty var progressText = panel.Get("PROGRESS_LABEL"); @@ -82,33 +84,163 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (showWaitingCheckbox != null) { showWaitingCheckbox.IsChecked = () => showWaiting; - showWaitingCheckbox.OnClick = () => { showWaiting ^= true; ServerList.Query(games => RefreshServerList(panel, games)); }; + showWaitingCheckbox.OnClick = () => { showWaiting ^= true; RefreshServerList(); }; } var showEmptyCheckbox = panel.GetOrNull("EMPTY"); if (showEmptyCheckbox != null) { showEmptyCheckbox.IsChecked = () => showEmpty; - showEmptyCheckbox.OnClick = () => { showEmpty ^= true; ServerList.Query(games => RefreshServerList(panel, games)); }; + showEmptyCheckbox.OnClick = () => { showEmpty ^= true; RefreshServerList(); }; } var showAlreadyStartedCheckbox = panel.GetOrNull("ALREADY_STARTED"); if (showAlreadyStartedCheckbox != null) { showAlreadyStartedCheckbox.IsChecked = () => showStarted; - showAlreadyStartedCheckbox.OnClick = () => { showStarted ^= true; ServerList.Query(games => RefreshServerList(panel, games)); }; + showAlreadyStartedCheckbox.OnClick = () => { showStarted ^= true; RefreshServerList(); }; } var showIncompatibleCheckbox = panel.GetOrNull("INCOMPATIBLE_VERSION"); if (showIncompatibleCheckbox != null) { showIncompatibleCheckbox.IsChecked = () => showIncompatible; - showIncompatibleCheckbox.OnClick = () => { showIncompatible ^= true; ServerList.Query(games => RefreshServerList(panel, games)); }; + showIncompatibleCheckbox.OnClick = () => { showIncompatible ^= true; RefreshServerList(); }; } // Game.LoadWidget(null, "SERVERBROWSER_IRC", panel.Get("IRC_ROOT"), new WidgetArgs()); + RefreshServerList(); + } - ServerList.Query(games => RefreshServerList(panel, games)); + void RefreshServerList() + { + // Query in progress + if (currentQuery != null) + return; + + searchStatus = SearchStatus.Fetching; + + Action onComplete = (i, cancelled) => + { + currentQuery = null; + + if (i.Error != null || cancelled) + { + RefreshServerListInner(null); + return; + } + + var data = Encoding.UTF8.GetString(i.Result); + var yaml = MiniYaml.FromString(data); + + var games = yaml.Select(a => FieldLoader.Load(a.Value)) + .Where(gs => gs.Address != null); + + RefreshServerListInner(games); + Game.RunAfterTick(() => RefreshServerListInner(games)); + }; + + currentQuery = new Download(Game.Settings.Server.MasterServer + "list.php", _ => {}, onComplete); + } + + public void RefreshServerListInner(IEnumerable games) + { + List rows = new List(); + + Game.RunAfterTick(() => + { + serverList.RemoveChildren(); + currentServer = null; + + if (games == null) + { + searchStatus = SearchStatus.Failed; + return; + } + + if (!games.Any()) + { + searchStatus = SearchStatus.NoGames; + return; + } + + currentServer = games.FirstOrDefault(); + searchStatus = SearchStatus.Hidden; + + foreach (var row in rows) + serverList.AddChild(row); + }); + + foreach (var loop in games.OrderByDescending(g => g.CanJoin()).ThenByDescending(g => g.Players)) + { + var game = loop; + if (game == null) + continue; + + var canJoin = game.CanJoin(); + + var item = ScrollItemWidget.Setup(serverTemplate, () => currentServer == game, () => currentServer = game, () => Join(game)); + + var map = Game.modData.MapCache[game.Map]; + var preview = item.GetOrNull("MAP_PREVIEW"); + if (preview != null) + preview.Preview = () => map; + + var title = item.GetOrNull("TITLE"); + if (title != null) + { + title.GetText = () => game.Name; + title.GetColor = () => canJoin ? title.TextColor : Color.Gray; + } + + var maptitle = item.GetOrNull("MAP"); + if (title != null) + { + maptitle.GetText = () => map.Title; + maptitle.GetColor = () => canJoin ? maptitle.TextColor : Color.Gray; + } + + var players = item.GetOrNull("PLAYERS"); + if (players != null) + { + players.GetText = () => "{0} / {1}".F(game.Players, map.PlayerCount); + players.GetColor = () => canJoin ? players.TextColor : Color.Gray; + } + + var state = item.GetOrNull("STATE"); + if (state != null) + { + state.GetText = () => GetStateLabel(game); + state.GetColor = () => canJoin ? state.TextColor : Color.Gray; + } + + var ip = item.GetOrNull("IP"); + if (ip != null) + { + ip.GetText = () => game.Address; + ip.GetColor = () => canJoin ? ip.TextColor : Color.Gray; + } + + var version = item.GetOrNull("VERSION"); + if (version != null) + { + version.GetText = () => GenerateModLabel(game); + version.IsVisible = () => !game.CompatibleVersion(); + version.GetColor = () => canJoin ? version.TextColor : Color.Gray; + } + + var location = item.GetOrNull("LOCATION"); + if (location != null) + { + var cachedServerLocation = LobbyUtils.LookupCountry(game.Address.Split(':')[0]); + location.GetText = () => cachedServerLocation; + location.IsVisible = () => game.CompatibleVersion(); + location.GetColor = () => canJoin ? location.TextColor : Color.Gray; + } + + if (!Filtered(game)) + rows.Add(item); + } } void OpenLobby() @@ -155,10 +287,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 +306,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 +366,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..f693929bb6 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)(uid => preview = Game.modData.MapCache[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);