diff --git a/ArchiSteamFarm.Tests/Bot.cs b/ArchiSteamFarm.Tests/Bot.cs index f508757f7..125b85f41 100644 --- a/ArchiSteamFarm.Tests/Bot.cs +++ b/ArchiSteamFarm.Tests/Bot.cs @@ -50,6 +50,87 @@ namespace ArchiSteamFarm.Tests { AssertResultMatchesExpectation(expectedResult, itemsToSend); } + [TestMethod] + public void MaxItemsBarelyEnoughForOneSet() { + const uint relevantAppID = 42; + + Dictionary itemsPerSet = new() { + { relevantAppID, ArchiSteamFarm.Bot.MinCardsPerBadge }, + { 43, ArchiSteamFarm.Bot.MinCardsPerBadge + 1 } + }; + + HashSet items = new(); + + foreach ((uint appID, byte cards) in itemsPerSet) { + for (byte i = 1; i <= cards; i++) { + items.Add(CreateCard(i, appID)); + } + } + + HashSet itemsToSend = GetItemsForFullBadge(items, itemsPerSet, ArchiSteamFarm.Bot.MinCardsPerBadge); + + Dictionary<(uint RealAppID, ulong ContextID, ulong ClassID), uint> expectedResult = items.Where(item => item.RealAppID == relevantAppID) + .GroupBy(item => (item.RealAppID, item.ContextID, item.ClassID)) + .ToDictionary(grouping => grouping.Key, grouping => (uint) grouping.Sum(item => item.Amount)); + + AssertResultMatchesExpectation(expectedResult, itemsToSend); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void MaxItemsTooSmall() { + const uint appID = 42; + + HashSet items = new() { + CreateCard(1, appID), + CreateCard(2, appID) + }; + + GetItemsForFullBadge(items, 2, appID, ArchiSteamFarm.Bot.MinCardsPerBadge - 1); + + Assert.Fail(); + } + + [TestMethod] + public void TooManyCardsForSingleTrade() { + const uint appID = 42; + + HashSet items = new(); + + for (byte i = 0; i < ArchiSteamFarm.Trading.MaxItemsPerTrade; i++) { + items.Add(CreateCard(1, appID)); + items.Add(CreateCard(2, appID)); + } + + HashSet itemsToSend = GetItemsForFullBadge(items, 2, appID); + + Assert.IsTrue(itemsToSend.Count <= ArchiSteamFarm.Trading.MaxItemsPerTrade); + } + + [TestMethod] + public void TooManyCardsForSingleTradeMultipleAppIDs() { + const uint appID0 = 42; + const uint appID1 = 43; + + HashSet items = new(); + + for (byte i = 0; i < 100; i++) { + items.Add(CreateCard(1, appID0)); + items.Add(CreateCard(2, appID0)); + items.Add(CreateCard(1, appID1)); + items.Add(CreateCard(2, appID1)); + } + + Dictionary itemsPerSet = new() { + { appID0, 2 }, + { appID1, 2 } + }; + + HashSet itemsToSend = GetItemsForFullBadge(items, itemsPerSet); + + Assert.IsTrue(itemsToSend.Count <= ArchiSteamFarm.Trading.MaxItemsPerTrade); + } + [TestMethod] public void MultipleSets() { const uint appID = 42; @@ -419,12 +500,12 @@ namespace ArchiSteamFarm.Tests { private static Steam.Asset CreateCard(ulong classID, uint realAppID, uint amount = 1, Steam.Asset.EType type = Steam.Asset.EType.TradingCard, Steam.Asset.ERarity rarity = Steam.Asset.ERarity.Common) => new(Steam.Asset.SteamAppID, Steam.Asset.SteamCommunityContextID, classID, amount, realAppID: realAppID, type: type, rarity: rarity); - private static HashSet GetItemsForFullBadge(IReadOnlyCollection inventory, byte cardsPerSet, uint appID) => GetItemsForFullBadge(inventory, new Dictionary { { appID, cardsPerSet } }); + private static HashSet GetItemsForFullBadge(IReadOnlyCollection inventory, byte cardsPerSet, uint appID, ushort maxItems = ArchiSteamFarm.Trading.MaxItemsPerTrade) => GetItemsForFullBadge(inventory, new Dictionary { { appID, cardsPerSet } }, maxItems); - private static HashSet GetItemsForFullBadge(IReadOnlyCollection inventory, IDictionary cardsPerSet) { + private static HashSet GetItemsForFullBadge(IReadOnlyCollection inventory, IDictionary cardsPerSet, ushort maxItems = ArchiSteamFarm.Trading.MaxItemsPerTrade) { Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), List> inventorySets = ArchiSteamFarm.Trading.GetInventorySets(inventory); - return ArchiSteamFarm.Bot.GetItemsForFullSets(inventory, inventorySets.ToDictionary(kv => kv.Key, kv => (SetsToExtract: inventorySets[kv.Key][0], cardsPerSet[kv.Key.RealAppID]))).ToHashSet(); + return ArchiSteamFarm.Bot.GetItemsForFullSets(inventory, inventorySets.ToDictionary(kv => kv.Key, kv => (SetsToExtract: inventorySets[kv.Key][0], cardsPerSet[kv.Key.RealAppID])), maxItems).ToHashSet(); } } } diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index 6881f26d7..6e85065f3 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -49,6 +49,7 @@ namespace ArchiSteamFarm { public sealed class Bot : IAsyncDisposable { internal const ushort CallbackSleep = 500; // In milliseconds internal const ushort MaxMessagePrefixLength = MaxMessageLength - ReservedMessageLength - 2; // 2 for a minimum of 2 characters (escape one and real one) + internal const byte MinCardsPerBadge = 5; internal const byte MinPlayingBlockedTTL = 60; // Delay in seconds added when account was occupied during our disconnect, to not disconnect other Steam client session too soon private const char DefaultBackgroundKeysRedeemerSeparator = '\t'; @@ -57,7 +58,6 @@ namespace ArchiSteamFarm { private const byte MaxInvalidPasswordFailures = WebBrowser.MaxTries; // Max InvalidPassword failures in a row before we determine that our password is invalid (because Steam wrongly returns those, of course) private const ushort MaxMessageLength = 5000; // This is a limitation enforced by Steam private const byte MaxTwoFactorCodeFailures = WebBrowser.MaxTries; // Max TwoFactorCodeMismatch failures in a row before we determine that our 2FA credentials are invalid (because Steam wrongly returns those, of course) - private const byte MinimumCardsPerBadge = 5; private const byte RedeemCooldownInHours = 1; // 1 hour since first redeem attempt, this is a limitation enforced by Steam private const byte ReservedMessageLength = 2; // 2 for 2x optional … @@ -509,19 +509,23 @@ namespace ArchiSteamFarm { } [PublicAPI] - public static HashSet GetItemsForFullSets(IReadOnlyCollection inventory, IReadOnlyDictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), (uint SetsToExtract, byte ItemsPerSet)> amountsToExtract) { + public static HashSet GetItemsForFullSets(IReadOnlyCollection inventory, IReadOnlyDictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), (uint SetsToExtract, byte ItemsPerSet)> amountsToExtract, ushort maxItems = Trading.MaxItemsPerTrade) { if ((inventory == null) || (inventory.Count == 0)) { throw new ArgumentNullException(nameof(inventory)); } - if ((amountsToExtract == null) || (amountsToExtract.Count == 0)) { + if ((amountsToExtract == null) || (amountsToExtract.Count == 0) || amountsToExtract.Any(kv => kv.Value.SetsToExtract == 0)) { throw new ArgumentNullException(nameof(amountsToExtract)); } + if (maxItems < MinCardsPerBadge) { + throw new ArgumentOutOfRangeException(nameof(maxItems)); + } + HashSet result = new(); Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), Dictionary>> itemsPerClassIDPerSet = inventory.GroupBy(item => (item.RealAppID, item.Type, item.Rarity)).ToDictionary(grouping => grouping.Key, grouping => grouping.GroupBy(item => item.ClassID).ToDictionary(group => group.Key, group => group.ToHashSet())); - foreach (((uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity) set, (uint setsToExtract, byte itemsPerSet)) in amountsToExtract) { + foreach (((uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity) set, (uint setsToExtract, byte itemsPerSet)) in amountsToExtract.OrderBy(kv => kv.Value.ItemsPerSet)) { if (!itemsPerClassIDPerSet.TryGetValue(set, out Dictionary>? itemsPerClassID)) { continue; } @@ -534,8 +538,17 @@ namespace ArchiSteamFarm { continue; } + ushort maxSetsAllowed = (ushort) (maxItems - result.Count); + maxSetsAllowed -= (ushort) (maxSetsAllowed % itemsPerSet); + maxSetsAllowed /= itemsPerSet; + ushort realSetsToExtract = (ushort) Math.Min(setsToExtract, maxSetsAllowed); + + if (realSetsToExtract == 0) { + break; + } + foreach (HashSet itemsOfClass in itemsPerClassID.Values) { - uint classRemaining = setsToExtract; + ushort classRemaining = realSetsToExtract; foreach (Steam.Asset item in itemsOfClass.TakeWhile(_ => classRemaining > 0)) { if (item.Amount > classRemaining) { @@ -547,7 +560,7 @@ namespace ArchiSteamFarm { } else { result.Add(item); - classRemaining -= item.Amount; + classRemaining -= (ushort) item.Amount; } } } @@ -3234,7 +3247,7 @@ namespace ArchiSteamFarm { } Dictionary<(uint RealAppID, Steam.Asset.EType Type, Steam.Asset.ERarity Rarity), List> inventorySets = Trading.GetInventorySets(inventory); - appIDs.IntersectWith(inventorySets.Where(kv => kv.Value.Count >= MinimumCardsPerBadge).Select(kv => kv.Key.RealAppID)); + appIDs.IntersectWith(inventorySets.Where(kv => kv.Value.Count >= MinCardsPerBadge).Select(kv => kv.Key.RealAppID)); if (appIDs.Count == 0) { return;