From 0b98608d5c4bb117dccfbee5b1abe7f22c9d15c8 Mon Sep 17 00:00:00 2001 From: Cortland Bleasel <87231282+xCortlandx@users.noreply.github.com> Date: Fri, 6 Feb 2026 06:28:51 +1100 Subject: [PATCH] Nice try Blizzard (Error 2310 fix) --- BNetInstaller/AgentClient.cs | 5 +- BNetInstaller/AgentException.cs | 24 ++++++ BNetInstaller/Endpoints/BaseEndpoint.cs | 75 ++++++++++++++++--- BNetInstaller/Models/InstallModel.cs | 6 +- BNetInstaller/Operations/AgentTask.cs | 2 +- .../Operations/InstallProductTask.cs | 26 +++++-- BNetInstaller/Program.cs | 32 +++++++- 7 files changed, 147 insertions(+), 23 deletions(-) create mode 100644 BNetInstaller/AgentException.cs diff --git a/BNetInstaller/AgentClient.cs b/BNetInstaller/AgentClient.cs index 48dfa80..8c5894c 100644 --- a/BNetInstaller/AgentClient.cs +++ b/BNetInstaller/AgentClient.cs @@ -1,4 +1,6 @@ using System.Diagnostics; +using System.Net.Http.Headers; +using System.Text; namespace BNetInstaller; @@ -11,6 +13,7 @@ public AgentClient(int port) { _client = new(); _client.DefaultRequestHeaders.Add("User-Agent", "phoenix-agent/1.0"); + _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); _client.BaseAddress = new($"http://127.0.0.1:{port}"); _serializerOptions = new() @@ -30,7 +33,7 @@ public async Task SendAsync(string endpoint, HttpMethod met var request = new HttpRequestMessage(method, endpoint); if (!string.IsNullOrEmpty(content)) - request.Content = new StringContent(content); + request.Content = new StringContent(content, Encoding.UTF8, "application/json"); var response = await _client.SendAsync(request); diff --git a/BNetInstaller/AgentException.cs b/BNetInstaller/AgentException.cs new file mode 100644 index 0000000..c155f0b --- /dev/null +++ b/BNetInstaller/AgentException.cs @@ -0,0 +1,24 @@ +using System; + +namespace BNetInstaller; + +internal sealed class AgentException : Exception +{ + public int ErrorCode { get; } + public string? ResponseContent { get; } + + public AgentException(int errorCode, string message, string? responseContent = null, Exception? inner = null) + : base(message, inner) + { + ErrorCode = errorCode; + ResponseContent = responseContent; + } + + public static string Describe(int errorCode) => errorCode switch + { + 2221 => "The supplied TACT Product is unavailable or invalid.", + 2421 => "Your computer doesn't meet the minimum specs and/or space requirements.", + 3001 => "The supplied TACT Product requires an encryption key which is missing.", + _ => "The Battle.net Agent returned an error." + }; +} diff --git a/BNetInstaller/Endpoints/BaseEndpoint.cs b/BNetInstaller/Endpoints/BaseEndpoint.cs index 0ec53fe..88d460a 100644 --- a/BNetInstaller/Endpoints/BaseEndpoint.cs +++ b/BNetInstaller/Endpoints/BaseEndpoint.cs @@ -1,4 +1,8 @@ -namespace BNetInstaller.Endpoints; +using System; +using System.Text; +using System.Text.Json.Nodes; + +namespace BNetInstaller.Endpoints; internal abstract class BaseEndpoint(string endpoint, AgentClient client) where T : class, IModel, new() { @@ -16,7 +20,7 @@ public virtual async Task Get() public virtual async Task Post() { if (Model is NullModel) - return default; + return default!; using var response = await Client.SendAsync(Endpoint, HttpMethod.Post, Model); return await Deserialize(response); @@ -25,7 +29,7 @@ public virtual async Task Post() public virtual async Task Put() { if (Model is NullModel) - return default; + return default!; using var response = await Client.SendAsync(Endpoint, HttpMethod.Put, Model); return await Deserialize(response); @@ -39,15 +43,68 @@ public virtual async Task Delete() protected async Task Deserialize(HttpResponseMessage response) { var content = await response.Content.ReadAsStringAsync(); - var result = JsonNode.Parse(content); - ValidateResponse(result, content); - return result; + + JsonNode? result = null; + try + { + result = JsonNode.Parse(content); + } + catch + { + // if bnet gives non-JSON then use the raw payload instead + throw new Exception("Agent returned a non-JSON response.", new Exception(content)); + } + + ValidateResponse(result!, content); + return result!; + } + + private static int GetErrorCode(JsonNode? node) + { + try + { + var v = node?.GetValue(); + return (int)Math.Round(v.GetValueOrDefault()); + } + catch + { + return 0; + } } protected virtual void ValidateResponse(JsonNode response, string content) { - var errorCode = response["error"]?.GetValue(); - if (errorCode > 0) - throw new Exception($"Agent Error: {errorCode}", new(content)); + var agentError = GetErrorCode(response?["error"]); + + if (agentError <= 0) + return; + + // see if bnet agent gives an error with the form payload + string? section = null; + int sectionError = 0; + + if (response?["form"] is JsonObject form) + { + foreach (var kvp in form) + { + var node = kvp.Value; + var code = GetErrorCode(node?["error"]); + if (code > 0) + { + section = kvp.Key; + sectionError = code; + break; + } + } + } + + var sb = new StringBuilder(); + sb.Append($"Agent Error: {agentError} - {AgentException.Describe(agentError)}"); + + if (!string.IsNullOrWhiteSpace(section)) + sb.Append($" (section: {section}, section error: {sectionError})"); + + // keep raw json (debug) + throw new AgentException(agentError, sb.ToString(), content, new Exception(content)); } } diff --git a/BNetInstaller/Models/InstallModel.cs b/BNetInstaller/Models/InstallModel.cs index 31000e5..300a1fa 100644 --- a/BNetInstaller/Models/InstallModel.cs +++ b/BNetInstaller/Models/InstallModel.cs @@ -1,8 +1,10 @@ -namespace BNetInstaller.Models; +using System; + +namespace BNetInstaller.Models; internal sealed class InstallModel : ProductPriorityModel { public string InstructionsPatchUrl { get; set; } public string InstructionsProduct { get; set; } = "NGDP"; - public double MonitorPid { get; set; } = 12345; + public double MonitorPid { get; set; } = Environment.ProcessId; } diff --git a/BNetInstaller/Operations/AgentTask.cs b/BNetInstaller/Operations/AgentTask.cs index 6e0c5f6..1fdd19f 100644 --- a/BNetInstaller/Operations/AgentTask.cs +++ b/BNetInstaller/Operations/AgentTask.cs @@ -16,7 +16,7 @@ internal abstract class AgentTask(Options options) protected async Task PrintProgress(ProductEndpoint endpoint) { - var locale = _options.Locale.ToString(); + var locale = _options.Locale.ToString().ToLowerInvariant(); var cursor = (Left: 0, Top: 0); if (_options.Verbose) diff --git a/BNetInstaller/Operations/InstallProductTask.cs b/BNetInstaller/Operations/InstallProductTask.cs index c5107d9..e68a837 100644 --- a/BNetInstaller/Operations/InstallProductTask.cs +++ b/BNetInstaller/Operations/InstallProductTask.cs @@ -1,4 +1,4 @@ -namespace BNetInstaller.Operations; +namespace BNetInstaller.Operations; internal sealed class InstallProductTask(Options options, AgentApp app) : AgentTask(options) { @@ -7,17 +7,31 @@ internal sealed class InstallProductTask(Options options, AgentApp app) : AgentT protected override async Task InnerTask() { - // initiate the download - _app.UpdateEndpoint.Model.Uid = _options.UID; - await _app.UpdateEndpoint.Post(); + if (await PrintProgress(_app.InstallEndpoint.Product)) + return true; + + try + { + // initiate the download + _app.UpdateEndpoint.Model.Uid = _options.UID; + await _app.UpdateEndpoint.Post(); + } + catch (AgentException ex) when (ex.ErrorCode == 2421) + { + Console.WriteLine("Agent returned 2421 (minimum specs and/or disk space requirement not met)."); + Console.WriteLine("Double-check the install directory/drive has enough free space and that the product meets OS/hardware requirements."); + } // first try the install endpoint if (await PrintProgress(_app.InstallEndpoint.Product)) return true; // then try the update endpoint instead - if (await PrintProgress(_app.UpdateEndpoint.Product)) - return true; + if (_app.UpdateEndpoint.Product != null) + { + if (await PrintProgress(_app.UpdateEndpoint.Product)) + return true; + } // failing that another agent or the BNet app has // probably taken control of the install diff --git a/BNetInstaller/Program.cs b/BNetInstaller/Program.cs index a74d001..1d2f07c 100644 --- a/BNetInstaller/Program.cs +++ b/BNetInstaller/Program.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System.Diagnostics; +using System.Globalization; using BNetInstaller.Operations; namespace BNetInstaller; @@ -21,8 +22,28 @@ private static async Task Run(Options options) using AgentApp app = new(); options.Sanitise(); - var locale = options.Locale.ToString(); - var mode = options.Repair ? Mode.Repair : Mode.Install; + // check if target directory exists before requesting bnet agent to validate + Directory.CreateDirectory(options.Directory); + + if (options.Verbose) + { + try + { + var root = Path.GetPathRoot(options.Directory); + if (!string.IsNullOrWhiteSpace(root)) + { + var drive = new DriveInfo(root); + var gib = drive.AvailableFreeSpace / (1024d * 1024d * 1024d); + Console.WriteLine($"Target drive free space: {gib.ToString("F2", CultureInfo.InvariantCulture)} GiB"); + } + } + catch + { + } + } + + var localeCode = options.Locale.ToString().ToLowerInvariant(); +var mode = options.Repair ? Mode.Repair : Mode.Install; Console.WriteLine("Authenticating"); await app.AgentEndpoint.Get(); @@ -34,6 +55,9 @@ private static async Task Run(Options options) Console.WriteLine($"Starting {mode}"); app.InstallEndpoint.Product.Model.GameDir = options.Directory; + app.InstallEndpoint.Product.Model.Language = [localeCode]; + app.InstallEndpoint.Product.Model.SelectedLocale = localeCode; + app.InstallEndpoint.Product.Model.SelectedAssetLocale = localeCode; await app.InstallEndpoint.Product.Post(); Console.WriteLine(); @@ -52,7 +76,7 @@ private static async Task Run(Options options) await app.AgentEndpoint.Delete(); // run the post download app/script if applicable - if (complete && File.Exists(options.PostDownload)) + if (complete && !string.IsNullOrWhiteSpace(options.PostDownload) && File.Exists(options.PostDownload)) Process.Start(options.PostDownload); } }