diff --git a/ArchiSteamFarm.sln.DotSettings b/ArchiSteamFarm.sln.DotSettings index 49d46e295654d..59f75dad632ac 100644 --- a/ArchiSteamFarm.sln.DotSettings +++ b/ArchiSteamFarm.sln.DotSettings @@ -187,6 +187,7 @@ SUGGESTION SUGGESTION SUGGESTION + SUGGESTION DO_NOT_SHOW WARNING diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index b2d3e25cd34c3..cd1c8be2434d8 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -2101,6 +2101,7 @@ private void Disconnect(bool reconnect = false) { } private void DisposeShared() { + ArchiHandler.Dispose(); ArchiWebHandler.Dispose(); BotDatabase.Dispose(); ConnectionSemaphore.Dispose(); diff --git a/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs index 02f482ba85a66..0363bd6ae81c8 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs @@ -25,6 +25,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.Localization; @@ -49,11 +50,11 @@ namespace ArchiSteamFarm.Steam.Integration; -public sealed class ArchiHandler : ClientMsgHandler { +public sealed class ArchiHandler : ClientMsgHandler, IDisposable { internal const byte MaxGamesPlayedConcurrently = 32; // This is limit introduced by Steam Network private readonly ArchiLogger ArchiLogger; - + private readonly SemaphoreSlim InventorySemaphore = new(1, 1); private readonly AccountPrivateApps UnifiedAccountPrivateApps; private readonly ChatRoom UnifiedChatRoomService; private readonly ClanChatRooms UnifiedClanChatRoomsService; @@ -87,6 +88,8 @@ internal ArchiHandler(ArchiLogger archiLogger, SteamUnifiedMessages steamUnified UnifiedTwoFactorService = steamUnifiedMessages.CreateService(); } + public void Dispose() => InventorySemaphore.Dispose(); + [PublicAPI] public async Task AddFriend(ulong steamID) { if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { @@ -208,111 +211,117 @@ public async IAsyncEnumerable GetMyInventoryAsync(uint appID = Asset.Stea Dictionary<(ulong ClassID, ulong InstanceID), InventoryDescription>? descriptions = null; - while (true) { - SteamUnifiedMessages.ServiceMethodResponse? response = null; + await InventorySemaphore.WaitAsync().ConfigureAwait(false); - for (byte i = 0; (i < WebBrowser.MaxTries) && (response?.Result != EResult.OK) && Client.IsConnected && (Client.SteamID != null); i++) { - if (i > 0) { - // It seems 2 seconds is enough to win over DuplicateRequest, so we'll use that for this and also other network-related failures - await Task.Delay(2000).ConfigureAwait(false); - } + try { + while (true) { + SteamUnifiedMessages.ServiceMethodResponse? response = null; - try { - response = await UnifiedEconService.GetInventoryItemsWithDescriptions(request).ToLongRunningTask().ConfigureAwait(false); - } catch (Exception e) { - ArchiLogger.LogGenericWarningException(e); + for (byte i = 0; (i < WebBrowser.MaxTries) && (response?.Result != EResult.OK) && Client.IsConnected && (Client.SteamID != null); i++) { + if (i > 0) { + // It seems 2 seconds is enough to win over DuplicateRequest, so we'll use that for this and also other network-related failures + await Task.Delay(2000).ConfigureAwait(false); + } - continue; - } + try { + response = await UnifiedEconService.GetInventoryItemsWithDescriptions(request).ToLongRunningTask().ConfigureAwait(false); + } catch (Exception e) { + ArchiLogger.LogGenericWarningException(e); - // Interpret the result and see what we should do about it - switch (response.Result) { - case EResult.OK: - // Success, we can continue - break; - case EResult.Busy: - case EResult.DuplicateRequest: - case EResult.Fail: - case EResult.RemoteCallFailed: - case EResult.ServiceUnavailable: - case EResult.Timeout: - // Expected failures that we should be able to retry continue; - case EResult.NoMatch: - // Expected failures that we're not going to retry - throw new TimeoutException(Strings.FormatWarningFailedWithError(response.Result)); - default: - // Unknown failures, report them and do not retry since we're unsure if we should - ArchiLogger.LogGenericError(Strings.FormatWarningUnknownValuePleaseReport(nameof(response.Result), response.Result)); - - throw new TimeoutException(Strings.FormatWarningFailedWithError(response.Result)); - } - } + } - if (response == null) { - throw new TimeoutException(Strings.FormatErrorObjectIsNull(nameof(response))); - } + // Interpret the result and see what we should do about it + switch (response.Result) { + case EResult.OK: + // Success, we can continue + break; + case EResult.Busy: + case EResult.DuplicateRequest: + case EResult.Fail: + case EResult.RemoteCallFailed: + case EResult.ServiceUnavailable: + case EResult.Timeout: + // Expected failures that we should be able to retry + continue; + case EResult.NoMatch: + // Expected failures that we're not going to retry + throw new TimeoutException(Strings.FormatWarningFailedWithError(response.Result)); + default: + // Unknown failures, report them and do not retry since we're unsure if we should + ArchiLogger.LogGenericError(Strings.FormatWarningUnknownValuePleaseReport(nameof(response.Result), response.Result)); + + throw new TimeoutException(Strings.FormatWarningFailedWithError(response.Result)); + } + } - if (response.Result != EResult.OK) { - throw new TimeoutException(Strings.FormatWarningFailedWithError(response.Result)); - } + if (response == null) { + throw new TimeoutException(Strings.FormatErrorObjectIsNull(nameof(response))); + } - if ((response.Body.total_inventory_count == 0) || (response.Body.assets.Count == 0)) { - // Empty inventory - yield break; - } + if (response.Result != EResult.OK) { + throw new TimeoutException(Strings.FormatWarningFailedWithError(response.Result)); + } - if (response.Body.descriptions.Count == 0) { - throw new InvalidOperationException(nameof(response.Body.descriptions)); - } + if ((response.Body.total_inventory_count == 0) || (response.Body.assets.Count == 0)) { + // Empty inventory + yield break; + } - if (response.Body.total_inventory_count > Array.MaxLength) { - throw new InvalidOperationException(nameof(response.Body.total_inventory_count)); - } + if (response.Body.descriptions.Count == 0) { + throw new InvalidOperationException(nameof(response.Body.descriptions)); + } - assetIDs ??= new HashSet((int) response.Body.total_inventory_count); + if (response.Body.total_inventory_count > Array.MaxLength) { + throw new InvalidOperationException(nameof(response.Body.total_inventory_count)); + } - if (descriptions == null) { - descriptions = new Dictionary<(ulong ClassID, ulong InstanceID), InventoryDescription>(); - } else { - // We don't need descriptions from the previous request - descriptions.Clear(); - } + assetIDs ??= new HashSet((int) response.Body.total_inventory_count); - foreach (CEconItem_Description? description in response.Body.descriptions) { - if (description.classid == 0) { - throw new NotSupportedException(Strings.FormatErrorObjectIsNull(nameof(description.classid))); + if (descriptions == null) { + descriptions = new Dictionary<(ulong ClassID, ulong InstanceID), InventoryDescription>(); + } else { + // We don't need descriptions from the previous request + descriptions.Clear(); } - (ulong ClassID, ulong InstanceID) key = (description.classid, description.instanceid); + foreach (CEconItem_Description? description in response.Body.descriptions) { + if (description.classid == 0) { + throw new NotSupportedException(Strings.FormatErrorObjectIsNull(nameof(description.classid))); + } + + (ulong ClassID, ulong InstanceID) key = (description.classid, description.instanceid); + + if (descriptions.ContainsKey(key)) { + continue; + } - if (descriptions.ContainsKey(key)) { - continue; + descriptions.Add(key, new InventoryDescription(description)); } - descriptions.Add(key, new InventoryDescription(description)); - } + foreach (CEcon_Asset? asset in response.Body.assets.Where(asset => assetIDs.Add(asset.assetid))) { + InventoryDescription? description = descriptions.GetValueOrDefault((asset.classid, asset.instanceid)); - foreach (CEcon_Asset? asset in response.Body.assets.Where(asset => assetIDs.Add(asset.assetid))) { - InventoryDescription? description = descriptions.GetValueOrDefault((asset.classid, asset.instanceid)); + // Extra bulletproofing against Steam showing us middle finger + if ((tradableOnly && (description?.Tradable != true)) || (marketableOnly && (description?.Marketable != true))) { + continue; + } - // Extra bulletproofing against Steam showing us middle finger - if ((tradableOnly && (description?.Tradable != true)) || (marketableOnly && (description?.Marketable != true))) { - continue; + yield return new Asset(asset, description); } - yield return new Asset(asset, description); - } + if (!response.Body.more_items) { + yield break; + } - if (!response.Body.more_items) { - yield break; - } + if (response.Body.last_assetid == 0) { + throw new NotSupportedException(Strings.FormatErrorObjectIsNull(nameof(response.Body.last_assetid))); + } - if (response.Body.last_assetid == 0) { - throw new NotSupportedException(Strings.FormatErrorObjectIsNull(nameof(response.Body.last_assetid))); + request.start_assetid = response.Body.last_assetid; } - - request.start_assetid = response.Body.last_assetid; + } finally { + InventorySemaphore.Release(); } }