diff --git a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs index eed4de88f..255b94078 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/BotController.cs @@ -31,10 +31,10 @@ using ArchiSteamFarm.IPC.Requests; using ArchiSteamFarm.IPC.Responses; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam; -using ArchiSteamFarm.Steam.Integration.Callbacks; using ArchiSteamFarm.Steam.Storage; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json.Linq; +using SteamKit2; namespace ArchiSteamFarm.IPC.Controllers.Api; @@ -318,7 +318,7 @@ public sealed class BotController : ArchiController { /// [Consumes("application/json")] [HttpPost("{botNames:required}/Redeem")] - [ProducesResponseType(typeof(GenericResponse>>), (int) HttpStatusCode.OK)] + [ProducesResponseType(typeof(GenericResponse>>), (int) HttpStatusCode.OK)] [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)] public async Task> RedeemPost(string botNames, [FromBody] BotRedeemRequest request) { if (string.IsNullOrEmpty(botNames)) { @@ -339,14 +339,14 @@ public sealed class BotController : ArchiController { return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.BotNotFound, botNames))); } - IList results = await Utilities.InParallel(bots.Select(bot => request.KeysToRedeem.Select(key => bot.Actions.RedeemKey(key))).SelectMany(static task => task)).ConfigureAwait(false); + IList results = await Utilities.InParallel(bots.Select(bot => request.KeysToRedeem.Select(key => bot.Actions.RedeemKey(key))).SelectMany(static task => task)).ConfigureAwait(false); - Dictionary> result = new(bots.Count, Bot.BotsComparer); + Dictionary> result = new(bots.Count, Bot.BotsComparer); int count = 0; foreach (Bot bot in bots) { - Dictionary responses = new(request.KeysToRedeem.Count, StringComparer.Ordinal); + Dictionary responses = new(request.KeysToRedeem.Count, StringComparer.Ordinal); result[bot.BotName] = responses; foreach (string key in request.KeysToRedeem) { @@ -354,7 +354,7 @@ public sealed class BotController : ArchiController { } } - return Ok(new GenericResponse>>(result.Values.SelectMany(static responses => responses.Values).All(static value => value != null), result)); + return Ok(new GenericResponse>>(result.Values.SelectMany(static responses => responses.Values).All(static value => value != null), result)); } /// diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index c1e516a59..c976a13a0 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -3181,7 +3181,7 @@ public sealed class Bot : IAsyncDisposable { } // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - PurchaseResponseCallback? result = await Actions.RedeemKey(key!).ConfigureAwait(false); + SteamApps.PurchaseResponseCallback? result = await Actions.RedeemKey(key!).ConfigureAwait(false); if (result == null) { continue; @@ -3201,7 +3201,9 @@ public sealed class Bot : IAsyncDisposable { } } - ArchiLogger.LogGenericDebug(result.Items?.Count > 0 ? string.Format(CultureInfo.CurrentCulture, Strings.BotRedeemWithItems, key, $"{result.Result}/{result.PurchaseResultDetail}", string.Join(", ", result.Items)) : string.Format(CultureInfo.CurrentCulture, Strings.BotRedeem, key, $"{result.Result}/{result.PurchaseResultDetail}")); + Dictionary? items = result.ParseItems(); + + ArchiLogger.LogGenericDebug(items?.Count > 0 ? string.Format(CultureInfo.CurrentCulture, Strings.BotRedeemWithItems, key, $"{result.Result}/{result.PurchaseResultDetail}", string.Join(", ", items)) : string.Format(CultureInfo.CurrentCulture, Strings.BotRedeem, key, $"{result.Result}/{result.PurchaseResultDetail}")); bool rateLimited = false; bool redeemed = false; @@ -3239,11 +3241,11 @@ public sealed class Bot : IAsyncDisposable { // If user omitted the name or intentionally provided the same name as key, replace it with the Steam result // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - if (name!.Equals(key, StringComparison.OrdinalIgnoreCase) && (result.Items?.Count > 0)) { - name = string.Join(", ", result.Items.Values); + if (name!.Equals(key, StringComparison.OrdinalIgnoreCase) && (items?.Count > 0)) { + name = string.Join(", ", items.Values); } - string logEntry = $"{name}{DefaultBackgroundKeysRedeemerSeparator}[{result.PurchaseResultDetail}]{(result.Items?.Count > 0 ? $"{DefaultBackgroundKeysRedeemerSeparator}{string.Join(", ", result.Items)}" : "")}{DefaultBackgroundKeysRedeemerSeparator}{key}"; + string logEntry = $"{name}{DefaultBackgroundKeysRedeemerSeparator}[{result.PurchaseResultDetail}]{(items?.Count > 0 ? $"{DefaultBackgroundKeysRedeemerSeparator}{string.Join(", ", items)}" : "")}{DefaultBackgroundKeysRedeemerSeparator}{key}"; string filePath = GetFilePath(redeemed ? EFileType.KeysToRedeemUsed : EFileType.KeysToRedeemUnused); diff --git a/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs index ce626ca33..d04c79eef 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs @@ -157,16 +157,6 @@ public sealed class ArchiHandler : ClientMsgHandler { ClientMsgProtobuf playingSessionState = new(packetMsg); Client.PostCallback(new PlayingSessionStateCallback(packetMsg.TargetJobID, playingSessionState.Body)); - break; - case EMsg.ClientPurchaseResponse: - ClientMsgProtobuf purchaseResponse = new(packetMsg); - Client.PostCallback(new PurchaseResponseCallback(packetMsg.TargetJobID, purchaseResponse.Body)); - - break; - case EMsg.ClientRedeemGuestPassResponse: - ClientMsgProtobuf redeemGuestPassResponse = new(packetMsg); - Client.PostCallback(new RedeemGuestPassResponseCallback(packetMsg.TargetJobID, redeemGuestPassResponse.Body)); - break; case EMsg.ClientSharedLibraryLockStatus: ClientMsgProtobuf sharedLibraryLockStatus = new(packetMsg); @@ -566,7 +556,7 @@ public sealed class ArchiHandler : ClientMsgHandler { Client.Send(request); } - internal async Task RedeemGuestPass(ulong guestPassID) { + internal async Task RedeemGuestPass(ulong guestPassID) { if (guestPassID == 0) { throw new ArgumentOutOfRangeException(nameof(guestPassID)); } @@ -587,7 +577,7 @@ public sealed class ArchiHandler : ClientMsgHandler { Client.Send(request); try { - return await new AsyncJob(Client, request.SourceJobID).ToLongRunningTask().ConfigureAwait(false); + return await new AsyncJob(Client, request.SourceJobID).ToLongRunningTask().ConfigureAwait(false); } catch (Exception e) { ArchiLogger.LogGenericException(e); @@ -595,7 +585,7 @@ public sealed class ArchiHandler : ClientMsgHandler { } } - internal async Task RedeemKey(string key) { + internal async Task RedeemKey(string key) { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } @@ -616,7 +606,7 @@ public sealed class ArchiHandler : ClientMsgHandler { Client.Send(request); try { - return await new AsyncJob(Client, request.SourceJobID).ToLongRunningTask().ConfigureAwait(false); + return await new AsyncJob(Client, request.SourceJobID).ToLongRunningTask().ConfigureAwait(false); } catch (Exception e) { ArchiLogger.LogGenericException(e); @@ -796,23 +786,6 @@ public sealed class ArchiHandler : ClientMsgHandler { } } - internal sealed class RedeemGuestPassResponseCallback : CallbackMsg { - internal readonly EResult Result; - - internal RedeemGuestPassResponseCallback(JobID jobID, CMsgClientRedeemGuestPassResponse msg) { - if (jobID == null) { - throw new ArgumentNullException(nameof(jobID)); - } - - if (msg == null) { - throw new ArgumentNullException(nameof(msg)); - } - - JobID = jobID; - Result = (EResult) msg.eresult; - } - } - internal sealed class SharedLibraryLockStatusCallback : CallbackMsg { internal readonly ulong LibraryLockedBySteamID; diff --git a/ArchiSteamFarm/Steam/Integration/Callbacks/PurchaseResponseCallback.cs b/ArchiSteamFarm/Steam/Integration/SteamUtilities.cs similarity index 51% rename from ArchiSteamFarm/Steam/Integration/Callbacks/PurchaseResponseCallback.cs rename to ArchiSteamFarm/Steam/Integration/SteamUtilities.cs index f7a2e7283..17bdb9647 100644 --- a/ArchiSteamFarm/Steam/Integration/Callbacks/PurchaseResponseCallback.cs +++ b/ArchiSteamFarm/Steam/Integration/SteamUtilities.cs @@ -21,74 +21,25 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.IO; using System.Net; using ArchiSteamFarm.Core; -using JetBrains.Annotations; using SteamKit2; -using SteamKit2.Internal; -namespace ArchiSteamFarm.Steam.Integration.Callbacks; +namespace ArchiSteamFarm.Steam.Integration; -public sealed class PurchaseResponseCallback : CallbackMsg { - [PublicAPI] - public Dictionary? Items { get; } - - public EPurchaseResultDetail PurchaseResultDetail { get; internal set; } - - [PublicAPI] - public EResult Result { get; internal set; } - - internal PurchaseResponseCallback(EResult result, EPurchaseResultDetail purchaseResult) { - if (!Enum.IsDefined(typeof(EResult), result)) { - throw new InvalidEnumArgumentException(nameof(result), (int) result, typeof(EResult)); +internal static class SteamUtilities { + internal static Dictionary? ParseItems(this SteamApps.PurchaseResponseCallback callback) { + if (callback == null) { + throw new ArgumentNullException(nameof(callback)); } - if (!Enum.IsDefined(typeof(EPurchaseResultDetail), purchaseResult)) { - throw new InvalidEnumArgumentException(nameof(purchaseResult), (int) purchaseResult, typeof(EPurchaseResultDetail)); - } - - Result = result; - PurchaseResultDetail = purchaseResult; - } - - internal PurchaseResponseCallback(JobID jobID, CMsgClientPurchaseResponse msg) { - if (jobID == null) { - throw new ArgumentNullException(nameof(jobID)); - } - - if (msg == null) { - throw new ArgumentNullException(nameof(msg)); - } - - JobID = jobID; - PurchaseResultDetail = (EPurchaseResultDetail) msg.purchase_result_details; - Result = (EResult) msg.eresult; - - if (msg.purchase_receipt_info == null) { - ASF.ArchiLogger.LogNullError(nameof(msg.purchase_receipt_info)); - - return; - } - - KeyValue receiptInfo = new(); - - using (MemoryStream ms = new(msg.purchase_receipt_info)) { - if (!receiptInfo.TryReadAsBinary(ms)) { - ASF.ArchiLogger.LogNullError(nameof(ms)); - - return; - } - } - - List lineItems = receiptInfo["lineitems"].Children; + List lineItems = callback.PurchaseReceiptInfo["lineitems"].Children; if (lineItems.Count == 0) { - return; + return null; } - Items = new Dictionary(lineItems.Count); + Dictionary result = new(lineItems.Count); foreach (KeyValue lineItem in lineItems) { uint packageID = lineItem["PackageID"].AsUnsignedInteger(); @@ -101,7 +52,7 @@ public sealed class PurchaseResponseCallback : CallbackMsg { if (packageID == 0) { ASF.ArchiLogger.LogNullError(nameof(packageID)); - return; + return null; } } @@ -110,12 +61,14 @@ public sealed class PurchaseResponseCallback : CallbackMsg { if (string.IsNullOrEmpty(gameName)) { ASF.ArchiLogger.LogNullError(nameof(gameName)); - return; + return null; } // Apparently steam expects client to decode sent HTML gameName = WebUtility.HtmlDecode(gameName); - Items[packageID] = gameName; + result[packageID] = gameName; } + + return result; } } diff --git a/ArchiSteamFarm/Steam/Interaction/Actions.cs b/ArchiSteamFarm/Steam/Interaction/Actions.cs index a7ee3d1af..f2103de2c 100644 --- a/ArchiSteamFarm/Steam/Interaction/Actions.cs +++ b/ArchiSteamFarm/Steam/Interaction/Actions.cs @@ -33,8 +33,6 @@ using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Exchange; -using ArchiSteamFarm.Steam.Integration; -using ArchiSteamFarm.Steam.Integration.Callbacks; using ArchiSteamFarm.Steam.Security; using ArchiSteamFarm.Steam.Storage; using ArchiSteamFarm.Storage; @@ -243,7 +241,7 @@ public sealed class Actions : IAsyncDisposable { } [PublicAPI] - public async Task RedeemKey(string key) { + public async Task RedeemKey(string key) { await LimitGiftsRequestsAsync().ConfigureAwait(false); return await Bot.ArchiHandler.RedeemKey(key).ConfigureAwait(false); @@ -494,7 +492,7 @@ public sealed class Actions : IAsyncDisposable { Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.BotAcceptingGift, guestPassID)); await LimitGiftsRequestsAsync().ConfigureAwait(false); - ArchiHandler.RedeemGuestPassResponseCallback? response = await Bot.ArchiHandler.RedeemGuestPass(guestPassID).ConfigureAwait(false); + SteamApps.RedeemGuestPassResponseCallback? response = await Bot.ArchiHandler.RedeemGuestPass(guestPassID).ConfigureAwait(false); if (response != null) { if (response.Result == EResult.OK) { diff --git a/ArchiSteamFarm/Steam/Interaction/Commands.cs b/ArchiSteamFarm/Steam/Interaction/Commands.cs index c7028dde9..15ac7d4c2 100644 --- a/ArchiSteamFarm/Steam/Interaction/Commands.cs +++ b/ArchiSteamFarm/Steam/Interaction/Commands.cs @@ -34,7 +34,6 @@ using ArchiSteamFarm.Plugins; using ArchiSteamFarm.Steam.Cards; using ArchiSteamFarm.Steam.Data; using ArchiSteamFarm.Steam.Integration; -using ArchiSteamFarm.Steam.Integration.Callbacks; using ArchiSteamFarm.Steam.Storage; using ArchiSteamFarm.Storage; using JetBrains.Annotations; @@ -2546,49 +2545,59 @@ public sealed class Commands { } else { bool skipRequest = triedBots.Contains(currentBot) || rateLimitedBots.Contains(currentBot); - // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - PurchaseResponseCallback? result = skipRequest ? new PurchaseResponseCallback(EResult.Fail, EPurchaseResultDetail.CancelledByUser) : await currentBot.Actions.RedeemKey(key!).ConfigureAwait(false); + EResult result = EResult.Fail; + EPurchaseResultDetail purchaseResultDetail = EPurchaseResultDetail.CancelledByUser; + Dictionary? items = null; - if (result == null) { - response.AppendLine(FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotRedeem, key, EPurchaseResultDetail.Timeout), currentBot.BotName)); + if (!skipRequest) { + // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework + SteamApps.PurchaseResponseCallback? redeemResult = await currentBot.Actions.RedeemKey(key!).ConfigureAwait(false); + + result = redeemResult?.Result ?? EResult.Timeout; + purchaseResultDetail = redeemResult?.PurchaseResultDetail ?? EPurchaseResultDetail.Timeout; + items = redeemResult?.ParseItems(); + } + + if ((result == EResult.Timeout) || (purchaseResultDetail == EPurchaseResultDetail.Timeout)) { + response.AppendLine(FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotRedeem, key, $"{result}/{purchaseResultDetail}"), currentBot.BotName)); // Either bot will be changed, or loop aborted currentBot = null; } else { triedBots.Add(currentBot); - if ((result.PurchaseResultDetail == EPurchaseResultDetail.CannotRedeemCodeFromClient) || ((result.PurchaseResultDetail == EPurchaseResultDetail.BadActivationCode) && assumeWalletKeyOnBadActivationCode)) { + if ((purchaseResultDetail == EPurchaseResultDetail.CannotRedeemCodeFromClient) || ((purchaseResultDetail == EPurchaseResultDetail.BadActivationCode) && assumeWalletKeyOnBadActivationCode)) { if (Bot.WalletCurrency != ECurrencyCode.Invalid) { // If it's a wallet code, we try to redeem it first, then handle the inner result as our primary one // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework (EResult Result, EPurchaseResultDetail? PurchaseResult)? walletResult = await currentBot.ArchiWebHandler.RedeemWalletKey(key!).ConfigureAwait(false); if (walletResult != null) { - result.Result = walletResult.Value.Result; - result.PurchaseResultDetail = walletResult.Value.PurchaseResult.GetValueOrDefault(walletResult.Value.Result == EResult.OK ? EPurchaseResultDetail.NoDetail : EPurchaseResultDetail.CannotRedeemCodeFromClient); + result = walletResult.Value.Result; + purchaseResultDetail = walletResult.Value.PurchaseResult.GetValueOrDefault(walletResult.Value.Result == EResult.OK ? EPurchaseResultDetail.NoDetail : EPurchaseResultDetail.CannotRedeemCodeFromClient); } else { - result.Result = EResult.Timeout; - result.PurchaseResultDetail = EPurchaseResultDetail.Timeout; + result = EResult.Timeout; + purchaseResultDetail = EPurchaseResultDetail.Timeout; } } else { // We're unable to redeem this code from the client due to missing currency information - result.PurchaseResultDetail = EPurchaseResultDetail.CannotRedeemCodeFromClient; + purchaseResultDetail = EPurchaseResultDetail.CannotRedeemCodeFromClient; } } - if (result.Items?.Count > 0) { - response.AppendLine(FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotRedeemWithItems, key, $"{result.Result}/{result.PurchaseResultDetail}", string.Join(", ", result.Items)), currentBot.BotName)); + if (items?.Count > 0) { + response.AppendLine(FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotRedeemWithItems, key, $"{result}/{purchaseResultDetail}", string.Join(", ", items)), currentBot.BotName)); } else if (!skipRequest) { - response.AppendLine(FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotRedeem, key, $"{result.Result}/{result.PurchaseResultDetail}"), currentBot.BotName)); + response.AppendLine(FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotRedeem, key, $"{result}/{purchaseResultDetail}"), currentBot.BotName)); } - switch (result.PurchaseResultDetail) { + switch (purchaseResultDetail) { case EPurchaseResultDetail.BadActivationCode: case EPurchaseResultDetail.CannotRedeemCodeFromClient: case EPurchaseResultDetail.DuplicateActivationCode: case EPurchaseResultDetail.NoDetail: // OK case EPurchaseResultDetail.Timeout: - if ((result.Result != EResult.Timeout) && (result.PurchaseResultDetail != EPurchaseResultDetail.Timeout)) { + if ((result != EResult.Timeout) && (purchaseResultDetail != EPurchaseResultDetail.Timeout)) { // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework unusedKeys.Remove(key!); } @@ -2596,7 +2605,7 @@ public sealed class Commands { // Next key key = keysEnumerator.MoveNext() ? keysEnumerator.Current : null; - if (result.PurchaseResultDetail == EPurchaseResultDetail.NoDetail) { + if (purchaseResultDetail == EPurchaseResultDetail.NoDetail) { // Next bot (if needed) break; } @@ -2608,7 +2617,7 @@ public sealed class Commands { case EPurchaseResultDetail.CancelledByUser: case EPurchaseResultDetail.DoesNotOwnRequiredApp: case EPurchaseResultDetail.RestrictedCountry: - if (!forward || (keepMissingGames && (result.PurchaseResultDetail != EPurchaseResultDetail.AlreadyPurchased))) { + if (!forward || (keepMissingGames && (purchaseResultDetail != EPurchaseResultDetail.AlreadyPurchased))) { // Next key key = keysEnumerator.MoveNext() ? keysEnumerator.Current : null; @@ -2621,15 +2630,15 @@ public sealed class Commands { break; } - Dictionary items = result.Items ?? new Dictionary(); + items ??= new Dictionary(); bool alreadyHandled = false; foreach (Bot innerBot in Bot.Bots.Where(bot => (bot.Value != currentBot) && (!redeemFlags.HasFlag(ERedeemFlags.SkipInitial) || (bot.Value != Bot)) && !triedBots.Contains(bot.Value) && !rateLimitedBots.Contains(bot.Value) && bot.Value.IsConnectedAndLoggedOn && bot.Value.Commands.Bot.HasAccess(steamID, BotConfig.EAccess.Operator) && ((items.Count == 0) || items.Keys.Any(packageID => !bot.Value.OwnedPackageIDs.ContainsKey(packageID)))).OrderBy(static bot => bot.Key, Bot.BotsComparer).Select(static bot => bot.Value)) { // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - PurchaseResponseCallback? otherResult = await innerBot.Actions.RedeemKey(key!).ConfigureAwait(false); + SteamApps.PurchaseResponseCallback? redeemResult = await innerBot.Actions.RedeemKey(key!).ConfigureAwait(false); - if (otherResult == null) { + if (redeemResult == null) { response.AppendLine(FormatBotResponse(string.Format(CultureInfo.CurrentCulture, Strings.BotRedeem, key, $"{EResult.Timeout}/{EPurchaseResultDetail.Timeout}"), innerBot.BotName)); continue; @@ -2637,7 +2646,7 @@ public sealed class Commands { triedBots.Add(innerBot); - switch (otherResult.PurchaseResultDetail) { + switch (redeemResult.PurchaseResultDetail) { case EPurchaseResultDetail.BadActivationCode: case EPurchaseResultDetail.DuplicateActivationCode: case EPurchaseResultDetail.NoDetail: // OK @@ -2654,17 +2663,19 @@ public sealed class Commands { break; } - response.AppendLine(FormatBotResponse(otherResult.Items?.Count > 0 ? string.Format(CultureInfo.CurrentCulture, Strings.BotRedeemWithItems, key, $"{otherResult.Result}/{otherResult.PurchaseResultDetail}", string.Join(", ", otherResult.Items)) : string.Format(CultureInfo.CurrentCulture, Strings.BotRedeem, key, $"{otherResult.Result}/{otherResult.PurchaseResultDetail}"), innerBot.BotName)); + Dictionary? redeemItems = redeemResult.ParseItems(); + + response.AppendLine(FormatBotResponse(redeemItems?.Count > 0 ? string.Format(CultureInfo.CurrentCulture, Strings.BotRedeemWithItems, key, $"{redeemResult.Result}/{redeemResult.PurchaseResultDetail}", string.Join(", ", redeemItems)) : string.Format(CultureInfo.CurrentCulture, Strings.BotRedeem, key, $"{redeemResult.Result}/{redeemResult.PurchaseResultDetail}"), innerBot.BotName)); if (alreadyHandled) { break; } - if (otherResult.Items == null) { + if (redeemItems == null) { continue; } - foreach ((uint packageID, string packageName) in otherResult.Items.Where(item => !items.ContainsKey(item.Key))) { + foreach ((uint packageID, string packageName) in redeemItems.Where(item => !items.ContainsKey(item.Key))) { items[packageID] = packageName; } } @@ -2679,7 +2690,7 @@ public sealed class Commands { goto case EPurchaseResultDetail.CancelledByUser; default: - ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(result.PurchaseResultDetail), result.PurchaseResultDetail)); + ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.WarningUnknownValuePleaseReport, nameof(purchaseResultDetail), purchaseResultDetail)); // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework unusedKeys.Remove(key!);