From 33c9aeda21fda2d8e4d64b784aca024c311d71a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Domeradzki?= Date: Sun, 6 Oct 2024 15:14:48 +0200 Subject: [PATCH] Closes #3304 --- ArchiSteamFarm/Steam/Bot.cs | 46 +++++++++++++++++++- ArchiSteamFarm/Steam/Interaction/Actions.cs | 29 ++++++++++++ ArchiSteamFarm/Steam/Interaction/Commands.cs | 22 +--------- ArchiSteamFarm/Steam/Storage/BotConfig.cs | 5 ++- 4 files changed, 79 insertions(+), 23 deletions(-) diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index a5f7c495b171f..f77449df657a4 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -183,6 +183,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable { private readonly ConcurrentHashSet SteamFamilySharingIDs = []; private readonly SteamUser SteamUser; private readonly Trading Trading; + private readonly SemaphoreSlim UnpackBoosterPacksSemaphore = new(1, 1); private IEnumerable<(string FilePath, EFileType FileType)> RelatedFiles { get { @@ -315,6 +316,7 @@ private set { private SteamSaleEvent? SteamSaleEvent; private Timer? TradeCheckTimer; private string? TwoFactorCode; + private bool UnpackBoosterPacksScheduled; private Bot(string botName, BotConfig botConfig, BotDatabase botDatabase) { ArgumentException.ThrowIfNullOrEmpty(botName); @@ -415,6 +417,7 @@ public void Dispose() { RefreshWebSessionSemaphore.Dispose(); SendCompleteTypesSemaphore.Dispose(); Trading.Dispose(); + UnpackBoosterPacksSemaphore.Dispose(); Actions.Dispose(); CardsFarmer.Dispose(); @@ -442,6 +445,7 @@ public async ValueTask DisposeAsync() { RefreshWebSessionSemaphore.Dispose(); SendCompleteTypesSemaphore.Dispose(); Trading.Dispose(); + UnpackBoosterPacksSemaphore.Dispose(); await Actions.DisposeAsync().ConfigureAwait(false); await CardsFarmer.DisposeAsync().ConfigureAwait(false); @@ -3121,7 +3125,21 @@ private void OnInventoryChanged() { Utilities.InBackground(ArchiWebHandler.MarkInventory); } - if (BotConfig.CompleteTypesToSend.Count > 0) { + // The following actions should be synchronized, as they modify the state of the inventory + if (BotConfig.FarmingPreferences.HasFlag(BotConfig.EFarmingPreferences.AutoUnpackBoosterPacks)) { + Utilities.InBackground( + async () => { + if (!await UnpackBoosterPacks().ConfigureAwait(false)) { + // Another task is already in progress, so it'll handle the actions below as well + return; + } + + if (BotConfig.CompleteTypesToSend.Count > 0) { + await SendCompletedSets().ConfigureAwait(false); + } + } + ); + } else if (BotConfig.CompleteTypesToSend.Count > 0) { Utilities.InBackground(SendCompletedSets); } } @@ -3923,6 +3941,32 @@ private void StopRefreshTokensTimer() { RefreshTokensTimer = null; } + private async Task UnpackBoosterPacks() { + // ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that + lock (UnpackBoosterPacksSemaphore) { + if (UnpackBoosterPacksScheduled) { + return false; + } + + UnpackBoosterPacksScheduled = true; + } + + await UnpackBoosterPacksSemaphore.WaitAsync().ConfigureAwait(false); + + try { + // ReSharper disable once SuspiciousLockOverSynchronizationPrimitive - this is not a mistake, we need extra synchronization, and we can re-use the semaphore object for that + lock (UnpackBoosterPacksSemaphore) { + UnpackBoosterPacksScheduled = false; + } + + await Actions.UnpackBoosterPacks().ConfigureAwait(false); + } finally { + UnpackBoosterPacksSemaphore.Release(); + } + + return true; + } + private void UpdateTokens(string accessToken, string? refreshToken = null) { ArgumentException.ThrowIfNullOrEmpty(accessToken); diff --git a/ArchiSteamFarm/Steam/Interaction/Actions.cs b/ArchiSteamFarm/Steam/Interaction/Actions.cs index 31e0fcf3b9b21..c973a34859880 100644 --- a/ArchiSteamFarm/Steam/Interaction/Actions.cs +++ b/ArchiSteamFarm/Steam/Interaction/Actions.cs @@ -476,6 +476,35 @@ static async () => { return (true, Strings.Done); } + [PublicAPI] + public async Task UnpackBoosterPacks() { + if (!Bot.IsConnectedAndLoggedOn) { + return false; + } + + // It'd make sense here to actually check return code of ArchiWebHandler.UnpackBooster(), but it lies most of the time | https://github.com/JustArchi/ArchiSteamFarm/issues/704 + bool result = true; + + // It'd also make sense to run all of this in parallel, but it seems that Steam has a lot of problems with inventory-related parallel requests | https://steamcommunity.com/groups/archiasf/discussions/1/3559414588264550284/ + try { + await foreach (Asset item in Bot.ArchiHandler.GetMyInventoryAsync().Where(static item => item.Type == EAssetType.BoosterPack).ConfigureAwait(false)) { + if (!await Bot.ArchiWebHandler.UnpackBooster(item.RealAppID, item.AssetID).ConfigureAwait(false)) { + result = false; + } + } + } catch (TimeoutException e) { + Bot.ArchiLogger.LogGenericWarningException(e); + + return false; + } catch (Exception e) { + Bot.ArchiLogger.LogGenericException(e); + + return false; + } + + return result; + } + [PublicAPI] public static async Task<(bool Success, string? Message, Version? Version)> Update(GlobalConfig.EUpdateChannel? channel = null, bool forced = false) { if (channel.HasValue && !Enum.IsDefined(channel.Value)) { diff --git a/ArchiSteamFarm/Steam/Interaction/Commands.cs b/ArchiSteamFarm/Steam/Interaction/Commands.cs index f69d02bd235f5..06964fab62339 100644 --- a/ArchiSteamFarm/Steam/Interaction/Commands.cs +++ b/ArchiSteamFarm/Steam/Interaction/Commands.cs @@ -3418,27 +3418,9 @@ internal void OnNewLicenseList() { return FormatBotResponse(Strings.BotNotConnected); } - // It'd make sense here to actually check return code of ArchiWebHandler.UnpackBooster(), but it lies most of the time | https://github.com/JustArchi/ArchiSteamFarm/issues/704 - bool completeSuccess = true; - - // It'd also make sense to run all of this in parallel, but it seems that Steam has a lot of problems with inventory-related parallel requests | https://steamcommunity.com/groups/archiasf/discussions/1/3559414588264550284/ - try { - await foreach (Asset item in Bot.ArchiHandler.GetMyInventoryAsync().Where(static item => item.Type == EAssetType.BoosterPack).ConfigureAwait(false)) { - if (!await Bot.ArchiWebHandler.UnpackBooster(item.RealAppID, item.AssetID).ConfigureAwait(false)) { - completeSuccess = false; - } - } - } catch (TimeoutException e) { - Bot.ArchiLogger.LogGenericWarningException(e); - - completeSuccess = false; - } catch (Exception e) { - Bot.ArchiLogger.LogGenericException(e); - - completeSuccess = false; - } + bool result = await Bot.Actions.UnpackBoosterPacks().ConfigureAwait(false); - return FormatBotResponse(completeSuccess ? Strings.Success : Strings.Done); + return FormatBotResponse(result ? Strings.Success : Strings.Done); } private static async Task ResponseUnpackBoosters(EAccess access, string botNames, ulong steamID = 0) { diff --git a/ArchiSteamFarm/Steam/Storage/BotConfig.cs b/ArchiSteamFarm/Steam/Storage/BotConfig.cs index a7805b8625e03..8018d7b8cd58f 100644 --- a/ArchiSteamFarm/Steam/Storage/BotConfig.cs +++ b/ArchiSteamFarm/Steam/Storage/BotConfig.cs @@ -641,7 +641,7 @@ public enum EFarmingOrder : byte { [Flags] [PublicAPI] - public enum EFarmingPreferences : byte { + public enum EFarmingPreferences : ushort { None = 0, FarmingPausedByDefault = 1, ShutdownOnFarmingFinished = 2, @@ -651,7 +651,8 @@ public enum EFarmingPreferences : byte { SkipUnplayedGames = 32, EnableRiskyCardsDiscovery = 64, AutoSteamSaleEvent = 128, - All = FarmingPausedByDefault | ShutdownOnFarmingFinished | SendOnFarmingFinished | FarmPriorityQueueOnly | SkipRefundableGames | SkipUnplayedGames | EnableRiskyCardsDiscovery | AutoSteamSaleEvent + AutoUnpackBoosterPacks = 256, + All = FarmingPausedByDefault | ShutdownOnFarmingFinished | SendOnFarmingFinished | FarmPriorityQueueOnly | SkipRefundableGames | SkipUnplayedGames | EnableRiskyCardsDiscovery | AutoSteamSaleEvent | AutoUnpackBoosterPacks } [Flags]