diff --git a/BoosterManager/BoosterManager.cs b/BoosterManager/BoosterManager.cs index be42569..113efe1 100644 --- a/BoosterManager/BoosterManager.cs +++ b/BoosterManager/BoosterManager.cs @@ -53,6 +53,11 @@ public Task OnASFInit(IReadOnlyDictionary? additionalConfig BoosterHandler.AllowCraftUntradableBoosters = configProperty.Value.GetBoolean(); break; } + case "AllowCraftUnmarketableBoosters" when (configProperty.Value.ValueKind == JsonValueKind.True || configProperty.Value.ValueKind == JsonValueKind.False): { + ASF.ArchiLogger.LogGenericInfo("Allow Craft Unmarketable Boosters : " + configProperty.Value); + BoosterHandler.AllowCraftUnmarketableBoosters = configProperty.Value.GetBoolean(); + break; + } case "BoosterDelayBetweenBots" when configProperty.Value.ValueKind == JsonValueKind.Number: { ASF.ArchiLogger.LogGenericInfo("Booster Delay Between Bots : " + configProperty.Value); BoosterHandler.UpdateBotDelays(configProperty.Value.GetInt32()); diff --git a/BoosterManager/Boosters/BoosterQueue.cs b/BoosterManager/Boosters/BoosterQueue.cs index 2d445a1..b467253 100644 --- a/BoosterManager/Boosters/BoosterQueue.cs +++ b/BoosterManager/Boosters/BoosterQueue.cs @@ -103,22 +103,35 @@ private async Task Run() { internal void AddBooster(uint gameID, BoosterType type) { void handler() { - if (BoosterInfos.TryGetValue(gameID, out Steam.BoosterInfo? boosterInfo)) { + try { + if (!BoosterInfos.TryGetValue(gameID, out Steam.BoosterInfo? boosterInfo)) { + Bot.ArchiLogger.LogGenericError(String.Format("Can't craft boosters for {0}", gameID)); + + return; + } + if (Boosters.TryGetValue(gameID, out Booster? existingBooster)) { // Re-add a booster that was successfully crafted and is waiting to be cleared out of the queue if (existingBooster.Type == BoosterType.OneTime && existingBooster.WasCrafted) { RemoveBooster(gameID); } } + + if (!BoosterHandler.AllowCraftUnmarketableBoosters && !MarketableApps.AppIDs.Contains(gameID)) { + Bot.ArchiLogger.LogGenericError(String.Format("Won't craft unmarketable boosters for {0}", gameID)); + + return; + } + Booster newBooster = new Booster(Bot, gameID, type, boosterInfo, this, GetLastCraft(gameID)); if (Boosters.TryAdd(gameID, newBooster)) { Bot.ArchiLogger.LogGenericInfo(String.Format("Added {0} to booster queue.", gameID)); } - } else { - Bot.ArchiLogger.LogGenericError(String.Format("Can't craft boosters for {0}", gameID)); + } finally { + OnBoosterInfosUpdated -= handler; } - OnBoosterInfosUpdated -= handler; } + OnBoosterInfosUpdated += handler; } @@ -144,6 +157,10 @@ private async Task UpdateBoosterInfos() { return true; } + if (!BoosterHandler.AllowCraftUnmarketableBoosters && !await MarketableApps.Update().ConfigureAwait(false)) { + return false; + } + (BoosterPageResponse? boosterPage, _) = await WebRequest.GetBoosterPage(Bot).ConfigureAwait(false); if (boosterPage == null) { Bot.ArchiLogger.LogNullError(boosterPage); @@ -171,6 +188,7 @@ private async Task CraftBooster(Booster booster) { } else { nTp = TradabilityPreference.Default; } + Steam.BoostersResponse? result = await booster.Craft(nTp).ConfigureAwait(false); GooAmount = result?.GooAmount ?? GooAmount; TradableGooAmount = result?.TradableGooAmount ?? TradableGooAmount; @@ -184,8 +202,18 @@ private void VerifyCraftBoosterError(Booster booster) { // Sometimes Steam will falsely report that an attempt to craft a booster failed, when it really didn't. It could also happen that the user crafted the booster on their own. // For any error we get, we'll need to refresh the booster page and see if the AvailableAtTime has changed to determine if we really failed to craft void handler() { - Bot.ArchiLogger.LogGenericInfo(String.Format("An error was encountered when trying to craft a booster from {0}, trying to resolve it now", booster.GameID)); - if (BoosterInfos.TryGetValue(booster.GameID, out Steam.BoosterInfo? newBoosterInfo)) { + try { + Bot.ArchiLogger.LogGenericInfo(String.Format("An error was encountered when trying to craft a booster from {0}, trying to resolve it now", booster.GameID)); + + if (!BoosterInfos.TryGetValue(booster.GameID, out Steam.BoosterInfo? newBoosterInfo)) { + // No longer have access to craft boosters for this game (game removed from account, or sometimes due to very rare Steam bugs) + BoosterHandler.PerpareStatusReport(String.Format("No longer able to craft boosters from {0} ({1})", booster.Info.Name, booster.GameID)); + RemoveBooster(booster.GameID); + CheckIfFinished(booster.Type); + + return; + } + if (newBoosterInfo.Unavailable && newBoosterInfo.AvailableAtTime != null && newBoosterInfo.AvailableAtTime != booster.Info.AvailableAtTime && ( @@ -196,17 +224,16 @@ void handler() { Bot.ArchiLogger.LogGenericInfo(String.Format("Booster from {0} was recently created either by us or by user", booster.GameID)); booster.SetWasCrafted(); CheckIfFinished(booster.Type); - } else { - Bot.ArchiLogger.LogGenericInfo(String.Format("Booster from {0} was not created, retrying", booster.GameID)); + + return; } - } else { - // No longer have access to craft boosters for this game (game removed from account, or sometimes due to very rare Steam bugs) - BoosterHandler.PerpareStatusReport(String.Format("No longer able to craft boosters from {0} ({1})", booster.Info.Name, booster.GameID)); - RemoveBooster(booster.GameID); - CheckIfFinished(booster.Type); + + Bot.ArchiLogger.LogGenericInfo(String.Format("Booster from {0} was not created, retrying", booster.GameID)); + } finally { + OnBoosterInfosUpdated -= handler; } - OnBoosterInfosUpdated -= handler; } + OnBoosterInfosUpdated += handler; } diff --git a/BoosterManager/Data/MarketableApps.cs b/BoosterManager/Data/MarketableApps.cs new file mode 100644 index 0000000..0cead51 --- /dev/null +++ b/BoosterManager/Data/MarketableApps.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using ArchiSteamFarm.Core; +using ArchiSteamFarm.Web.Responses; + +namespace BoosterManager { + internal static class MarketableApps { + internal static HashSet AppIDs = new(); + + private static Uri Source = new("https://raw.githubusercontent.com/Citrinate/Steam-MarketableApps/main/data/marketable_apps.min.json"); + private static TimeSpan UpdateFrequency = TimeSpan.FromHours(1); + private static DateTime? LastUpdate; + private static SemaphoreSlim UpdateSemaphore = new SemaphoreSlim(1, 1); + + internal static async Task Update() { + ArgumentNullException.ThrowIfNull(ASF.WebBrowser); + + await UpdateSemaphore.WaitAsync().ConfigureAwait(false); + try { + if (LastUpdate != null && (LastUpdate + UpdateFrequency) > DateTime.Now) { + // Data is still fresh + return true; + } + + // https://api.steampowered.com/ISteamApps/GetApplist/v2 can be used to get a list which includes all marketable apps and excludes all unmarketable apps + // It's however not reliable and also not perfect. At random times, tens of thousands of apps will be missing (some of which are marketable) + // Can't account for these errors whithin this plugin (in a timely fashion), and so we use a cached version of ISteamApps/GetApplist which is known to be good + + ObjectResponse>? response = await ASF.WebBrowser.UrlGetToJsonObject>(Source).ConfigureAwait(false); + if (response == null || response.Content == null) { + ASF.ArchiLogger.LogGenericDebug("Failed to fetch marketable apps data"); + + return false; + } + + AppIDs = response.Content; + LastUpdate = DateTime.Now; + + return true; + } finally { + UpdateSemaphore.Release(); + } + } + } +} diff --git a/BoosterManager/Handlers/BoosterHandler.cs b/BoosterManager/Handlers/BoosterHandler.cs index d41e803..4ff51b7 100644 --- a/BoosterManager/Handlers/BoosterHandler.cs +++ b/BoosterManager/Handlers/BoosterHandler.cs @@ -20,6 +20,7 @@ internal sealed class BoosterHandler : IDisposable { internal static ConcurrentDictionary BoosterHandlers = new(); private static int DelayBetweenBots = 0; // Delay, in minutes, between when bots will craft boosters internal static bool AllowCraftUntradableBoosters = true; + internal static bool AllowCraftUnmarketableBoosters = true; private Timer? MarketRepeatTimer = null; private BoosterHandler(Bot bot) { diff --git a/README.md b/README.md index 5cc56e5..1cafe64 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,16 @@ Command | Alias | --- +### AllowCraftUnmarketableBoosters + +`bool` type with default value of `true`. This configuration setting can be added to your `ASF.json` config file. If set to `false`, the plugin will not craft unmarketable boosters. + +```json +"AllowCraftUnmarketableBoosters": false, +``` + +--- + ### GamesToBooster `HashSet` type with default value of `[]`. This configuration setting can be added to your individual bot config files. It will automatically add all of the `AppIDs` to that bot's booster queue, and will automatically re-queue them after they've been crafted.