diff --git a/ArchiSteamFarm/Actions.cs b/ArchiSteamFarm/Actions.cs index f513f9f07..7da817929 100644 --- a/ArchiSteamFarm/Actions.cs +++ b/ArchiSteamFarm/Actions.cs @@ -294,7 +294,7 @@ namespace ArchiSteamFarm { return (false, Strings.BotLootingFailed); } - (bool success, HashSet mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(targetSteamID, inventory, Bot.BotConfig.SteamTradeToken).ConfigureAwait(false); + (bool success, HashSet mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(targetSteamID, inventory, token: Bot.BotConfig.SteamTradeToken).ConfigureAwait(false); if ((mobileTradeOfferIDs != null) && (mobileTradeOfferIDs.Count > 0) && Bot.HasMobileAuthenticator) { if (!await AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, targetSteamID, mobileTradeOfferIDs, true).ConfigureAwait(false)) { diff --git a/ArchiSteamFarm/ArchiWebHandler.cs b/ArchiSteamFarm/ArchiWebHandler.cs index 5b3389b37..718d28bc4 100644 --- a/ArchiSteamFarm/ArchiWebHandler.cs +++ b/ArchiSteamFarm/ArchiWebHandler.cs @@ -43,6 +43,7 @@ namespace ArchiSteamFarm { private const string ISteamApps = "ISteamApps"; private const string ISteamUserAuth = "ISteamUserAuth"; private const string ITwoFactorService = "ITwoFactorService"; + private const ushort MaxItemsInSingleInventoryRequest = 5000; private const byte MinSessionValidityInSeconds = GlobalConfig.DefaultConnectionTimeout / 6; private const string SteamCommunityHost = "steamcommunity.com"; private const string SteamCommunityURL = "https://" + SteamCommunityHost; @@ -541,7 +542,7 @@ namespace ArchiSteamFarm { } [SuppressMessage("ReSharper", "FunctionComplexityOverflow")] - internal async Task> GetInventory(ulong steamID = 0, uint appID = Steam.Asset.SteamAppID, byte contextID = Steam.Asset.SteamCommunityContextID, bool? tradable = null, IReadOnlyCollection wantedTypes = null, IReadOnlyCollection wantedRealAppIDs = null) { + internal async Task> GetInventory(ulong steamID = 0, uint appID = Steam.Asset.SteamAppID, byte contextID = Steam.Asset.SteamCommunityContextID, bool? tradable = null, IReadOnlyCollection wantedTypes = null, IReadOnlyCollection wantedRealAppIDs = null, IReadOnlyCollection<(uint AppID, Steam.Asset.EType Type)> skippedSets = null) { if ((appID == 0) || (contextID == 0)) { Bot.ArchiLogger.LogNullError(nameof(appID) + " || " + nameof(contextID)); return null; @@ -564,8 +565,7 @@ namespace ArchiSteamFarm { HashSet result = new HashSet(); - // 5000 is maximum allowed count per single request - string request = "/inventory/" + steamID + "/" + appID + "/" + contextID + "?count=5000&l=english"; + string request = "/inventory/" + steamID + "/" + appID + "/" + contextID + "?count=" + MaxItemsInSingleInventoryRequest + "&l=english"; ulong startAssetID = 0; await InventorySemaphore.WaitAsync().ConfigureAwait(false); @@ -625,7 +625,7 @@ namespace ArchiSteamFarm { foreach (Steam.Asset asset in response.Assets.Where(asset => asset != null)) { if (descriptionMap.TryGetValue(asset.ClassID, out (bool Tradable, Steam.Asset.EType Type, uint RealAppID) description)) { - if ((tradable.HasValue && (description.Tradable != tradable.Value)) || (wantedTypes?.Contains(description.Type) == false) || (wantedRealAppIDs?.Contains(description.RealAppID) == false)) { + if ((tradable.HasValue && (description.Tradable != tradable.Value)) || (wantedTypes?.Contains(description.Type) == false) || (wantedRealAppIDs?.Contains(description.RealAppID) == false) || (skippedSets?.Contains((description.RealAppID, description.Type)) == true)) { continue; } @@ -1146,26 +1146,43 @@ namespace ArchiSteamFarm { return response == null ? ((EResult Result, EPurchaseResultDetail? PurchaseResult)?) null : (response.Result, response.PurchaseResultDetail); } - internal async Task<(bool Success, HashSet MobileTradeOfferIDs)> SendTradeOffer(ulong partnerID, IReadOnlyCollection itemsToGive, string token = null) { - if ((partnerID == 0) || (itemsToGive == null) || (itemsToGive.Count == 0)) { - Bot.ArchiLogger.LogNullError(nameof(partnerID) + " || " + nameof(itemsToGive)); + internal async Task<(bool Success, HashSet MobileTradeOfferIDs)> SendTradeOffer(ulong partnerID, IReadOnlyCollection itemsToGive = null, IReadOnlyCollection itemsToReceive = null, string token = null, bool forcedSingleOffer = false) { + if ((partnerID == 0) || (((itemsToGive == null) || (itemsToGive.Count == 0)) && ((itemsToReceive == null) || (itemsToReceive.Count == 0)))) { + Bot.ArchiLogger.LogNullError(nameof(partnerID) + " || (" + nameof(itemsToGive) + " && " + nameof(itemsToReceive) + ")"); return (false, null); } Steam.TradeOfferSendRequest singleTrade = new Steam.TradeOfferSendRequest(); HashSet trades = new HashSet { singleTrade }; - foreach (Steam.Asset itemToGive in itemsToGive) { - if (singleTrade.ItemsToGive.Assets.Count >= Trading.MaxItemsPerTrade) { - if (trades.Count >= Trading.MaxTradesPerAccount) { - break; + if (itemsToGive != null) { + foreach (Steam.Asset itemToGive in itemsToGive) { + if (!forcedSingleOffer && (singleTrade.ItemsToGive.Assets.Count + singleTrade.ItemsToReceive.Assets.Count >= Trading.MaxItemsPerTrade)) { + if (trades.Count >= Trading.MaxTradesPerAccount) { + break; + } + + singleTrade = new Steam.TradeOfferSendRequest(); + trades.Add(singleTrade); } - singleTrade = new Steam.TradeOfferSendRequest(); - trades.Add(singleTrade); + singleTrade.ItemsToGive.Assets.Add(itemToGive); } + } - singleTrade.ItemsToGive.Assets.Add(itemToGive); + if (itemsToReceive != null) { + foreach (Steam.Asset itemToReceive in itemsToReceive) { + if (!forcedSingleOffer && (singleTrade.ItemsToGive.Assets.Count + singleTrade.ItemsToReceive.Assets.Count >= Trading.MaxItemsPerTrade)) { + if (trades.Count >= Trading.MaxTradesPerAccount) { + break; + } + + singleTrade = new Steam.TradeOfferSendRequest(); + trades.Add(singleTrade); + } + + singleTrade.ItemsToReceive.Assets.Add(itemToReceive); + } } const string request = "/tradeoffer/new/send"; diff --git a/ArchiSteamFarm/BotConfig.cs b/ArchiSteamFarm/BotConfig.cs index 76556d413..1dd85a4e9 100644 --- a/ArchiSteamFarm/BotConfig.cs +++ b/ArchiSteamFarm/BotConfig.cs @@ -401,7 +401,8 @@ namespace ArchiSteamFarm { SteamTradeMatcher = 2, MatchEverything = 4, DontAcceptBotTrades = 8, - All = AcceptDonations | SteamTradeMatcher | MatchEverything | DontAcceptBotTrades + MatchActively = 16, + All = AcceptDonations | SteamTradeMatcher | MatchEverything | DontAcceptBotTrades | MatchActively } // ReSharper disable UnusedMember.Global diff --git a/ArchiSteamFarm/Statistics.cs b/ArchiSteamFarm/Statistics.cs index 1746b0e34..a41f205ba 100644 --- a/ArchiSteamFarm/Statistics.cs +++ b/ArchiSteamFarm/Statistics.cs @@ -25,10 +25,14 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Json; +using ArchiSteamFarm.Localization; using Newtonsoft.Json; namespace ArchiSteamFarm { internal sealed class Statistics : IDisposable { + private const byte MaxMatchedBotsHard = 20; + private const byte MaxMatchesBotsSoft = 10; + private const byte MaxMatchingRounds = 10; private const byte MinAnnouncementCheckTTL = 6; // Minimum amount of hours we must wait before checking eligibility for Announcement, should be lower than MinPersonaStateTTL private const byte MinHeartBeatTTL = 10; // Minimum amount of minutes we must wait before sending next HeartBeat private const byte MinItemsCount = 100; // Minimum amount of items to be eligible for public listing @@ -43,6 +47,7 @@ namespace ArchiSteamFarm { }; private readonly Bot Bot; + private readonly Timer MatchActivelyTimer; private readonly SemaphoreSlim RequestsSemaphore = new SemaphoreSlim(1, 1); private DateTime LastAnnouncementCheck; @@ -50,9 +55,22 @@ namespace ArchiSteamFarm { private DateTime LastPersonaStateRequest; private bool ShouldSendHeartBeats; - internal Statistics(Bot bot) => Bot = bot ?? throw new ArgumentNullException(nameof(bot)); + internal Statistics(Bot bot) { + Bot = bot ?? throw new ArgumentNullException(nameof(bot)); - public void Dispose() => RequestsSemaphore.Dispose(); + // TODO: This should start from 1 hour, not 1 minute + MatchActivelyTimer = new Timer( + async e => await MatchActively().ConfigureAwait(false), + null, + TimeSpan.FromMinutes(1) + TimeSpan.FromSeconds(Program.GlobalConfig.LoginLimiterDelay + Program.LoadBalancingDelay * Bot.Bots.Count), // Delay + TimeSpan.FromDays(1) // Period + ); + } + + public void Dispose() { + MatchActivelyTimer.Dispose(); + RequestsSemaphore.Dispose(); + } internal async Task OnHeartBeat() { // Request persona update if needed @@ -78,8 +96,7 @@ namespace ArchiSteamFarm { { "Guid", Program.GlobalDatabase.Guid.ToString("N") } }; - // We don't need retry logic here - if (await Program.WebBrowser.UrlPost(request, data, maxTries: 1).ConfigureAwait(false) != null) { + if (await Program.WebBrowser.UrlPost(request, data).ConfigureAwait(false) != null) { LastHeartBeat = DateTime.UtcNow; } } finally { @@ -103,7 +120,7 @@ namespace ArchiSteamFarm { // Don't announce if we don't meet conditions string tradeToken; - if (!await ShouldAnnounce().ConfigureAwait(false) || string.IsNullOrEmpty(tradeToken = await Bot.ArchiHandler.GetTradeToken().ConfigureAwait(false))) { + if (!await IsEligibleForMatching().ConfigureAwait(false) || string.IsNullOrEmpty(tradeToken = await Bot.ArchiHandler.GetTradeToken().ConfigureAwait(false))) { LastAnnouncementCheck = DateTime.UtcNow; ShouldSendHeartBeats = false; return; @@ -111,6 +128,7 @@ namespace ArchiSteamFarm { HashSet acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(type => AcceptedMatchableTypes.Contains(type)).ToHashSet(); if (acceptedMatchableTypes.Count == 0) { + Bot.ArchiLogger.LogNullError(nameof(acceptedMatchableTypes)); LastAnnouncementCheck = DateTime.UtcNow; ShouldSendHeartBeats = false; return; @@ -144,7 +162,7 @@ namespace ArchiSteamFarm { { "TradeToken", tradeToken } }; - // We don't need retry logic here + // Listing is free to deny our announce request, hence we don't retry if (await Program.WebBrowser.UrlPost(request, data, maxTries: 1).ConfigureAwait(false) != null) { LastAnnouncementCheck = DateTime.UtcNow; ShouldSendHeartBeats = true; @@ -154,7 +172,14 @@ namespace ArchiSteamFarm { } } - private async Task ShouldAnnounce() { + private static async Task> GetListedUsers() { + const string request = URL + "/Api/Bots"; + + WebBrowser.ObjectResponse> objectResponse = await Program.WebBrowser.UrlGetToJsonObject>(request).ConfigureAwait(false); + return objectResponse?.Content; + } + + private async Task IsEligibleForMatching() { // Bot must have ASF 2FA if (!Bot.HasMobileAuthenticator) { return false; @@ -178,5 +203,316 @@ namespace ArchiSteamFarm { // Bot must have valid API key (e.g. not being restricted account) return await Bot.ArchiWebHandler.HasValidApiKey().ConfigureAwait(false); } + + private async Task MatchActively() { + // TODO: This function has a lot of debug leftovers for logic testing, once that period is over, get rid of them + bool match = true; + + Bot.ArchiLogger.LogGenericDebug("Matching started!"); + + for (byte i = 0; (i < MaxMatchingRounds) && match; i++) { + if (i > 0) { + // After each round we wait at least 5 minutes for all bots to react + Bot.ArchiLogger.LogGenericDebug("Cooldown..."); + await Task.Delay(5 * 60 * 1000).ConfigureAwait(false); + } + + Bot.ArchiLogger.LogGenericDebug("Now matching, round #" + i); + match = await MatchActivelyRound().ConfigureAwait(false); + Bot.ArchiLogger.LogGenericDebug("Matching ended, round #" + i); + } + + Bot.ArchiLogger.LogGenericDebug("Matching finished!"); + } + + private async Task MatchActivelyRound() { + // TODO: This function has a lot of debug leftovers for logic testing, once that period is over, get rid of them + if (!Bot.IsConnectedAndLoggedOn || Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything) || !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchActively) || !await IsEligibleForMatching().ConfigureAwait(false)) { + Bot.ArchiLogger.LogGenericDebug("User not eligible for this function, returning"); + return false; + } + + HashSet acceptedMatchableTypes = Bot.BotConfig.MatchableTypes.Where(type => AcceptedMatchableTypes.Contains(type)).ToHashSet(); + if (acceptedMatchableTypes.Count == 0) { + Bot.ArchiLogger.LogGenericDebug("No acceptable matchable types, returning"); + return false; + } + + HashSet ourInventory = await Bot.ArchiWebHandler.GetInventory(Bot.SteamID, tradable: true, wantedTypes: acceptedMatchableTypes).ConfigureAwait(false); + if ((ourInventory == null) || (ourInventory.Count == 0)) { + Bot.ArchiLogger.LogGenericDebug("Empty inventory, returning"); + return false; + } + + Dictionary classIDs = new Dictionary(); + + foreach (Steam.Asset item in ourInventory) { + if (classIDs.TryGetValue(item.ClassID, out uint amount)) { + classIDs[item.ClassID] = amount + item.Amount; + } else { + classIDs.Add(item.ClassID, item.Amount); + } + } + + if (classIDs.Values.All(amount => amount <= 1)) { + // User doesn't have any dupes in the inventory + Bot.ArchiLogger.LogGenericDebug("No dupes in inventory, returning"); + return false; + } + + HashSet listedUsers = await GetListedUsers().ConfigureAwait(false); + if ((listedUsers == null) || (listedUsers.Count == 0)) { + Bot.ArchiLogger.LogGenericDebug("No listed users, returning"); + return false; + } + + Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary> ourInventoryState = Trading.GetInventoryState(ourInventory); + + byte emptyMatches = 0; + HashSet<(uint AppID, Steam.Asset.EType Type)> skippedSets = new HashSet<(uint AppID, Steam.Asset.EType Type)>(); + + foreach (ListedUser listedUser in listedUsers.Where(listedUser => listedUser.MatchEverything && !Bot.IsBlacklistedFromTrades(listedUser.SteamID)).OrderByDescending(listedUser => listedUser.Score).Take(MaxMatchedBotsHard)) { + Bot.ArchiLogger.LogGenericDebug("Now matching " + listedUser.SteamID + "..."); + + HashSet theirInventory = await Bot.ArchiWebHandler.GetInventory(listedUser.SteamID, tradable: true, wantedTypes: acceptedMatchableTypes, skippedSets: skippedSets).ConfigureAwait(false); + if ((theirInventory == null) || (theirInventory.Count == 0)) { + Bot.ArchiLogger.LogGenericDebug("Inventory of " + listedUser.SteamID + " is empty, continuing..."); + continue; + } + + Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary> theirInventoryState = Trading.GetInventoryState(theirInventory); + + Dictionary classIDsToGive = new Dictionary(); + Dictionary classIDsToReceive = new Dictionary(); + HashSet<(uint AppID, Steam.Asset.EType Type)> skippedSetsThisTrade = new HashSet<(uint AppID, Steam.Asset.EType Type)>(); + + foreach (KeyValuePair<(uint AppID, Steam.Asset.EType Type), Dictionary> ourInventoryStateSet in ourInventoryState.Where(set => listedUser.MatchableTypes.Contains(set.Key.Type) && set.Value.Values.Any(count => count > 1))) { + if (!theirInventoryState.TryGetValue(ourInventoryStateSet.Key, out Dictionary theirItems)) { + continue; + } + + bool match; + + do { + match = false; + + foreach (KeyValuePair ourItem in ourInventoryStateSet.Value.Where(item => item.Value > 1).OrderByDescending(item => item.Value)) { + foreach (KeyValuePair theirItem in theirItems.OrderBy(item => ourInventoryStateSet.Value.TryGetValue(item.Key, out uint ourAmount) ? ourAmount : 0)) { + if (ourInventoryStateSet.Value.TryGetValue(theirItem.Key, out uint ourAmountOfTheirItem) && (ourItem.Value <= ourAmountOfTheirItem + 1)) { + continue; + } + + Bot.ArchiLogger.LogGenericDebug("Found a match: our " + ourItem.Key + " for theirs " + theirItem.Key); + + // Skip this set from the remaining of this round + skippedSetsThisTrade.Add(ourInventoryStateSet.Key); + + // Update our state based on given items + classIDsToGive[ourItem.Key] = classIDsToGive.TryGetValue(ourItem.Key, out uint givenAmount) ? givenAmount + 1 : 1; + ourInventoryStateSet.Value[ourItem.Key] = ourItem.Value - 1; + + // Update our state based on received items + classIDsToReceive[theirItem.Key] = classIDsToReceive.TryGetValue(theirItem.Key, out uint receivedAmount) ? receivedAmount + 1 : 1; + ourInventoryStateSet.Value[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); + } + + match = true; + break; + } + + if (match) { + break; + } + } + } while (match); + } + + if ((classIDsToGive.Count == 0) && (classIDsToReceive.Count == 0)) { + Bot.ArchiLogger.LogGenericDebug("No matches found, continuing..."); + + if (++emptyMatches >= MaxMatchesBotsSoft) { + break; + } + + continue; + } + + emptyMatches = 0; + + HashSet itemsToGive = Trading.GetItemsFromInventory(ourInventory, classIDsToGive); + HashSet itemsToReceive = Trading.GetItemsFromInventory(theirInventory, classIDsToReceive); + + // TODO: Debug only offer, should be removed after tests + Steam.TradeOffer debugOffer = new Steam.TradeOffer(1, 46697991, Steam.TradeOffer.ETradeOfferState.Active); + + foreach (Steam.Asset itemToGive in itemsToGive) { + debugOffer.ItemsToGive.Add(itemToGive); + } + + foreach (Steam.Asset itemToReceive in itemsToReceive) { + debugOffer.ItemsToReceive.Add(itemToReceive); + } + + if (!debugOffer.IsFairTypesExchange()) { + Bot.ArchiLogger.LogGenericDebug("CRITICAL: This offer is NOT fair!!!"); + return false; + } + + Bot.ArchiLogger.LogGenericDebug("Sending trade: our " + string.Join(", ", itemsToGive.Select(item => item.RealAppID + "/" + item.Type + " " + item.ClassID + " of " + item.Amount))); + + (bool success, HashSet mobileTradeOfferIDs) = await Bot.ArchiWebHandler.SendTradeOffer(listedUser.SteamID, itemsToGive, itemsToReceive, listedUser.TradeToken, true).ConfigureAwait(false); + + if ((mobileTradeOfferIDs != null) && (mobileTradeOfferIDs.Count > 0) && Bot.HasMobileAuthenticator) { + if (!await Bot.Actions.AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, listedUser.SteamID, mobileTradeOfferIDs, true).ConfigureAwait(false)) { + return false; + } + } + + if (!success) { + Bot.ArchiLogger.LogGenericDebug("Trade failed (?), continuing..."); + continue; + } + + Bot.ArchiLogger.LogGenericDebug("Trade succeeded!"); + + foreach (KeyValuePair classIDToGive in classIDsToGive) { + if (!classIDs.TryGetValue(classIDToGive.Key, out uint amount)) { + continue; + } + + if (amount <= classIDToGive.Value) { + classIDs.Remove(classIDToGive.Key); + } else { + classIDs[classIDToGive.Key] = amount - classIDToGive.Value; + } + } + + if (classIDs.Values.All(amount => amount <= 1)) { + // User doesn't have any more dupes in the inventory + Bot.ArchiLogger.LogGenericDebug("No dupes in inventory, returning"); + return false; + } + + skippedSets.UnionWith(skippedSetsThisTrade); + } + + Bot.ArchiLogger.LogGenericDebug("This round is over, we traded " + skippedSets.Count + " sets!"); + return skippedSets.Count > 0; + } + + private sealed class ListedUser { + internal readonly HashSet MatchableTypes = new HashSet(); + + [JsonProperty(PropertyName = "steam_id", Required = Required.Always)] + internal readonly ulong SteamID; + + [JsonProperty(PropertyName = "trade_token", Required = Required.Always)] + internal readonly string TradeToken; + + internal float Score => GamesCount / (float) ItemsCount; + +#pragma warning disable 649 + [JsonProperty(PropertyName = "games_count", Required = Required.Always)] + private readonly ushort GamesCount; +#pragma warning restore 649 + +#pragma warning disable 649 + [JsonProperty(PropertyName = "items_count", Required = Required.Always)] + private readonly ushort ItemsCount; +#pragma warning restore 649 + + internal bool MatchEverything { get; private set; } + + [JsonProperty(PropertyName = "matchable_backgrounds", Required = Required.Always)] + private byte MatchableBackgroundsNumber { + set { + switch (value) { + case 0: + MatchableTypes.Remove(Steam.Asset.EType.ProfileBackground); + break; + case 1: + MatchableTypes.Add(Steam.Asset.EType.ProfileBackground); + break; + default: + ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(value), value)); + return; + } + } + } + + [JsonProperty(PropertyName = "matchable_cards", Required = Required.Always)] + private byte MatchableCardsNumber { + set { + switch (value) { + case 0: + MatchableTypes.Remove(Steam.Asset.EType.TradingCard); + break; + case 1: + MatchableTypes.Add(Steam.Asset.EType.TradingCard); + break; + default: + ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(value), value)); + return; + } + } + } + + [JsonProperty(PropertyName = "matchable_emoticons", Required = Required.Always)] + private byte MatchableEmoticonsNumber { + set { + switch (value) { + case 0: + MatchableTypes.Remove(Steam.Asset.EType.Emoticon); + break; + case 1: + MatchableTypes.Add(Steam.Asset.EType.Emoticon); + break; + default: + ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(value), value)); + return; + } + } + } + + [JsonProperty(PropertyName = "matchable_foil_cards", Required = Required.Always)] + private byte MatchableFoilCardsNumber { + set { + switch (value) { + case 0: + MatchableTypes.Remove(Steam.Asset.EType.FoilTradingCard); + break; + case 1: + MatchableTypes.Add(Steam.Asset.EType.FoilTradingCard); + break; + default: + ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(value), value)); + return; + } + } + } + + [JsonProperty(PropertyName = "match_everything", Required = Required.Always)] + private byte MatchEverythingNumber { + set { + switch (value) { + case 0: + MatchEverything = false; + break; + case 1: + MatchEverything = true; + break; + default: + ASF.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(value), value)); + return; + } + } + } + } } } diff --git a/ArchiSteamFarm/Trading.cs b/ArchiSteamFarm/Trading.cs index 02aa6e32b..4f6213122 100644 --- a/ArchiSteamFarm/Trading.cs +++ b/ArchiSteamFarm/Trading.cs @@ -43,6 +43,60 @@ namespace ArchiSteamFarm { public void Dispose() => TradesSemaphore.Dispose(); + internal static Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary> GetInventoryState(IReadOnlyCollection inventory) { + if ((inventory == null) || (inventory.Count == 0)) { + ASF.ArchiLogger.LogNullError(nameof(inventory)); + return null; + } + + Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary> sets = new Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary>(); + + foreach (Steam.Asset item in inventory) { + (uint RealAppID, Steam.Asset.EType Type) key = (item.RealAppID, item.Type); + + if (sets.TryGetValue(key, out Dictionary set)) { + if (set.TryGetValue(item.ClassID, out uint amount)) { + set[item.ClassID] = amount + item.Amount; + } else { + set[item.ClassID] = item.Amount; + } + } else { + sets[key] = new Dictionary { { item.ClassID, item.Amount } }; + } + } + + return sets; + } + + internal static HashSet GetItemsFromInventory(IReadOnlyCollection inventory, IDictionary classIDs) { + if ((inventory == null) || (inventory.Count == 0) || (classIDs == null) || (classIDs.Count == 0)) { + ASF.ArchiLogger.LogNullError(nameof(inventory) + " || " + nameof(classIDs)); + return null; + } + + HashSet result = new HashSet(); + + foreach (Steam.Asset item in inventory) { + if (!classIDs.TryGetValue(item.ClassID, out uint amount)) { + continue; + } + + if (amount < item.Amount) { + item.Amount = amount; + } + + result.Add(item); + + if (amount == item.Amount) { + classIDs.Remove(item.ClassID); + } else { + classIDs[item.ClassID] = amount - item.Amount; + } + } + + return result; + } + internal void OnDisconnected() => IgnoredTrades.Clear(); internal async Task OnNewTrade() { @@ -75,20 +129,7 @@ namespace ArchiSteamFarm { return null; } - Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary> sets = new Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary>(); - - foreach (Steam.Asset item in inventory) { - if (sets.TryGetValue((item.RealAppID, item.Type), out Dictionary set)) { - if (set.TryGetValue(item.ClassID, out uint amount)) { - set[item.ClassID] = amount + item.Amount; - } else { - set[item.ClassID] = item.Amount; - } - } else { - sets[(item.RealAppID, item.Type)] = new Dictionary { { item.ClassID, item.Amount } }; - } - } - + Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary> sets = GetInventoryState(inventory); return sets.ToDictionary(set => set.Key, set => set.Value.Values.OrderBy(amount => amount).ToList()); }