From c3ba27ef6c3d657a7ce0a3b5f46c5208ce481128 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Thu, 13 Mar 2014 12:43:42 +1300 Subject: [PATCH] Reimplement ingame map downloading. --- OpenRA.Game/GameRules/Settings.cs | 2 +- OpenRA.Game/MapCache.cs | 40 ++++- OpenRA.Game/MapPreview.cs | 136 ++++++++++++++- OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs | 2 + .../Widgets/Logic/LobbyMapPreviewLogic.cs | 130 ++++++++++++-- mods/cnc/chrome/lobby-mappreview.yaml | 159 ++++++++++++++--- mods/ra/chrome/lobby-mappreview.yaml | 161 +++++++++++++++--- 7 files changed, 563 insertions(+), 67 deletions(-) diff --git a/OpenRA.Game/GameRules/Settings.cs b/OpenRA.Game/GameRules/Settings.cs index 4295333806..d320d8eb9b 100644 --- a/OpenRA.Game/GameRules/Settings.cs +++ b/OpenRA.Game/GameRules/Settings.cs @@ -136,7 +136,7 @@ namespace OpenRA.GameRules public bool TeamHealthColors = false; public bool AllowDownloading = true; - public string[] MapRepositories = { "http://resource.openra.net/map/", "http://resource.ihptru.net:8080/map/" }; + public string MapRepository = "http://resource.openra.net/map/"; } public class KeySettings diff --git a/OpenRA.Game/MapCache.cs b/OpenRA.Game/MapCache.cs index cd0beec736..cf030d4103 100755 --- a/OpenRA.Game/MapCache.cs +++ b/OpenRA.Game/MapCache.cs @@ -14,10 +14,11 @@ using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; +using System.Net; +using System.Text; using System.Threading; using OpenRA.FileFormats; using OpenRA.Graphics; -using OpenRA.Widgets; namespace OpenRA { @@ -57,6 +58,43 @@ namespace OpenRA } } + public void QueryRemoteMapDetails(IEnumerable uids) + { + var maps = uids.Distinct() + .Select(uid => previews[uid]) + .Where(p => p.Status == MapStatus.Unavailable) + .ToDictionary(p => p.Uid, p => p); + + if (!maps.Any()) + return; + + foreach (var p in maps.Values) + p.UpdateRemoteSearch(MapStatus.Searching, null); + + var url = Game.Settings.Game.MapRepository + "hash/" + string.Join(",", maps.Keys.ToArray()) + "/yaml"; + + Action onInfoComplete = (i, cancelled) => + { + if (cancelled || i.Error != null) + { + Log.Write("debug", "Remote map query failed with error: {0}", i.Error != null ? i.Error.Message : "cancelled"); + Log.Write("debug", "URL was: {0}", url); + foreach (var p in maps.Values) + p.UpdateRemoteSearch(MapStatus.Unavailable, null); + + return; + } + + var data = Encoding.UTF8.GetString(i.Result); + var yaml = MiniYaml.FromString(data); + + foreach (var kv in yaml) + maps[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value); + }; + + new Download(url, _ => { }, onInfoComplete); + } + public static IEnumerable FindMapsIn(string dir) { string[] noMaps = { }; diff --git a/OpenRA.Game/MapPreview.cs b/OpenRA.Game/MapPreview.cs index de0e3d1425..7b07dc293b 100755 --- a/OpenRA.Game/MapPreview.cs +++ b/OpenRA.Game/MapPreview.cs @@ -11,9 +11,11 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Drawing; using System.IO; using System.Linq; +using System.Net; using System.Threading; using OpenRA.FileFormats; using OpenRA.Graphics; @@ -21,10 +23,26 @@ using OpenRA.Widgets; namespace OpenRA { - public enum MapStatus { Available, Unavailable } + public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError } + + // Fields names must match the with the remote API + public class RemoteMapData + { + public readonly string title; + public readonly string author; + public readonly string map_type; + public readonly int players; + public readonly Rectangle bounds; + public readonly int[] spawnpoints = {}; + public readonly string minimap; + public readonly bool downloading; + public readonly bool requires_upgrade; + } + public class MapPreview { static readonly List NoSpawns = new List(); + MapCache cache; public readonly string Uid; public string Title { get; private set; } @@ -37,6 +55,10 @@ namespace OpenRA public Map Map { get; private set; } public MapStatus Status { get; private set; } + Download download; + public long DownloadBytes { get; private set; } + public int DownloadPercentage { get; private set; } + Sprite minimap; bool generatingMinimap; public Sprite Minimap @@ -62,7 +84,6 @@ namespace OpenRA } } - MapCache cache; public MapPreview(string uid, MapCache cache) { this.cache = cache; @@ -89,5 +110,116 @@ namespace OpenRA CustomPreview = m.CustomPreview; Status = MapStatus.Available; } + + public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml) + { + // Update on the main thread to ensure consistency + Game.RunAfterTick(() => + { + if (status == MapStatus.DownloadAvailable) + { + try + { + var r = FieldLoader.Load(yaml); + + // Map is not useable by the current version + if (!r.downloading || r.requires_upgrade) + { + Status = MapStatus.Unavailable; + return; + } + + Title = r.title; + Type = r.map_type; + Author = r.author; + PlayerCount = r.players; + Bounds = r.bounds; + + var spawns = new List(); + for (var j = 0; j < r.spawnpoints.Length; j += 2) + spawns.Add(new CPos(r.spawnpoints[j], r.spawnpoints[j+1])); + SpawnPoints = spawns; + + CustomPreview = new Bitmap(new MemoryStream(Convert.FromBase64String(r.minimap))); + } + catch (Exception) {} + + if (CustomPreview != null) + cache.CacheMinimap(this); + } + Status = status; + + }); + } + + public void Install() + { + if (Status != MapStatus.DownloadAvailable || !Game.Settings.Game.AllowDownloading) + return; + + Status = MapStatus.Downloading; + var baseMapPath = new[] { Platform.SupportDir, "maps", Game.modData.Manifest.Mod.Id }.Aggregate(Path.Combine); + + // Create the map directory if it doesn't exist + if (!Directory.Exists(baseMapPath)) + Directory.CreateDirectory(baseMapPath); + + new Thread(() => + { + // Request the filename from the server + // Run in a worker thread to avoid network delays + var mapUrl = Game.Settings.Game.MapRepository + Uid; + try + { + + var request = WebRequest.Create(mapUrl); + request.Method = "HEAD"; + var res = request.GetResponse(); + + // Map not found + if (res.Headers["Content-Disposition"] == null) + { + Status = MapStatus.DownloadError; + return; + } + + var mapPath = Path.Combine(baseMapPath, res.Headers["Content-Disposition"].Replace("attachment; filename = ", "")); + + Action onDownloadProgress = i => { DownloadBytes = i.BytesReceived; DownloadPercentage = i.ProgressPercentage; }; + Action onDownloadComplete = (i, cancelled) => + { + download = null; + + if (cancelled || i.Error != null) + { + Log.Write("debug", "Remote map download failed with error: {0}", i.Error != null ? i.Error.Message : "cancelled"); + Log.Write("debug", "URL was: {0}", mapUrl); + + Status = MapStatus.DownloadError; + return; + } + + Log.Write("debug", "Downloaded map to '{0}'", mapPath); + Game.RunAfterTick(() => UpdateFromMap(new Map(mapPath))); + }; + + download = new Download(mapUrl, mapPath, onDownloadProgress, onDownloadComplete); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + Status = MapStatus.DownloadError; + } + }).Start(); + } + + public void CancelInstall() + { + if (download == null) + return; + + download.Cancel(); + download = null; + } } } diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs index ad183ba8b7..6036605dca 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyLogic.cs @@ -554,6 +554,8 @@ namespace OpenRA.Mods.RA.Widgets.Logic if (!Map.Map.Options.StartingCash.HasValue && !pri.SelectableCash.Contains(orderManager.LobbyInfo.GlobalSettings.StartingCash)) orderManager.IssueOrder(Order.Command("startingcash {0}".F(pri.DefaultCash))); } + else if (Game.Settings.Game.AllowDownloading) + Game.modData.MapCache.QueryRemoteMapDetails(new [] { uid }); } void UpdatePlayerList() diff --git a/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs index c01303bdc7..a62cfa1b63 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/LobbyMapPreviewLogic.cs @@ -23,22 +23,126 @@ namespace OpenRA.Mods.RA.Widgets.Logic [ObjectCreator.UseCtor] internal LobbyMapPreviewLogic(Widget widget, OrderManager orderManager, LobbyLogic lobby) { - var mapPreview = widget.Get("MAP_PREVIEW"); - mapPreview.Preview = () => lobby.Map; - mapPreview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, mapPreview, lobby.Map, mi); - mapPreview.SpawnClients = () => LobbyUtils.GetSpawnClients(orderManager, lobby.Map); + var available = widget.GetOrNull("MAP_AVAILABLE"); + if (available != null) + { + available.IsVisible = () => lobby.Map.Status == MapStatus.Available; - var mapTitle = widget.GetOrNull("MAP_TITLE"); - if (mapTitle != null) - mapTitle.GetText = () => lobby.Map.Title; + var preview = available.Get("MAP_PREVIEW"); + preview.Preview = () => lobby.Map; + preview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, preview, lobby.Map, mi); + preview.SpawnClients = () => LobbyUtils.GetSpawnClients(orderManager, lobby.Map); - var mapType = widget.GetOrNull("MAP_TYPE"); - if (mapType != null) - mapType.GetText = () => lobby.Map.Type; + var title = available.GetOrNull("MAP_TITLE"); + if (title != null) + title.GetText = () => lobby.Map.Title; - var mapAuthor = widget.GetOrNull("MAP_AUTHOR"); - if (mapAuthor != null) - mapAuthor.GetText = () => "Created by {0}".F(lobby.Map.Author); + var type = available.GetOrNull("MAP_TYPE"); + if (type != null) + type.GetText = () => lobby.Map.Type; + + var author = available.GetOrNull("MAP_AUTHOR"); + if (author != null) + author.GetText = () => "Created by {0}".F(lobby.Map.Author); + } + + var download = widget.GetOrNull("MAP_DOWNLOADABLE"); + if (download != null) + { + download.IsVisible = () => lobby.Map.Status == MapStatus.DownloadAvailable; + + var preview = download.Get("MAP_PREVIEW"); + preview.Preview = () => lobby.Map; + preview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, preview, lobby.Map, mi); + preview.SpawnClients = () => LobbyUtils.GetSpawnClients(orderManager, lobby.Map); + + var title = download.GetOrNull("MAP_TITLE"); + if (title != null) + title.GetText = () => lobby.Map.Title; + + var type = download.GetOrNull("MAP_TYPE"); + if (type != null) + type.GetText = () => lobby.Map.Type; + + var author = download.GetOrNull("MAP_AUTHOR"); + if (author != null) + author.GetText = () => "Created by {0}".F(lobby.Map.Author); + + var install = download.GetOrNull("MAP_INSTALL"); + if (install != null) + install.OnClick = () => lobby.Map.Install(); + } + + var progress = widget.GetOrNull("MAP_PROGRESS"); + if (progress != null) + { + progress.IsVisible = () => lobby.Map.Status != MapStatus.Available && lobby.Map.Status != MapStatus.DownloadAvailable; + + var preview = progress.Get("MAP_PREVIEW"); + preview.Preview = () => lobby.Map; + preview.OnMouseDown = mi => LobbyUtils.SelectSpawnPoint(orderManager, preview, lobby.Map, mi); + preview.SpawnClients = () => LobbyUtils.GetSpawnClients(orderManager, lobby.Map); + + var title = progress.GetOrNull("MAP_TITLE"); + if (title != null) + title.GetText = () => lobby.Map.Title; + + var type = progress.GetOrNull("MAP_TYPE"); + if (type != null) + type.GetText = () => lobby.Map.Type; + + var statusSearching = progress.GetOrNull("MAP_STATUS_SEARCHING"); + if (statusSearching != null) + statusSearching.IsVisible = () => lobby.Map.Status == MapStatus.Searching; + + var statusUnavailable = progress.GetOrNull("MAP_STATUS_UNAVAILABLE"); + if (statusUnavailable != null) + statusUnavailable.IsVisible = () => lobby.Map.Status == MapStatus.Unavailable; + + var statusError = progress.GetOrNull("MAP_STATUS_ERROR"); + if (statusError != null) + statusError.IsVisible = () => lobby.Map.Status == MapStatus.DownloadError; + + var statusDownloading = progress.GetOrNull("MAP_STATUS_DOWNLOADING"); + if (statusDownloading != null) + { + statusDownloading.IsVisible = () => lobby.Map.Status == MapStatus.Downloading; + statusDownloading.GetText = () => + { + if (lobby.Map.DownloadBytes == 0) + return "Connecting..."; + + // Server does not provide the total file length + if (lobby.Map.DownloadPercentage == 0) + return "Downloading {0} kB".F(lobby.Map.DownloadBytes / 1024); + + return "Downloading {0} kB ({1}%)".F(lobby.Map.DownloadBytes / 1024, lobby.Map.DownloadPercentage); + }; + } + + var retry = progress.GetOrNull("MAP_RETRY"); + if (retry != null) + { + retry.IsVisible = () => lobby.Map.Status == MapStatus.DownloadError || lobby.Map.Status == MapStatus.Unavailable; + retry.OnClick = () => + { + if (lobby.Map.Status == MapStatus.DownloadError) + lobby.Map.Install(); + else if (lobby.Map.Status == MapStatus.Unavailable) + Game.modData.MapCache.QueryRemoteMapDetails(new [] { lobby.Map.Uid }); + }; + + retry.GetText = () => lobby.Map.Status == MapStatus.DownloadError ? "Retry Install" : "Retry Search"; + } + + var progressbar = progress.GetOrNull("MAP_PROGRESSBAR"); + if (progressbar != null) + { + progressbar.IsIndeterminate = () => lobby.Map.DownloadPercentage == 0; + progressbar.GetPercentage = () => lobby.Map.DownloadPercentage; + progressbar.IsVisible = () => !retry.IsVisible(); + } + } } } } diff --git a/mods/cnc/chrome/lobby-mappreview.yaml b/mods/cnc/chrome/lobby-mappreview.yaml index d3c1e7dae5..f0eeb944f4 100644 --- a/mods/cnc/chrome/lobby-mappreview.yaml +++ b/mods/cnc/chrome/lobby-mappreview.yaml @@ -5,32 +5,141 @@ Container@LOBBY_MAP_PREVIEW: Width:194 Height:250 Children: - Background@MAP_BG: + Container@MAP_AVAILABLE: Width:PARENT_RIGHT - Height:194 - Background:panel-gray + Height:PARENT_BOTTOM Children: - MapPreview@MAP_PREVIEW: - X:1 - Y:1 - Width:PARENT_RIGHT-2 - Height:PARENT_BOTTOM-2 - TooltipContainer:TOOLTIP_CONTAINER - Label@MAP_TITLE: - Y:197 + Background@MAP_BG: + Width:PARENT_RIGHT + Height:194 + Background:panel-gray + Children: + MapPreview@MAP_PREVIEW: + X:1 + Y:1 + Width:PARENT_RIGHT-2 + Height:PARENT_BOTTOM-2 + TooltipContainer:TOOLTIP_CONTAINER + Label@MAP_TITLE: + Y:197 + Width:PARENT_RIGHT + Height:25 + Font:Bold + Align:Center + Label@MAP_TYPE: + Y:212 + Width:PARENT_RIGHT + Height:25 + Font:TinyBold + Align:Center + Label@MAP_AUTHOR: + Y:225 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + Container@MAP_DOWNLOADABLE: Width:PARENT_RIGHT - Height:25 - Font:Bold - Align:Center - Label@MAP_TYPE: - Y:212 + Height:PARENT_BOTTOM + Children: + Background@MAP_BG: + Width:PARENT_RIGHT + Height:164 + Background:panel-gray + Children: + MapPreview@MAP_PREVIEW: + X:1 + Y:1 + Width:PARENT_RIGHT-2 + Height:PARENT_BOTTOM-2 + TooltipContainer:TOOLTIP_CONTAINER + Label@MAP_TITLE: + Y:167 + Width:PARENT_RIGHT + Height:25 + Font:Bold + Align:Center + Label@MAP_TYPE: + Y:184 + Width:PARENT_RIGHT + Height:25 + Font:TinyBold + Align:Center + Label@MAP_AUTHOR: + Y:197 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + Button@MAP_INSTALL: + Y:224 + Width:PARENT_RIGHT + Height:25 + Text:Install Map + Container@MAP_PROGRESS: Width:PARENT_RIGHT - Height:25 - Font:TinyBold - Align:Center - Label@MAP_AUTHOR: - Y:225 - Width:PARENT_RIGHT - Height:25 - Font:Tiny - Align:Center + Height:PARENT_BOTTOM + Children: + Background@MAP_BG: + Width:PARENT_RIGHT + Height:164 + Background:panel-gray + Children: + MapPreview@MAP_PREVIEW: + X:1 + Y:1 + Width:PARENT_RIGHT-2 + Height:PARENT_BOTTOM-2 + TooltipContainer:TOOLTIP_CONTAINER + Label@MAP_TITLE: + Y:167 + Width:PARENT_RIGHT + Height:25 + Font:Bold + Align:Center + Label@MAP_STATUS_SEARCHING: + Y:197 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + Text: Searching OpenRA Resource Center... + Container@MAP_STATUS_UNAVAILABLE: + Width:PARENT_RIGHT + Children: + Label@a: + Y:184 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + Text:This map was not found on the + Label@b: + Y:197 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + Text:OpenRA Resource Center + Label@MAP_STATUS_ERROR: + Y:197 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + Text:An error occurred during installation + Label@MAP_STATUS_DOWNLOADING: + Y:197 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + ProgressBar@MAP_PROGRESSBAR: + Y:224 + Width:PARENT_RIGHT + Height:25 + Indeterminate:True + Button@MAP_RETRY: + Y:224 + Width:PARENT_RIGHT + Height:25 \ No newline at end of file diff --git a/mods/ra/chrome/lobby-mappreview.yaml b/mods/ra/chrome/lobby-mappreview.yaml index 3dfb153feb..375465d466 100644 --- a/mods/ra/chrome/lobby-mappreview.yaml +++ b/mods/ra/chrome/lobby-mappreview.yaml @@ -5,32 +5,143 @@ Container@LOBBY_MAP_PREVIEW: Width:214 Height:250 Children: - Background@MAP_BG: + Container@MAP_AVAILABLE: Width:PARENT_RIGHT - Height:214 - Background:dialog3 + Height:PARENT_BOTTOM Children: - MapPreview@MAP_PREVIEW: - X:2 - Y:2 - Width:210 - Height:210 - TooltipContainer:TOOLTIP_CONTAINER - Label@MAP_TITLE: - Y:215 + Background@MAP_BG: + Width:PARENT_RIGHT + Height:214 + Background:dialog3 + Children: + MapPreview@MAP_PREVIEW: + X:1 + Y:1 + Width:PARENT_RIGHT-2 + Height:PARENT_BOTTOM-2 + TooltipContainer:TOOLTIP_CONTAINER + Label@MAP_TITLE: + Y:215 + Width:PARENT_RIGHT + Height:25 + Font:Bold + Align:Center + Label@MAP_TYPE: + Y:232 + Width:PARENT_RIGHT + Height:25 + Font:TinyBold + Align:Center + Label@MAP_AUTHOR: + Y:245 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + Container@MAP_DOWNLOADABLE: Width:PARENT_RIGHT - Height:25 - Font:Bold - Align:Center - Label@MAP_TYPE: - Y:230 + Height:PARENT_BOTTOM + Children: + Background@MAP_BG: + Width:PARENT_RIGHT + Height:182 + Background:dialog3 + Children: + MapPreview@MAP_PREVIEW: + X:1 + Y:1 + Width:PARENT_RIGHT-2 + Height:PARENT_BOTTOM-2 + TooltipContainer:TOOLTIP_CONTAINER + Label@MAP_TITLE: + Y:185 + Width:PARENT_RIGHT + Height:25 + Font:Bold + Align:Center + Label@MAP_TYPE: + Y:202 + Width:PARENT_RIGHT + Height:25 + Font:TinyBold + Align:Center + Label@MAP_AUTHOR: + Y:215 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + Button@MAP_INSTALL: + Y:242 + Width:PARENT_RIGHT + Height:25 + Font:Bold + Text:Install Map + Container@MAP_PROGRESS: Width:PARENT_RIGHT - Height:25 - Font:TinyBold - Align:Center - Label@MAP_AUTHOR: - Y:243 - Width:PARENT_RIGHT - Height:25 - Font:Tiny - Align:Center \ No newline at end of file + Height:PARENT_BOTTOM + Children: + Background@MAP_BG: + Width:PARENT_RIGHT + Height:182 + Background:dialog3 + Children: + MapPreview@MAP_PREVIEW: + X:1 + Y:1 + Width:PARENT_RIGHT-2 + Height:PARENT_BOTTOM-2 + TooltipContainer:TOOLTIP_CONTAINER + Label@MAP_TITLE: + Y:185 + Width:PARENT_RIGHT + Height:25 + Font:Bold + Align:Center + Label@MAP_STATUS_SEARCHING: + Y:215 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + Text: Searching OpenRA Resource Center... + Container@MAP_STATUS_UNAVAILABLE: + Width:PARENT_RIGHT + Children: + Label@a: + Y:202 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + Text:This map was not found on the + Label@b: + Y:215 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + Text:OpenRA Resource Center + Label@MAP_STATUS_ERROR: + Y:215 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + Text:An error occurred during installation + Label@MAP_STATUS_DOWNLOADING: + Y:215 + Width:PARENT_RIGHT + Height:25 + Font:Tiny + Align:Center + ProgressBar@MAP_PROGRESSBAR: + Y:242 + Width:PARENT_RIGHT + Height:25 + Indeterminate:True + Button@MAP_RETRY: + Y:242 + Width:PARENT_RIGHT + Height:25 + Font:Bold \ No newline at end of file