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();
}
}