private async Task<ParseTradeResult> ShouldAcceptTrade(Steam.TradeOffer tradeOffer) { if (tradeOffer == null) { Bot.ArchiLogger.LogNullError(nameof(tradeOffer)); return null; } // Always accept trades from SteamMasterID if ((tradeOffer.OtherSteamID64 != 0) && ((tradeOffer.OtherSteamID64 == Bot.BotConfig.SteamMasterID) || (tradeOffer.OtherSteamID64 == Program.GlobalConfig.SteamOwnerID))) { return new ParseTradeResult(tradeOffer.TradeOfferID, tradeOffer.ItemsToGive.Count > 0 ? ParseTradeResult.EResult.AcceptedWithItemLose : ParseTradeResult.EResult.AcceptedWithoutItemLose); } // Check if it's donation trade if (tradeOffer.ItemsToGive.Count == 0) { ParseTradeResult.EResult donationResult; // If it's steam fuckup, temporarily ignore it, otherwise react accordingly, depending on our preference if (tradeOffer.ItemsToReceive.Count == 0) { donationResult = ParseTradeResult.EResult.RejectedTemporarily; } else if (Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.AcceptDonations) || ((tradeOffer.OtherSteamID64 != 0) && Bot.Bots.Values.Any(bot => bot.SteamID == tradeOffer.OtherSteamID64))) { donationResult = ParseTradeResult.EResult.AcceptedWithoutItemLose; } else { donationResult = ParseTradeResult.EResult.RejectedPermanently; } return new ParseTradeResult(tradeOffer.TradeOfferID, donationResult); } // If we don't have SteamTradeMatcher enabled, this is the end for us if (!Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.SteamTradeMatcher)) { return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.RejectedPermanently); } // Decline trade if we're giving more count-wise if (tradeOffer.ItemsToGive.Count > tradeOffer.ItemsToReceive.Count) { return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.RejectedPermanently); } // Decline trade if we're losing anything but steam cards, or if it's non-dupes trade if (!tradeOffer.IsSteamCardsRequest() || !tradeOffer.IsFairTypesExchange()) { return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.RejectedPermanently); } // At this point we're sure that STM trade is valid // Fetch trade hold duration byte? holdDuration = await Bot.ArchiWebHandler.GetTradeHoldDuration(tradeOffer.TradeOfferID).ConfigureAwait(false); if (!holdDuration.HasValue) { // If we can't get trade hold duration, reject trade temporarily return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.RejectedTemporarily); } // If user has a trade hold, we add extra logic if (holdDuration.Value > 0) { // If trade hold duration exceeds our max, or user asks for cards with short lifespan, reject the trade if ((holdDuration.Value > Program.GlobalConfig.MaxTradeHoldDuration) || tradeOffer.ItemsToGive.Any(item => GlobalConfig.GlobalBlacklist.Contains(item.RealAppID))) { return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.RejectedPermanently); } } // If we're matching everything, this is enough for us if (Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything)) { return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.AcceptedWithItemLose); } // Now check if it's worth for us to do the trade await LimitInventoryRequestsAsync().ConfigureAwait(false); HashSet<Steam.Item> inventory = await Bot.ArchiWebHandler.GetMySteamInventory(false).ConfigureAwait(false); if ((inventory == null) || (inventory.Count == 0)) { return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.AcceptedWithItemLose); // OK, assume that this trade is valid, we can't check our EQ } // Get appIDs we're interested in HashSet<uint> appIDs = new HashSet<uint>(tradeOffer.ItemsToGive.Select(item => item.RealAppID)); // Now remove from our inventory all items we're NOT interested in inventory.RemoveWhere(item => !appIDs.Contains(item.RealAppID)); // If for some reason Valve is talking crap and we can't find mentioned items, assume OK if (inventory.Count == 0) { return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.AcceptedWithItemLose); } // Now let's create a map which maps items to their amount in our EQ Dictionary<ulong, uint> amountMap = new Dictionary<ulong, uint>(); foreach (Steam.Item item in inventory) { uint amount; if (amountMap.TryGetValue(item.ClassID, out amount)) { amountMap[item.ClassID] = amount + item.Amount; } else { amountMap[item.ClassID] = item.Amount; } } // Calculate our value of items to give List<uint> amountsToGive = new List<uint>(tradeOffer.ItemsToGive.Count); Dictionary<ulong, uint> amountMapToGive = new Dictionary<ulong, uint>(amountMap); foreach (ulong key in tradeOffer.ItemsToGive.Select(item => item.ClassID)) { uint amount; if (!amountMapToGive.TryGetValue(key, out amount)) { amountsToGive.Add(0); continue; } amountsToGive.Add(amount); amountMapToGive[key] = amount - 1; // We're giving one, so we have one less } // Sort it ascending amountsToGive.Sort(); // Calculate our value of items to receive List<uint> amountsToReceive = new List<uint>(tradeOffer.ItemsToReceive.Count); Dictionary<ulong, uint> amountMapToReceive = new Dictionary<ulong, uint>(amountMap); foreach (ulong key in tradeOffer.ItemsToReceive.Select(item => item.ClassID)) { uint amount; if (!amountMapToReceive.TryGetValue(key, out amount)) { amountsToReceive.Add(0); continue; } amountsToReceive.Add(amount); amountMapToReceive[key] = amount + 1; // We're getting one, so we have one more } // Sort it ascending amountsToReceive.Sort(); // Check actual difference // We sum only values at proper indexes of giving, because user might be overpaying int difference = amountsToGive.Select((t, i) => (int) (t - amountsToReceive[i])).Sum(); // Trade is worth for us if the difference is greater than 0 // If not, we assume that the trade might be good for us in the future, unless we're bot account where we assume that inventory doesn't change return new ParseTradeResult(tradeOffer.TradeOfferID, difference > 0 ? ParseTradeResult.EResult.AcceptedWithItemLose : (Bot.BotConfig.IsBotAccount ? ParseTradeResult.EResult.RejectedPermanently : ParseTradeResult.EResult.RejectedTemporarily)); }