Try to address #3362

This commit is contained in:
Łukasz Domeradzki
2024-12-17 23:59:32 +01:00
parent dd7ae5801d
commit 054a317777
3 changed files with 102 additions and 91 deletions

View File

@@ -187,6 +187,7 @@
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToConstant_002ELocal/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToExpressionBodyWhenPossible/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToLambdaExpressionWhenPossible/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=DuplicatedSwitchSectionBodies/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=DuplicateResource/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceDoWhileStatementBraces/@EntryIndexedValue">WARNING</s:String>

View File

@@ -2101,6 +2101,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
}
private void DisposeShared() {
ArchiHandler.Dispose();
ArchiWebHandler.Dispose();
BotDatabase.Dispose();
ConnectionSemaphore.Dispose();

View File

@@ -25,6 +25,7 @@ using System;
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 @@ using EPersonaStateFlag = SteamKit2.EPersonaStateFlag;
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 @@ public sealed class ArchiHandler : ClientMsgHandler {
UnifiedTwoFactorService = steamUnifiedMessages.CreateService<TwoFactor>();
}
public void Dispose() => InventorySemaphore.Dispose();
[PublicAPI]
public async Task<bool> AddFriend(ulong steamID) {
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
@@ -208,111 +211,117 @@ public sealed class ArchiHandler : ClientMsgHandler {
Dictionary<(ulong ClassID, ulong InstanceID), InventoryDescription>? descriptions = null;
while (true) {
SteamUnifiedMessages.ServiceMethodResponse<CEcon_GetInventoryItemsWithDescriptions_Response>? 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<CEcon_GetInventoryItemsWithDescriptions_Response>? 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));
}
}
// 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));
if (response == null) {
throw new TimeoutException(Strings.FormatErrorObjectIsNull(nameof(response)));
}
if (response.Result != EResult.OK) {
throw new TimeoutException(Strings.FormatWarningFailedWithError(response.Result));
}
if ((response.Body.total_inventory_count == 0) || (response.Body.assets.Count == 0)) {
// Empty inventory
yield break;
}
if (response.Body.descriptions.Count == 0) {
throw new InvalidOperationException(nameof(response.Body.descriptions));
}
if (response.Body.total_inventory_count > Array.MaxLength) {
throw new InvalidOperationException(nameof(response.Body.total_inventory_count));
}
assetIDs ??= new HashSet<ulong>((int) 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();
}
foreach (CEconItem_Description? description in response.Body.descriptions) {
if (description.classid == 0) {
throw new NotSupportedException(Strings.FormatErrorObjectIsNull(nameof(description.classid)));
throw new TimeoutException(Strings.FormatWarningFailedWithError(response.Result));
}
}
(ulong ClassID, ulong InstanceID) key = (description.classid, description.instanceid);
if (descriptions.ContainsKey(key)) {
continue;
if (response == null) {
throw new TimeoutException(Strings.FormatErrorObjectIsNull(nameof(response)));
}
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));
// Extra bulletproofing against Steam showing us middle finger
if ((tradableOnly && (description?.Tradable != true)) || (marketableOnly && (description?.Marketable != true))) {
continue;
if (response.Result != EResult.OK) {
throw new TimeoutException(Strings.FormatWarningFailedWithError(response.Result));
}
yield return new Asset(asset, description);
}
if ((response.Body.total_inventory_count == 0) || (response.Body.assets.Count == 0)) {
// Empty inventory
yield break;
}
if (!response.Body.more_items) {
yield break;
}
if (response.Body.descriptions.Count == 0) {
throw new InvalidOperationException(nameof(response.Body.descriptions));
}
if (response.Body.last_assetid == 0) {
throw new NotSupportedException(Strings.FormatErrorObjectIsNull(nameof(response.Body.last_assetid)));
}
if (response.Body.total_inventory_count > Array.MaxLength) {
throw new InvalidOperationException(nameof(response.Body.total_inventory_count));
}
request.start_assetid = response.Body.last_assetid;
assetIDs ??= new HashSet<ulong>((int) 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();
}
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;
}
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));
// Extra bulletproofing against Steam showing us middle finger
if ((tradableOnly && (description?.Tradable != true)) || (marketableOnly && (description?.Marketable != true))) {
continue;
}
yield return new Asset(asset, description);
}
if (!response.Body.more_items) {
yield break;
}
if (response.Body.last_assetid == 0) {
throw new NotSupportedException(Strings.FormatErrorObjectIsNull(nameof(response.Body.last_assetid)));
}
request.start_assetid = response.Body.last_assetid;
}
} finally {
InventorySemaphore.Release();
}
}