Skip to content

Commit

Permalink
Try to address #3362
Browse files Browse the repository at this point in the history
  • Loading branch information
JustArchi committed Dec 17, 2024
1 parent dd7ae58 commit 054a317
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 82 deletions.
1 change: 1 addition & 0 deletions ArchiSteamFarm.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions ArchiSteamFarm/Steam/Bot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2101,6 +2101,7 @@ private void Disconnect(bool reconnect = false) {
}

private void DisposeShared() {
ArchiHandler.Dispose();
ArchiWebHandler.Dispose();
BotDatabase.Dispose();
ConnectionSemaphore.Dispose();
Expand Down
173 changes: 91 additions & 82 deletions ArchiSteamFarm/Steam/Integration/ArchiHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -49,11 +50,11 @@

namespace ArchiSteamFarm.Steam.Integration;

public sealed class ArchiHandler : ClientMsgHandler {
public sealed class ArchiHandler : ClientMsgHandler, IDisposable {

Check warning on line 53 in ArchiSteamFarm/Steam/Integration/ArchiHandler.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

RoslynAnalyzers Consider making public types internal

Because an application's API isn't typically referenced from outside the assembly, types can be made internal
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;
Expand Down Expand Up @@ -87,6 +88,8 @@ internal ArchiHandler(ArchiLogger archiLogger, SteamUnifiedMessages steamUnified
UnifiedTwoFactorService = steamUnifiedMessages.CreateService<TwoFactor>();
}

public void Dispose() => InventorySemaphore.Dispose();

[PublicAPI]
public async Task<bool> AddFriend(ulong steamID) {
if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
Expand Down Expand Up @@ -208,111 +211,117 @@ public async IAsyncEnumerable<Asset> GetMyInventoryAsync(uint appID = Asset.Stea

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) {

Check warning on line 228 in ArchiSteamFarm/Steam/Integration/ArchiHandler.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

RoslynAnalyzers Do not catch general exception types

Modify 'GetMyInventoryAsync' to catch a more specific allowed exception type, or rethrow the exception
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<ulong>((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<ulong>((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();
}
}

Expand Down

0 comments on commit 054a317

Please sign in to comment.