diff --git a/OpenRA.Game/Download.cs b/OpenRA.Game/Download.cs deleted file mode 100644 index 8a9d7415fd..0000000000 --- a/OpenRA.Game/Download.cs +++ /dev/null @@ -1,96 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2020 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, either version 3 of - * the License, or (at your option) any later version. For more - * information, see COPYING. - */ -#endregion - -using System; -using System.ComponentModel; -using System.Net; - -namespace OpenRA -{ - public class Download - { - readonly object syncObject = new object(); - WebClient wc; - - public static string FormatErrorMessage(Exception e) - { - var ex = e as WebException; - if (ex == null) - return e.Message; - - switch (ex.Status) - { - case WebExceptionStatus.RequestCanceled: - return "Cancelled"; - case WebExceptionStatus.NameResolutionFailure: - return "DNS lookup failed"; - case WebExceptionStatus.Timeout: - return "Connection timeout"; - case WebExceptionStatus.ConnectFailure: - return "Cannot connect to remote server"; - case WebExceptionStatus.ProtocolError: - return "File not found on remote server"; - default: - return ex.Message; - } - } - - void EnableTLS12OnWindows() - { - // Enable TLS 1.2 on Windows: .NET 4.7 on Windows 10 only supports obsolete protocols by default - // SecurityProtocolType.Tls12 is not defined in the .NET 4.5 reference dlls used by mono, - // so we must use the enum's constant value directly - if (Platform.CurrentPlatform == PlatformType.Windows) - ServicePointManager.SecurityProtocol |= (SecurityProtocolType)3072; - } - - public Download(string url, string path, Action onProgress, Action onComplete) - { - EnableTLS12OnWindows(); - - lock (syncObject) - { - wc = new WebClient { Proxy = null }; - wc.DownloadProgressChanged += (_, a) => onProgress(a); - wc.DownloadFileCompleted += (_, a) => { DisposeWebClient(); onComplete(a); }; - wc.DownloadFileAsync(new Uri(url), path); - } - } - - public Download(string url, Action onProgress, Action onComplete) - { - EnableTLS12OnWindows(); - - lock (syncObject) - { - wc = new WebClient { Proxy = null }; - wc.DownloadProgressChanged += (_, a) => onProgress(a); - wc.DownloadDataCompleted += (_, a) => { DisposeWebClient(); onComplete(a); }; - wc.DownloadDataAsync(new Uri(url)); - } - } - - void DisposeWebClient() - { - lock (syncObject) - { - wc.Dispose(); - wc = null; - } - } - - public void CancelAsync() - { - lock (syncObject) - wc?.CancelAsync(); - } - } -} diff --git a/OpenRA.Game/HttpExtension.cs b/OpenRA.Game/HttpExtension.cs new file mode 100644 index 0000000000..c6317799c7 --- /dev/null +++ b/OpenRA.Game/HttpExtension.cs @@ -0,0 +1,63 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenRA +{ + public delegate void OnProgress(long total, long totalRead, int progressPercentage); + + public static class HttpExtension + { + public static async Task ReadAsStreamWithProgress(this HttpResponseMessage response, Stream outputStream, OnProgress onProgress, CancellationToken token) + { + var total = response.Content.Headers.ContentLength ?? -1; + var canReportProgress = total > 0; + +#if !MONO + using (var contentStream = await response.Content.ReadAsStreamAsync(token)) +#else + using (var contentStream = await response.Content.ReadAsStreamAsync()) +#endif + { + var totalRead = 0L; + var buffer = new byte[8192]; + var hasMoreToRead = true; + + do + { + var read = await contentStream.ReadAsync(buffer.AsMemory(0, buffer.Length), token); + if (read == 0) + hasMoreToRead = false; + else + { + await outputStream.WriteAsync(buffer.AsMemory(0, read), token); + + totalRead += read; + + if (canReportProgress) + { + var progressPercentage = (int)((double)totalRead / total * 100); + onProgress?.Invoke(total, totalRead, progressPercentage); + } + } + } + while (hasMoreToRead && !token.IsCancellationRequested); + + onProgress?.Invoke(total, totalRead, 100); + } + } + } +} diff --git a/OpenRA.Game/LocalPlayerProfile.cs b/OpenRA.Game/LocalPlayerProfile.cs index 833a17a285..20d7407bb9 100644 --- a/OpenRA.Game/LocalPlayerProfile.cs +++ b/OpenRA.Game/LocalPlayerProfile.cs @@ -12,10 +12,10 @@ using System; using System.IO; using System.Linq; -using System.Net; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; +using OpenRA.Support; namespace OpenRA { @@ -76,17 +76,16 @@ namespace OpenRA if (State != LinkState.Unlinked && State != LinkState.Linked && State != LinkState.ConnectionFailed) return; - Action onQueryComplete = i => + Task.Run(async () => { try { - if (i.Error != null) - { - innerState = LinkState.ConnectionFailed; - return; - } + var client = HttpClientFactory.Create(); - var yaml = MiniYaml.FromString(Encoding.UTF8.GetString(i.Result)).First(); + var httpResponseMessage = await client.GetAsync(playerDatabase.Profile + Fingerprint); + var result = await httpResponseMessage.Content.ReadAsStringAsync(); + + var yaml = MiniYaml.FromString(result).First(); if (yaml.Key == "Player") { innerData = FieldLoader.Load(yaml.Value); @@ -110,10 +109,9 @@ namespace OpenRA { onComplete?.Invoke(); } - }; + }); innerState = LinkState.CheckingLink; - new Download(playerDatabase.Profile + Fingerprint, _ => { }, onQueryComplete); } public void GenerateKeypair() diff --git a/OpenRA.Game/Map/MapCache.cs b/OpenRA.Game/Map/MapCache.cs index 8e260bf4b8..7ea2a59280 100644 --- a/OpenRA.Game/Map/MapCache.cs +++ b/OpenRA.Game/Map/MapCache.cs @@ -14,9 +14,8 @@ using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; -using System.Text; using System.Threading; +using System.Threading.Tasks; using OpenRA.FileSystem; using OpenRA.Graphics; using OpenRA.Primitives; @@ -179,24 +178,16 @@ namespace OpenRA var url = repositoryUrl + "hash/" + string.Join(",", maps.Keys) + "/yaml"; - Action onInfoComplete = i => + Task.Run(async () => { - if (i.Error != null) - { - Log.Write("debug", "Remote map query failed with error: {0}", Download.FormatErrorMessage(i.Error)); - Log.Write("debug", "URL was: {0}", url); - foreach (var p in maps.Values) - p.UpdateRemoteSearch(MapStatus.Unavailable, null); - - queryFailed?.Invoke(); - - return; - } - - var data = Encoding.UTF8.GetString(i.Result); try { - var yaml = MiniYaml.FromString(data); + var client = HttpClientFactory.Create(); + + var httpResponseMessage = await client.GetAsync(url); + var result = await httpResponseMessage.Content.ReadAsStringAsync(); + + var yaml = MiniYaml.FromString(result); foreach (var kv in yaml) maps[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, mapDetailsReceived); @@ -206,13 +197,15 @@ namespace OpenRA } catch (Exception e) { - Log.Write("debug", "Can't parse remote map search data:\n{0}", data); - Log.Write("debug", "Exception: {0}", e); + Log.Write("debug", "Remote map query failed with error: {0}", e); + Log.Write("debug", "URL was: {0}", url); + + foreach (var p in maps.Values) + p.UpdateRemoteSearch(MapStatus.Unavailable, null); + queryFailed?.Invoke(); } - }; - - new Download(url, _ => { }, onInfoComplete); + }); } void LoadAsyncInternal() diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs index cc9569631b..ef15a58bac 100644 --- a/OpenRA.Game/Map/MapPreview.cs +++ b/OpenRA.Game/Map/MapPreview.cs @@ -14,13 +14,15 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Net; +using System.Net.Http; using System.Text; using System.Threading; +using System.Threading.Tasks; using OpenRA.FileFormats; using OpenRA.FileSystem; using OpenRA.Graphics; using OpenRA.Primitives; +using OpenRA.Support; namespace OpenRA { @@ -166,7 +168,6 @@ namespace OpenRA } } - Download download; public long DownloadBytes { get; private set; } public int DownloadPercentage { get; private set; } @@ -435,74 +436,57 @@ namespace OpenRA } var mapInstallPackage = installLocation.Key as IReadWritePackage; - new Thread(() => + + Task.Run(async () => { // Request the filename from the server // Run in a worker thread to avoid network delays var mapUrl = mapRepositoryUrl + Uid; - var mapFilename = string.Empty; try { - var request = WebRequest.Create(mapUrl); - request.Method = "HEAD"; - using (var res = request.GetResponse()) + void OnDownloadProgress(long total, long received, int percentage) { - // Map not found - if (res.Headers["Content-Disposition"] == null) - { - innerData.Status = MapStatus.DownloadError; - return; - } - - mapFilename = res.Headers["Content-Disposition"].Replace("attachment; filename = ", ""); + DownloadBytes = total; + DownloadPercentage = percentage; } - Action onDownloadProgress = i => { DownloadBytes = i.BytesReceived; DownloadPercentage = i.ProgressPercentage; }; - Action onDownloadComplete = i => + var client = HttpClientFactory.Create(); + + var response = await client.GetAsync(mapUrl, HttpCompletionOption.ResponseHeadersRead); + + if (!response.IsSuccessStatusCode) { - download = null; + innerData.Status = MapStatus.DownloadError; + return; + } - if (i.Error != null) - { - Log.Write("debug", "Remote map download failed with error: {0}", Download.FormatErrorMessage(i.Error)); - Log.Write("debug", "URL was: {0}", mapUrl); + response.Headers.TryGetValues("Content-Disposition", out var values); + var mapFilename = values.First().Replace("attachment; filename = ", ""); + var fileStream = new MemoryStream(); + + await response.ReadAsStreamWithProgress(fileStream, OnDownloadProgress, CancellationToken.None); + + mapInstallPackage.Update(mapFilename, fileStream.ToArray()); + Log.Write("debug", "Downloaded map to '{0}'", mapFilename); + Game.RunAfterTick(() => + { + var package = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles); + if (package == null) innerData.Status = MapStatus.DownloadError; - return; - } - - mapInstallPackage.Update(mapFilename, i.Result); - Log.Write("debug", "Downloaded map to '{0}'", mapFilename); - Game.RunAfterTick(() => + else { - var package = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles); - if (package == null) - innerData.Status = MapStatus.DownloadError; - else - { - UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType); - onSuccess(); - } - }); - }; - - download = new Download(mapUrl, onDownloadProgress, onDownloadComplete); + UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType); + onSuccess(); + } + }); } catch (Exception e) { Console.WriteLine(e.Message); innerData.Status = MapStatus.DownloadError; } - }).Start(); - } - - public void CancelInstall() - { - if (download == null) - return; - - download.CancelAsync(); - download = null; + }); } public void Invalidate() diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index e6c0d0354d..a10e4a94e8 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -41,6 +41,7 @@ + diff --git a/OpenRA.Game/PlayerDatabase.cs b/OpenRA.Game/PlayerDatabase.cs index ea0c700b13..bf5f4ea369 100644 --- a/OpenRA.Game/PlayerDatabase.cs +++ b/OpenRA.Game/PlayerDatabase.cs @@ -9,13 +9,12 @@ */ #endregion -using System; -using System.IO; using System.Linq; -using System.Net; +using System.Threading.Tasks; using OpenRA.FileFormats; using OpenRA.Graphics; using OpenRA.Primitives; +using OpenRA.Support; namespace OpenRA { @@ -39,14 +38,16 @@ namespace OpenRA var spriteSize = IconSize * density; var sprite = sheetBuilder.Allocate(new Size(spriteSize, spriteSize), 1f / density); - Action onComplete = i => + Task.Run(async () => { - if (i.Error != null) - return; - try { - var icon = new Png(new MemoryStream(i.Result)); + var client = HttpClientFactory.Create(); + + var httpResponseMessage = await client.GetAsync(url); + var result = await httpResponseMessage.Content.ReadAsStreamAsync(); + + var icon = new Png(result); if (icon.Width == spriteSize && icon.Height == spriteSize) { Game.RunAfterTick(() => @@ -57,9 +58,7 @@ namespace OpenRA } } catch { } - }; - - new Download(url, _ => { }, onComplete); + }); return sprite; } diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs index 1d45a15a84..b790e39a97 100644 --- a/OpenRA.Game/Server/Server.cs +++ b/OpenRA.Game/Server/Server.cs @@ -18,6 +18,7 @@ using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; +using System.Threading.Tasks; using OpenRA.FileFormats; using OpenRA.Network; using OpenRA.Primitives; @@ -560,53 +561,50 @@ namespace OpenRA.Server { waitingForAuthenticationCallback++; - Action onQueryComplete = i => + Task.Run(async () => { + var httpClient = HttpClientFactory.Create(); + var httpResponseMessage = await httpClient.GetAsync(playerDatabase.Profile + handshake.Fingerprint); + var result = await httpResponseMessage.Content.ReadAsStringAsync(); PlayerProfile profile = null; - if (i.Error == null) + try { - try + var yaml = MiniYaml.FromString(result).First(); + if (yaml.Key == "Player") { - var yaml = MiniYaml.FromString(Encoding.UTF8.GetString(i.Result)).First(); - if (yaml.Key == "Player") - { - profile = FieldLoader.Load(yaml.Value); + profile = FieldLoader.Load(yaml.Value); - var publicKey = Encoding.ASCII.GetString(Convert.FromBase64String(profile.PublicKey)); - var parameters = CryptoUtil.DecodePEMPublicKey(publicKey); - if (!profile.KeyRevoked && CryptoUtil.VerifySignature(parameters, newConn.AuthToken, handshake.AuthSignature)) - { - client.Fingerprint = handshake.Fingerprint; - Log.Write("server", "{0} authenticated as {1} (UID {2})", newConn.Socket.RemoteEndPoint, - profile.ProfileName, profile.ProfileID); - } - else if (profile.KeyRevoked) - { - profile = null; - Log.Write("server", "{0} failed to authenticate as {1} (key revoked)", newConn.Socket.RemoteEndPoint, handshake.Fingerprint); - } - else - { - profile = null; - Log.Write("server", "{0} failed to authenticate as {1} (signature verification failed)", - newConn.Socket.RemoteEndPoint, handshake.Fingerprint); - } + var publicKey = Encoding.ASCII.GetString(Convert.FromBase64String(profile.PublicKey)); + var parameters = CryptoUtil.DecodePEMPublicKey(publicKey); + if (!profile.KeyRevoked && CryptoUtil.VerifySignature(parameters, newConn.AuthToken, handshake.AuthSignature)) + { + client.Fingerprint = handshake.Fingerprint; + Log.Write("server", "{0} authenticated as {1} (UID {2})", newConn.Socket.RemoteEndPoint, + profile.ProfileName, profile.ProfileID); + } + else if (profile.KeyRevoked) + { + profile = null; + Log.Write("server", "{0} failed to authenticate as {1} (key revoked)", newConn.Socket.RemoteEndPoint, handshake.Fingerprint); } else - Log.Write("server", "{0} failed to authenticate as {1} (invalid server response: `{2}` is not `Player`)", - newConn.Socket.RemoteEndPoint, handshake.Fingerprint, yaml.Key); - } - catch (Exception ex) - { - Log.Write("server", "{0} failed to authenticate as {1} (exception occurred)", - newConn.Socket.RemoteEndPoint, handshake.Fingerprint); - Log.Write("server", ex.ToString()); + { + profile = null; + Log.Write("server", "{0} failed to authenticate as {1} (signature verification failed)", + newConn.Socket.RemoteEndPoint, handshake.Fingerprint); + } } + else + Log.Write("server", "{0} failed to authenticate as {1} (invalid server response: `{2}` is not `Player`)", + newConn.Socket.RemoteEndPoint, handshake.Fingerprint, yaml.Key); + } + catch (Exception ex) + { + Log.Write("server", "{0} failed to authenticate as {1} (exception occurred)", + newConn.Socket.RemoteEndPoint, handshake.Fingerprint); + Log.Write("server", ex.ToString()); } - else - Log.Write("server", "{0} failed to authenticate as {1} (server error: `{2}`)", - newConn.Socket.RemoteEndPoint, handshake.Fingerprint, i.Error); delayedActions.Add(() => { @@ -636,9 +634,7 @@ namespace OpenRA.Server waitingForAuthenticationCallback--; }, 0); - }; - - new Download(playerDatabase.Profile + handshake.Fingerprint, _ => { }, onQueryComplete); + }); } else { diff --git a/OpenRA.Game/Support/HttpClientFactory.cs b/OpenRA.Game/Support/HttpClientFactory.cs new file mode 100644 index 0000000000..6196b2cb1f --- /dev/null +++ b/OpenRA.Game/Support/HttpClientFactory.cs @@ -0,0 +1,48 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Net.Http; + +namespace OpenRA.Support +{ + public class HttpClientFactory + { +#if !MONO + const int MaxConnectionPerServer = 20; + static readonly TimeSpan ConnectionLifeTime = TimeSpan.FromMinutes(1); +#endif + + static readonly Lazy Handler = new Lazy(GetHandler); + + public static HttpClient Create() + { + return new HttpClient(Handler.Value, false); + } + + static HttpMessageHandler GetHandler() + { +#if !MONO + return new SocketsHttpHandler + { + // https://github.com/dotnet/corefx/issues/26895 + // https://github.com/dotnet/corefx/issues/26331 + // https://github.com/dotnet/corefx/pull/26839 + PooledConnectionLifetime = ConnectionLifeTime, + PooledConnectionIdleTimeout = ConnectionLifeTime, + MaxConnectionsPerServer = MaxConnectionPerServer + }; +#else + return new HttpClientHandler(); +#endif + } + } +} diff --git a/OpenRA.Game/Support/HttpQueryBuilder.cs b/OpenRA.Game/Support/HttpQueryBuilder.cs new file mode 100644 index 0000000000..c27d2eafd6 --- /dev/null +++ b/OpenRA.Game/Support/HttpQueryBuilder.cs @@ -0,0 +1,61 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 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, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace OpenRA.Support +{ + public class HttpQueryBuilder : IEnumerable + { + readonly string url; + readonly List parameters = new List(); + + public HttpQueryBuilder(string url) + { + this.url = url; + } + + public void Add(string name, object value) + { + parameters.Add(new Parameter + { + Name = name, + Value = Uri.EscapeUriString(value.ToString()) + }); + } + + public override string ToString() + { + var builder = new StringBuilder(url); + + builder.Append("?"); + + foreach (var parameter in parameters) + builder.Append($"{parameter.Name}={parameter.Value}&"); + + return builder.ToString(); + } + + class Parameter + { + public string Name { get; set; } + public string Value { get; set; } + } + + public IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + } +} diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 12290c97b6..fb3f866c49 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -33,6 +33,7 @@ + diff --git a/OpenRA.Mods.Common/ServerTraits/MasterServerPinger.cs b/OpenRA.Mods.Common/ServerTraits/MasterServerPinger.cs index 61e36ba212..ff9401eeca 100644 --- a/OpenRA.Mods.Common/ServerTraits/MasterServerPinger.cs +++ b/OpenRA.Mods.Common/ServerTraits/MasterServerPinger.cs @@ -11,12 +11,13 @@ using System; using System.Collections.Generic; -using System.Net; +using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; using BeaconLib; using OpenRA.Network; using OpenRA.Server; +using OpenRA.Support; using S = OpenRA.Server.Server; namespace OpenRA.Mods.Common.Server @@ -41,7 +42,7 @@ namespace OpenRA.Mods.Common.Server bool isInitialPing = true; volatile bool isBusy; - Queue masterServerMessages = new Queue(); + readonly Queue masterServerMessages = new Queue(); static MasterServerPinger() { @@ -108,47 +109,47 @@ namespace OpenRA.Mods.Common.Server { isBusy = true; - Task.Run(() => + Task.Run(async () => { try { var endpoint = server.ModData.Manifest.Get().ServerAdvertise; - using (var wc = new WebClient()) + + var client = HttpClientFactory.Create(); + var response = await client.PostAsync(endpoint, new StringContent(postData)); + + var masterResponseText = await response.Content.ReadAsStringAsync(); + + if (isInitialPing) { - wc.Proxy = null; - var masterResponseText = wc.UploadString(endpoint, postData); + Log.Write("server", "Master server: " + masterResponseText); + var errorCode = 0; + var errorMessage = string.Empty; - if (isInitialPing) + if (!string.IsNullOrWhiteSpace(masterResponseText)) { - Log.Write("server", "Master server: " + masterResponseText); - var errorCode = 0; - var errorMessage = string.Empty; + var regex = new Regex(@"^\[(?-?\d+)\](?.*)"); + var match = regex.Match(masterResponseText); + errorMessage = match.Success && int.TryParse(match.Groups["code"].Value, out errorCode) ? + match.Groups["message"].Value.Trim() : "Failed to parse error message"; + } - if (masterResponseText.Length > 0) + isInitialPing = false; + lock (masterServerMessages) + { + masterServerMessages.Enqueue("Master server communication established."); + if (errorCode != 0) { - var regex = new Regex(@"^\[(?-?\d+)\](?.*)"); - var match = regex.Match(masterResponseText); - errorMessage = match.Success && int.TryParse(match.Groups["code"].Value, out errorCode) ? - match.Groups["message"].Value.Trim() : "Failed to parse error message"; - } + // Hardcoded error messages take precedence over the server-provided messages + if (!MasterServerErrors.TryGetValue(errorCode, out var message)) + message = errorMessage; - isInitialPing = false; - lock (masterServerMessages) - { - masterServerMessages.Enqueue("Master server communication established."); - if (errorCode != 0) - { - // Hardcoded error messages take precedence over the server-provided messages - if (!MasterServerErrors.TryGetValue(errorCode, out var message)) - message = errorMessage; + masterServerMessages.Enqueue("Warning: " + message); - masterServerMessages.Enqueue("Warning: " + message); - - // Positive error codes indicate errors that prevent advertisement - // Negative error codes are non-fatal warnings - if (errorCode > 0) - masterServerMessages.Enqueue("Game has not been advertised online."); - } + // Positive error codes indicate errors that prevent advertisement + // Negative error codes are non-fatal warnings + if (errorCode > 0) + masterServerMessages.Enqueue("Game has not been advertised online."); } } } diff --git a/OpenRA.Mods.Common/WebServices.cs b/OpenRA.Mods.Common/WebServices.cs index cbbf3bcea9..7ee518327a 100644 --- a/OpenRA.Mods.Common/WebServices.cs +++ b/OpenRA.Mods.Common/WebServices.cs @@ -10,8 +10,8 @@ #endregion using System; -using System.Net; -using System.Text; +using System.Threading.Tasks; +using OpenRA.Support; namespace OpenRA.Mods.Common { @@ -31,34 +31,31 @@ namespace OpenRA.Mods.Common public void CheckModVersion() { - Action onComplete = i => + Task.Run(async () => { - if (i.Error != null) - return; - try + var queryURL = new HttpQueryBuilder(VersionCheck) { - var data = Encoding.UTF8.GetString(i.Result); + { "protocol", VersionCheckProtocol }, + { "engine", Game.EngineVersion }, + { "mod", Game.ModData.Manifest.Id }, + { "version", Game.ModData.Manifest.Metadata.Version } + }.ToString(); - var status = ModVersionStatus.Latest; - switch (data) - { - case "outdated": status = ModVersionStatus.Outdated; break; - case "unknown": status = ModVersionStatus.Unknown; break; - case "playtest": status = ModVersionStatus.PlaytestAvailable; break; - } + var client = HttpClientFactory.Create(); - Game.RunAfterTick(() => ModVersionStatus = status); + var httpResponseMessage = await client.GetAsync(queryURL); + var result = await httpResponseMessage.Content.ReadAsStringAsync(); + + var status = ModVersionStatus.Latest; + switch (result) + { + case "outdated": status = ModVersionStatus.Outdated; break; + case "unknown": status = ModVersionStatus.Unknown; break; + case "playtest": status = ModVersionStatus.PlaytestAvailable; break; } - catch { } - }; - var queryURL = VersionCheck + "?protocol={0}&engine={1}&mod={2}&version={3}".F( - VersionCheckProtocol, - Uri.EscapeUriString(Game.EngineVersion), - Uri.EscapeUriString(Game.ModData.Manifest.Id), - Uri.EscapeUriString(Game.ModData.Manifest.Metadata.Version)); - - new Download(queryURL, _ => { }, onComplete); + Game.RunAfterTick(() => ModVersionStatus = status); + }); } } } diff --git a/OpenRA.Mods.Common/Widgets/Logic/Installation/DownloadPackageLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Installation/DownloadPackageLogic.cs index 8b520e81a6..bdf9451a82 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Installation/DownloadPackageLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Installation/DownloadPackageLogic.cs @@ -11,10 +11,10 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.IO; -using System.Net; -using System.Text; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; using ICSharpCode.SharpZipLib.Zip; using OpenRA.Support; using OpenRA.Widgets; @@ -65,47 +65,35 @@ namespace OpenRA.Mods.Common.Widgets.Logic var cancelButton = panel.Get("CANCEL_BUTTON"); - var file = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - - Action deleteTempFile = () => - { - Log.Write("install", "Deleting temporary file " + file); - File.Delete(file); - }; - - Action onDownloadProgress = i => + void OnDownloadProgress(long total, long read, int progressPercentage) { var dataReceived = 0.0f; var dataTotal = 0.0f; var mag = 0; var dataSuffix = ""; - if (i.TotalBytesToReceive < 0) + if (total < 0) { - mag = (int)Math.Log(i.BytesReceived, 1024); - dataReceived = i.BytesReceived / (float)(1L << (mag * 10)); + mag = (int)Math.Log(read, 1024); + dataReceived = read / (float)(1L << (mag * 10)); dataSuffix = SizeSuffixes[mag]; - getStatusText = () => "Downloading from {2} {0:0.00} {1}".F(dataReceived, - dataSuffix, - downloadHost ?? "unknown host"); + getStatusText = () => "Downloading from {2} {0:0.00} {1}".F(dataReceived, dataSuffix, downloadHost ?? "unknown host"); progressBar.Indeterminate = true; } else { - mag = (int)Math.Log(i.TotalBytesToReceive, 1024); - dataTotal = i.TotalBytesToReceive / (float)(1L << (mag * 10)); - dataReceived = i.BytesReceived / (float)(1L << (mag * 10)); + mag = (int)Math.Log(total, 1024); + dataTotal = total / (float)(1L << (mag * 10)); + dataReceived = read / (float)(1L << (mag * 10)); dataSuffix = SizeSuffixes[mag]; - getStatusText = () => "Downloading from {4} {1:0.00}/{2:0.00} {3} ({0}%)".F(i.ProgressPercentage, - dataReceived, dataTotal, dataSuffix, - downloadHost ?? "unknown host"); + getStatusText = () => "Downloading from {4} {1:0.00}/{2:0.00} {3} ({0}%)".F(progressPercentage, dataReceived, dataTotal, dataSuffix, downloadHost ?? "unknown host"); progressBar.Indeterminate = false; } - progressBar.Percentage = i.ProgressPercentage; - }; + progressBar.Percentage = progressPercentage; + } Action onExtractProgress = s => Game.RunAfterTick(() => getStatusText = () => s); @@ -120,137 +108,140 @@ namespace OpenRA.Mods.Common.Widgets.Logic cancelButton.OnClick = Ui.CloseWindow; }); - Action onDownloadComplete = i => - { - if (i.Cancelled) - { - deleteTempFile(); - Game.RunAfterTick(Ui.CloseWindow); - return; - } - - if (i.Error != null) - { - deleteTempFile(); - onError(Download.FormatErrorMessage(i.Error)); - return; - } - - // Validate integrity - if (!string.IsNullOrEmpty(download.SHA1)) - { - getStatusText = () => "Verifying archive..."; - progressBar.Indeterminate = true; - - var archiveValid = false; - try - { - using (var stream = File.OpenRead(file)) - { - var archiveSHA1 = CryptoUtil.SHA1Hash(stream); - Log.Write("install", "Downloaded SHA1: " + archiveSHA1); - Log.Write("install", "Expected SHA1: " + download.SHA1); - - archiveValid = archiveSHA1 == download.SHA1; - } - } - catch (Exception e) - { - Log.Write("install", "SHA1 calculation failed: " + e.ToString()); - } - - if (!archiveValid) - { - onError("Archive validation failed"); - deleteTempFile(); - return; - } - } - - // Automatically extract - getStatusText = () => "Extracting..."; - progressBar.Indeterminate = true; - - var extracted = new List(); - try - { - using (var stream = File.OpenRead(file)) - using (var z = new ZipFile(stream)) - { - foreach (var kv in download.Extract) - { - var entry = z.GetEntry(kv.Value); - if (entry == null || !entry.IsFile) - continue; - - onExtractProgress("Extracting " + entry.Name); - Log.Write("install", "Extracting " + entry.Name); - var targetPath = Platform.ResolvePath(kv.Key); - Directory.CreateDirectory(Path.GetDirectoryName(targetPath)); - extracted.Add(targetPath); - - using (var zz = z.GetInputStream(entry)) - using (var f = File.Create(targetPath)) - zz.CopyTo(f); - } - - z.Close(); - } - - Game.RunAfterTick(() => { Ui.CloseWindow(); onSuccess(); }); - } - catch (Exception e) - { - Log.Write("install", "Archive extraction failed: " + e.ToString()); - - foreach (var f in extracted) - { - Log.Write("install", "Deleting " + f); - File.Delete(f); - } - - onError("Archive extraction failed"); - } - finally - { - deleteTempFile(); - } - }; - Action downloadUrl = url => { Log.Write("install", "Downloading " + url); + var tokenSource = new CancellationTokenSource(); + var token = tokenSource.Token; downloadHost = new Uri(url).Host; - var dl = new Download(url, file, onDownloadProgress, onDownloadComplete); - cancelButton.OnClick = dl.CancelAsync; + + cancelButton.OnClick = () => + { + tokenSource.Cancel(); + Game.RunAfterTick(Ui.CloseWindow); + }; + retryButton.OnClick = ShowDownloadDialog; + + Task.Run(async () => + { + var file = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + try + { + var client = HttpClientFactory.Create(); + + var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token); + + using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.ReadWrite, 8192, true)) + { + await response.ReadAsStreamWithProgress(fileStream, OnDownloadProgress, token); + } + + // Validate integrity + if (!string.IsNullOrEmpty(download.SHA1)) + { + getStatusText = () => "Verifying archive..."; + progressBar.Indeterminate = true; + + var archiveValid = false; + try + { + using (var stream = File.OpenRead(file)) + { + var archiveSHA1 = CryptoUtil.SHA1Hash(stream); + Log.Write("install", "Downloaded SHA1: " + archiveSHA1); + Log.Write("install", "Expected SHA1: " + download.SHA1); + + archiveValid = archiveSHA1 == download.SHA1; + } + } + catch (Exception e) + { + Log.Write("install", "SHA1 calculation failed: " + e.ToString()); + } + + if (!archiveValid) + { + onError("Archive validation failed"); + return; + } + } + + // Automatically extract + getStatusText = () => "Extracting..."; + progressBar.Indeterminate = true; + + var extracted = new List(); + try + { + using (var stream = File.OpenRead(file)) + using (var z = new ZipFile(stream)) + { + foreach (var kv in download.Extract) + { + var entry = z.GetEntry(kv.Value); + if (entry == null || !entry.IsFile) + continue; + + onExtractProgress("Extracting " + entry.Name); + Log.Write("install", "Extracting " + entry.Name); + var targetPath = Platform.ResolvePath(kv.Key); + Directory.CreateDirectory(Path.GetDirectoryName(targetPath)); + extracted.Add(targetPath); + + using (var zz = z.GetInputStream(entry)) + using (var f = File.Create(targetPath)) + zz.CopyTo(f); + } + + z.Close(); + } + + Game.RunAfterTick(() => + { + Ui.CloseWindow(); + onSuccess(); + }); + } + catch (Exception e) + { + Log.Write("install", "Archive extraction failed: " + e.ToString()); + + foreach (var f in extracted) + { + Log.Write("install", "Deleting " + f); + File.Delete(f); + } + + onError("Archive extraction failed"); + } + } + catch (Exception e) + { + onError(e.ToString()); + } + finally + { + File.Delete(file); + } + }, token); }; if (download.MirrorList != null) { Log.Write("install", "Fetching mirrors from " + download.MirrorList); - Action onFetchMirrorsComplete = i => + Task.Run(async () => { - progressBar.Indeterminate = true; - - if (i.Cancelled) - { - Game.RunAfterTick(Ui.CloseWindow); - return; - } - - if (i.Error != null) - { - onError(Download.FormatErrorMessage(i.Error)); - return; - } + var client = HttpClientFactory.Create(); + var httpResponseMessage = await client.GetAsync(download.MirrorList); + var result = await httpResponseMessage.Content.ReadAsStringAsync(); try { - var data = Encoding.UTF8.GetString(i.Result); - var mirrorList = data.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + var mirrorList = result.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); downloadUrl(mirrorList.Random(new MersenneTwister())); } catch (Exception e) @@ -259,11 +250,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic Log.Write("install", e.ToString()); onError("Online mirror is not available. Please install from an original disc."); } - }; - - var updateMirrors = new Download(download.MirrorList, onDownloadProgress, onFetchMirrorsComplete); - cancelButton.OnClick = updateMirrors.CancelAsync; - retryButton.OnClick = ShowDownloadDialog; + }); } else downloadUrl(download.URL); diff --git a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs index a5c9492756..e3a1feb48d 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs @@ -14,7 +14,9 @@ using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; +using System.Threading.Tasks; using OpenRA.Network; +using OpenRA.Support; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets.Logic @@ -274,19 +276,47 @@ namespace OpenRA.Mods.Common.Widgets.Logic { if (!fetchedNews) { - // Send the mod and engine version to support version-filtered news (update prompts) - var newsURL = "{0}?version={1}&mod={2}&modversion={3}".F( - webServices.GameNews, - Uri.EscapeUriString(Game.EngineVersion), - Uri.EscapeUriString(Game.ModData.Manifest.Id), - Uri.EscapeUriString(Game.ModData.Manifest.Metadata.Version)); + Task.Run(async () => + { + try + { + var client = HttpClientFactory.Create(); - // Parameter string is blank if the player has opted out - newsURL += SystemInfoPromptLogic.CreateParameterString(); + // Send the mod and engine version to support version-filtered news (update prompts) + var url = new HttpQueryBuilder(webServices.GameNews) + { + { "version", Game.EngineVersion }, + { "mod", Game.ModData.Manifest.Id }, + { "modversion", Game.ModData.Manifest.Metadata.Version } + }.ToString(); - new Download(newsURL, cacheFile, e => { }, - e => NewsDownloadComplete(e, cacheFile, currentNews, - () => OpenNewsPanel(newsButton))); + // Parameter string is blank if the player has opted out + url += SystemInfoPromptLogic.CreateParameterString(); + + var response = await client.GetStringAsync(url); + await File.WriteAllTextAsync(cacheFile, response); + + Game.RunAfterTick(() => // run on the main thread + { + fetchedNews = true; + var newNews = ParseNews(cacheFile); + if (newNews == null) + return; + + DisplayNews(newNews); + + if (currentNews == null || newNews.Any(n => !currentNews.Select(c => c.DateTime).Contains(n.DateTime))) + OpenNewsPanel(newsButton); + }); + } + catch (Exception e) + { + Game.RunAfterTick(() => // run on the main thread + { + SetNewsStatus("Failed to retrieve news: {0}".F(e)); + }); + } + }); } newsButton.OnClick = () => OpenNewsPanel(newsButton); @@ -364,28 +394,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic return null; } - void NewsDownloadComplete(AsyncCompletedEventArgs e, string cacheFile, NewsItem[] oldNews, Action onNewsDownloaded) - { - Game.RunAfterTick(() => // run on the main thread - { - if (e.Error != null) - { - SetNewsStatus("Failed to retrieve news: {0}".F(Download.FormatErrorMessage(e.Error))); - return; - } - - fetchedNews = true; - var newNews = ParseNews(cacheFile); - if (newNews == null) - return; - - DisplayNews(newNews); - - if (oldNews == null || newNews.Any(n => !oldNews.Select(c => c.DateTime).Contains(n.DateTime))) - onNewsDownloaded(); - }); - } - void DisplayNews(IEnumerable newsItems) { newsPanel.RemoveChildren(); diff --git a/OpenRA.Mods.Common/Widgets/Logic/PlayerProfileLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/PlayerProfileLogic.cs index 3fb984658f..0ca15a0b00 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/PlayerProfileLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/PlayerProfileLogic.cs @@ -11,10 +11,10 @@ using System; using System.Linq; -using System.Net; -using System.Text; +using System.Threading.Tasks; using OpenRA.Graphics; using OpenRA.Network; +using OpenRA.Support; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets.Logic @@ -157,79 +157,81 @@ namespace OpenRA.Mods.Common.Widgets.Logic var messageText = "Loading player profile..."; var messageWidth = messageFont.Measure(messageText).X + 2 * message.Bounds.Left; - Action onQueryComplete = i => + Task.Run(async () => { try { - if (i.Error == null) + var httpClient = HttpClientFactory.Create(); + + var httpResponseMessage = await httpClient.GetAsync(playerDatabase.Profile + client.Fingerprint); + var result = await httpResponseMessage.Content.ReadAsStringAsync(); + + var yaml = MiniYaml.FromString(result).First(); + if (yaml.Key == "Player") { - var yaml = MiniYaml.FromString(Encoding.UTF8.GetString(i.Result)).First(); - if (yaml.Key == "Player") + profile = FieldLoader.Load(yaml.Value); + Game.RunAfterTick(() => { - profile = FieldLoader.Load(yaml.Value); - Game.RunAfterTick(() => + var nameLabel = profileHeader.Get("PROFILE_NAME"); + var nameFont = Game.Renderer.Fonts[nameLabel.Font]; + var rankLabel = profileHeader.Get("PROFILE_RANK"); + var rankFont = Game.Renderer.Fonts[rankLabel.Font]; + + var adminContainer = profileHeader.Get("GAME_ADMIN"); + var adminLabel = adminContainer.Get("LABEL"); + var adminFont = Game.Renderer.Fonts[adminLabel.Font]; + + var headerSizeOffset = profileHeader.Bounds.Height - messageHeader.Bounds.Height; + + nameLabel.GetText = () => profile.ProfileName; + rankLabel.GetText = () => profile.ProfileRank; + + profileWidth = Math.Max(profileWidth, nameFont.Measure(profile.ProfileName).X + 2 * nameLabel.Bounds.Left); + profileWidth = Math.Max(profileWidth, rankFont.Measure(profile.ProfileRank).X + 2 * rankLabel.Bounds.Left); + + header.Bounds.Height += headerSizeOffset; + badgeContainer.Bounds.Y += header.Bounds.Height; + if (client.IsAdmin) { - var nameLabel = profileHeader.Get("PROFILE_NAME"); - var nameFont = Game.Renderer.Fonts[nameLabel.Font]; - var rankLabel = profileHeader.Get("PROFILE_RANK"); - var rankFont = Game.Renderer.Fonts[rankLabel.Font]; + profileWidth = Math.Max(profileWidth, adminFont.Measure(adminLabel.Text).X + 2 * adminLabel.Bounds.Left); - var adminContainer = profileHeader.Get("GAME_ADMIN"); - var adminLabel = adminContainer.Get("LABEL"); - var adminFont = Game.Renderer.Fonts[adminLabel.Font]; + adminContainer.IsVisible = () => true; + profileHeader.Bounds.Height += adminLabel.Bounds.Height; + header.Bounds.Height += adminLabel.Bounds.Height; + badgeContainer.Bounds.Y += adminLabel.Bounds.Height; + } - var headerSizeOffset = profileHeader.Bounds.Height - messageHeader.Bounds.Height; + Func negotiateWidth = badgeWidth => + { + profileWidth = Math.Min(Math.Max(badgeWidth, profileWidth), maxProfileWidth); + return profileWidth; + }; - nameLabel.GetText = () => profile.ProfileName; - rankLabel.GetText = () => profile.ProfileRank; - - profileWidth = Math.Max(profileWidth, nameFont.Measure(profile.ProfileName).X + 2 * nameLabel.Bounds.Left); - profileWidth = Math.Max(profileWidth, rankFont.Measure(profile.ProfileRank).X + 2 * rankLabel.Bounds.Left); - - header.Bounds.Height += headerSizeOffset; - badgeContainer.Bounds.Y += header.Bounds.Height; - if (client.IsAdmin) + if (profile.Badges.Any()) + { + var badges = Ui.LoadWidget("PLAYER_PROFILE_BADGES_INSERT", badgeContainer, new WidgetArgs() { - profileWidth = Math.Max(profileWidth, adminFont.Measure(adminLabel.Text).X + 2 * adminLabel.Bounds.Left); + { "worldRenderer", worldRenderer }, + { "profile", profile }, + { "negotiateWidth", negotiateWidth } + }); - adminContainer.IsVisible = () => true; - profileHeader.Bounds.Height += adminLabel.Bounds.Height; - header.Bounds.Height += adminLabel.Bounds.Height; - badgeContainer.Bounds.Y += adminLabel.Bounds.Height; + if (badges.Bounds.Height > 0) + { + badgeContainer.Bounds.Height = badges.Bounds.Height; + badgeContainer.IsVisible = () => true; } + } - Func negotiateWidth = badgeWidth => - { - profileWidth = Math.Min(Math.Max(badgeWidth, profileWidth), maxProfileWidth); - return profileWidth; - }; + profileWidth = Math.Min(profileWidth, maxProfileWidth); + header.Bounds.Width = widget.Bounds.Width = badgeContainer.Bounds.Width = profileWidth; + widget.Bounds.Height = header.Bounds.Height + badgeContainer.Bounds.Height; - if (profile.Badges.Any()) - { - var badges = Ui.LoadWidget("PLAYER_PROFILE_BADGES_INSERT", badgeContainer, new WidgetArgs() - { - { "worldRenderer", worldRenderer }, - { "profile", profile }, - { "negotiateWidth", negotiateWidth } - }); + if (badgeSeparator != null) + badgeSeparator.Bounds.Width = profileWidth - 2 * badgeSeparator.Bounds.X; - if (badges.Bounds.Height > 0) - { - badgeContainer.Bounds.Height = badges.Bounds.Height; - badgeContainer.IsVisible = () => true; - } - } - - profileWidth = Math.Min(profileWidth, maxProfileWidth); - header.Bounds.Width = widget.Bounds.Width = badgeContainer.Bounds.Width = profileWidth; - widget.Bounds.Height = header.Bounds.Height + badgeContainer.Bounds.Height; - - if (badgeSeparator != null) - badgeSeparator.Bounds.Width = profileWidth - 2 * badgeSeparator.Bounds.X; - - profileLoaded = true; - }); - } + profileLoaded = true; + }); } } catch (Exception e) @@ -245,15 +247,13 @@ namespace OpenRA.Mods.Common.Widgets.Logic header.Bounds.Width = widget.Bounds.Width = messageWidth; } } - }; + }); message.GetText = () => messageText; header.Bounds.Height += messageHeader.Bounds.Height; header.Bounds.Width = widget.Bounds.Width = messageWidth; widget.Bounds.Height = header.Bounds.Height; badgeContainer.Visible = false; - - new Download(playerDatabase.Profile + client.Fingerprint, _ => { }, onQueryComplete); } } diff --git a/OpenRA.Mods.Common/Widgets/Logic/ServerListLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ServerListLogic.cs index 3d3b39d80b..10a0fe92ff 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ServerListLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ServerListLogic.cs @@ -12,12 +12,12 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; -using System.Text; +using System.Threading.Tasks; using BeaconLib; using OpenRA.Network; using OpenRA.Primitives; using OpenRA.Server; +using OpenRA.Support; using OpenRA.Traits; using OpenRA.Widgets; @@ -59,7 +59,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic SearchStatus searchStatus = SearchStatus.Fetching; - Download currentQuery; + bool activeQuery; IEnumerable lanGameLocations; public string ProgressLabelText() @@ -322,41 +322,48 @@ namespace OpenRA.Mods.Common.Widgets.Logic public void RefreshServerList() { // Query in progress - if (currentQuery != null) + if (activeQuery) return; searchStatus = SearchStatus.Fetching; - Action onComplete = i => + var queryURL = new HttpQueryBuilder(services.ServerList) { - currentQuery = null; + { "protocol", GameServer.ProtocolVersion }, + { "engine", Game.EngineVersion }, + { "mod", Game.ModData.Manifest.Id }, + { "version", Game.ModData.Manifest.Metadata.Version } + }.ToString(); - List games = null; - if (i.Error == null) + Task.Run(async () => + { + var games = new List(); + var client = HttpClientFactory.Create(); + var httpResponseMessage = await client.GetAsync(queryURL); + var result = await httpResponseMessage.Content.ReadAsStringAsync(); + + activeQuery = true; + + try { - games = new List(); - try + var yaml = MiniYaml.FromString(result); + foreach (var node in yaml) { - var data = Encoding.UTF8.GetString(i.Result); - var yaml = MiniYaml.FromString(data); - foreach (var node in yaml) + try { - try - { - var gs = new GameServer(node.Value); - if (gs.Address != null) - games.Add(gs); - } - catch - { - // Ignore any invalid games advertised. - } + var gs = new GameServer(node.Value); + if (gs.Address != null) + games.Add(gs); + } + catch + { + // Ignore any invalid games advertised. } } - catch - { - searchStatus = SearchStatus.Failed; - } + } + catch + { + searchStatus = SearchStatus.Failed; } var lanGames = new List(); @@ -398,15 +405,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic games = groupedLanGames.ToList(); Game.RunAfterTick(() => RefreshServerListInner(games)); - }; - var queryURL = services.ServerList + "?protocol={0}&engine={1}&mod={2}&version={3}".F( - GameServer.ProtocolVersion, - Uri.EscapeUriString(Game.EngineVersion), - Uri.EscapeUriString(Game.ModData.Manifest.Id), - Uri.EscapeUriString(Game.ModData.Manifest.Metadata.Version)); - - currentQuery = new Download(queryURL, _ => { }, onComplete); + activeQuery = false; + }); } int GroupSortOrder(GameServer testEntry)