diff --git a/ArchiSteamFarm/RuntimeCompatibility.cs b/ArchiSteamFarm/RuntimeCompatibility.cs index 87b42c69a..ecdaf0a12 100644 --- a/ArchiSteamFarm/RuntimeCompatibility.cs +++ b/ArchiSteamFarm/RuntimeCompatibility.cs @@ -105,14 +105,6 @@ namespace ArchiSteamFarm { internal static async Task SendAsync(this WebSocket webSocket, byte[] buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) => await webSocket.SendAsync(new ArraySegment(buffer), messageType, endOfMessage, cancellationToken).ConfigureAwait(false); internal static string[] Split(this string text, char separator, StringSplitOptions options = StringSplitOptions.None) => text.Split(new[] { separator }, options); internal static void TrimExcess(this Dictionary _) { } // no-op - - internal static void TryAdd(this IDictionary dictionary, TKey key, TValue value) { - if (dictionary.ContainsKey(key)) { - return; - } - - dictionary.Add(key, value); - } #endif } } diff --git a/ArchiSteamFarm/Statistics.cs b/ArchiSteamFarm/Statistics.cs index 5ff68c646..d88da9d50 100644 --- a/ArchiSteamFarm/Statistics.cs +++ b/ArchiSteamFarm/Statistics.cs @@ -302,12 +302,10 @@ namespace ArchiSteamFarm { continue; } - // Those 3 collections are on user-basis since we can't be sure that the trade passes through (and therefore we need to keep original state in case of failure) HashSet<(uint AppID, Steam.Asset.EType Type)> skippedSetsThisUser = new HashSet<(uint AppID, Steam.Asset.EType Type)>(); - Dictionary ourFullSet = new Dictionary(); - Dictionary ourTradableSet = new Dictionary(); Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary> theirTradableState = Trading.GetInventoryState(theirInventory); + Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary> inventoryStateChanges = new Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary>(); for (byte i = 0; i < Trading.MaxTradesPerAccount; i++) { byte itemsInTrade = 0; @@ -317,16 +315,48 @@ namespace ArchiSteamFarm { Dictionary classIDsToReceive = new Dictionary(); foreach (KeyValuePair<(uint AppID, Steam.Asset.EType Type), Dictionary> ourInventoryStateSet in fullState.Where(set => listedUser.MatchableTypes.Contains(set.Key.Type) && set.Value.Values.Any(count => count > 1))) { - if (!tradableState.TryGetValue(ourInventoryStateSet.Key, out Dictionary ourTradableItems)) { + if (!tradableState.TryGetValue(ourInventoryStateSet.Key, out Dictionary ourTradableItems) || (ourTradableItems.Count == 0)) { continue; } - if (!theirTradableState.TryGetValue(ourInventoryStateSet.Key, out Dictionary theirItems)) { + if (!theirTradableState.TryGetValue(ourInventoryStateSet.Key, out Dictionary theirItems) || (theirItems.Count == 0)) { continue; } - ourFullSet.AddRange(ourInventoryStateSet.Value); - ourTradableSet.AddRange(ourTradableItems); + // Those 2 collections are on user-basis since we can't be sure that the trade passes through (and therefore we need to keep original state in case of failure) + Dictionary ourFullSet = new Dictionary(ourInventoryStateSet.Value); + Dictionary ourTradableSet = new Dictionary(ourTradableItems); + + // We also have to take into account changes that happened in previoius trades with this user, so this block will adapt to that + if (inventoryStateChanges.TryGetValue(ourInventoryStateSet.Key, out Dictionary pastChanges) && (pastChanges.Count > 0)) { + foreach (KeyValuePair pastChange in pastChanges) { + if (!ourFullSet.TryGetValue(pastChange.Key, out uint fullAmount) || (fullAmount == 0) || (fullAmount < pastChange.Value)) { + Bot.ArchiLogger.LogNullError(nameof(fullAmount)); + return false; + } + + if (fullAmount > pastChange.Value) { + ourFullSet[pastChange.Key] = fullAmount - pastChange.Value; + } else { + ourFullSet.Remove(pastChange.Key); + } + + if (!ourTradableSet.TryGetValue(pastChange.Key, out uint tradableAmount) || (tradableAmount == 0) || (tradableAmount < pastChange.Value)) { + Bot.ArchiLogger.LogNullError(nameof(tradableAmount)); + return false; + } + + if (fullAmount > pastChange.Value) { + ourTradableSet[pastChange.Key] = fullAmount - pastChange.Value; + } else { + ourTradableSet.Remove(pastChange.Key); + } + } + + if (Trading.IsEmptyForMatching(ourFullSet, ourTradableSet)) { + continue; + } + } bool match; @@ -334,7 +364,7 @@ namespace ArchiSteamFarm { match = false; foreach (KeyValuePair ourItem in ourFullSet.Where(item => item.Value > 1).OrderByDescending(item => item.Value)) { - if (!ourTradableSet.TryGetValue(ourItem.Key, out uint tradableAmount)) { + if (!ourTradableSet.TryGetValue(ourItem.Key, out uint tradableAmount) || (tradableAmount == 0)) { continue; } @@ -348,25 +378,42 @@ namespace ArchiSteamFarm { // Update our state based on given items classIDsToGive[ourItem.Key] = classIDsToGive.TryGetValue(ourItem.Key, out uint givenAmount) ? givenAmount + 1 : 1; - ourFullSet[ourItem.Key] = ourItem.Value - 1; + ourFullSet[ourItem.Key] = ourItem.Value - 1; // We don't need to remove anything here because we can guarantee that ourItem.Value is at least 2 + + if (inventoryStateChanges.TryGetValue(ourInventoryStateSet.Key, out Dictionary currentChanges)) { + if (currentChanges.TryGetValue(ourItem.Key, out uint amount)) { + currentChanges[ourItem.Key] = amount + 1; + } else { + currentChanges[ourItem.Key] = 1; + } + } else { + inventoryStateChanges[ourInventoryStateSet.Key] = new Dictionary { + { ourItem.Key, 1 } + }; + } // Update our state based on received items classIDsToReceive[theirItem.Key] = classIDsToReceive.TryGetValue(theirItem.Key, out uint receivedAmount) ? receivedAmount + 1 : 1; ourFullSet[theirItem.Key] = ourAmountOfTheirItem + 1; - // Update their state based on taken items - if (theirItems.TryGetValue(theirItem.Key, out uint theirAmount) && (theirAmount > 1)) { - theirItems[theirItem.Key] = theirAmount - 1; - } else { - theirItems.Remove(theirItem.Key); - } - if (tradableAmount > 1) { ourTradableSet[ourItem.Key] = tradableAmount - 1; } else { ourTradableSet.Remove(ourItem.Key); } + // Update their state based on taken items + if (!theirItems.TryGetValue(theirItem.Key, out uint theirAmount) || (theirAmount == 0)) { + Bot.ArchiLogger.LogNullError(nameof(theirAmount)); + return false; + } + + if (theirAmount > 1) { + theirItems[theirItem.Key] = theirAmount - 1; + } else { + theirItems.Remove(theirItem.Key); + } + itemsInTrade += 2; match = true; diff --git a/ArchiSteamFarm/Trading.cs b/ArchiSteamFarm/Trading.cs index 5ea8a1167..eeaf13a69 100644 --- a/ArchiSteamFarm/Trading.cs +++ b/ArchiSteamFarm/Trading.cs @@ -144,35 +144,49 @@ namespace ArchiSteamFarm { } foreach (KeyValuePair<(uint AppID, Steam.Asset.EType Type), Dictionary> tradableSet in tradableState) { - foreach (KeyValuePair tradableItem in tradableSet.Value) { - switch (tradableItem.Value) { - case 0: - // No tradable items, this should never happen, dictionary should not have this key to begin with - ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(tradableItem.Value), tradableItem.Value)); + if (!fullState.TryGetValue(tradableSet.Key, out Dictionary fullSet) || (fullSet == null) || (fullSet.Count == 0)) { + ASF.ArchiLogger.LogNullError(nameof(fullSet)); + return false; + } + + if (!IsEmptyForMatching(fullSet, tradableSet.Value)) { + return false; + } + } + + // We didn't find any matchable combinations, so this inventory is empty + return true; + } + + internal static bool IsEmptyForMatching(IReadOnlyDictionary fullSet, IReadOnlyDictionary tradableSet) { + if ((fullSet == null) || (tradableSet == null)) { + ASF.ArchiLogger.LogNullError(nameof(fullSet) + " || " + nameof(tradableSet)); + return false; + } + + foreach (KeyValuePair tradableItem in tradableSet) { + switch (tradableItem.Value) { + case 0: + // No tradable items, this should never happen, dictionary should not have this key to begin with + ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(tradableItem.Value), tradableItem.Value)); + return false; + case 1: + // Single tradable item, can be matchable or not depending on the rest of the inventory + if (!fullSet.TryGetValue(tradableItem.Key, out uint fullAmount) || (fullAmount == 0) || (fullAmount < tradableItem.Value)) { + ASF.ArchiLogger.LogNullError(nameof(fullAmount)); return false; - case 1: - // Single tradable item, can be matchable or not depending on the rest of the inventory - if (!fullState.TryGetValue(tradableSet.Key, out Dictionary fullItem) || (fullItem == null) || (fullItem.Count == 0)) { - ASF.ArchiLogger.LogNullError(nameof(fullItem)); - return false; - } + } - if (!fullItem.TryGetValue(tradableItem.Key, out uint fullAmount) || (fullAmount == 0)) { - ASF.ArchiLogger.LogNullError(nameof(fullAmount)); - return false; - } - - if (fullAmount > 1) { - // If we have a single tradable item but more than 1 in total, this is matchable - return false; - } - - // A single exclusive tradable item is not matchable, continue - continue; - default: - // Any other combination of tradable items is always matchable + if (fullAmount > 1) { + // If we have a single tradable item but more than 1 in total, this is matchable return false; - } + } + + // A single exclusive tradable item is not matchable, continue + continue; + default: + // Any other combination of tradable items is always matchable + return false; } } diff --git a/ArchiSteamFarm/Utilities.cs b/ArchiSteamFarm/Utilities.cs index 04142102f..d4e172241 100644 --- a/ArchiSteamFarm/Utilities.cs +++ b/ArchiSteamFarm/Utilities.cs @@ -36,17 +36,6 @@ namespace ArchiSteamFarm { // Normally we wouldn't need to use this singleton, but we want to ensure decent randomness across entire program's lifetime private static readonly Random Random = new Random(); - internal static void AddRange(this IDictionary dictionary, IEnumerable> other) { - if ((dictionary == null) || (other == null)) { - ASF.ArchiLogger.LogNullError(nameof(dictionary) + " || " + nameof(other)); - return; - } - - foreach (KeyValuePair item in other) { - dictionary.TryAdd(item.Key, item.Value); - } - } - internal static string GetArgsAsText(string[] args, byte argsToSkip, string delimiter) { if ((args == null) || (args.Length <= argsToSkip) || string.IsNullOrEmpty(delimiter)) { ASF.ArchiLogger.LogNullError(nameof(args) + " || " + nameof(argsToSkip) + " || " + nameof(delimiter));