Files
ArchiSteamFarm/ArchiSteamFarm/Trading.cs

662 lines
27 KiB
C#
Raw Normal View History

2017-11-18 17:27:06 +01:00
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
//
2019-01-02 16:32:53 +01:00
// Copyright 2015-2019 Łukasz "JustArchi" Domeradzki
2018-07-27 04:52:14 +02:00
// Contact: JustArchi@JustArchi.net
2017-11-18 17:27:06 +01:00
//
2018-07-27 04:52:14 +02:00
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
2017-11-18 17:27:06 +01:00
//
2018-07-27 04:52:14 +02:00
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
2015-10-28 19:21:27 +01:00
2016-04-12 19:12:45 +02:00
using System;
2015-10-28 19:21:27 +01:00
using System.Collections.Generic;
2016-05-13 06:32:42 +02:00
using System.Linq;
2015-10-25 06:16:50 +01:00
using System.Threading;
using System.Threading.Tasks;
2018-09-08 01:03:55 +02:00
using ArchiSteamFarm.Collections;
2017-12-14 08:23:17 +01:00
using ArchiSteamFarm.Json;
2017-01-06 15:32:12 +01:00
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.Plugins;
using JetBrains.Annotations;
2015-10-25 06:16:50 +01:00
namespace ArchiSteamFarm {
public sealed class Trading : IDisposable {
2017-07-09 09:09:46 +02:00
internal const byte MaxItemsPerTrade = byte.MaxValue; // This is due to limit on POST size in WebBrowser
internal const byte MaxTradesPerAccount = 5; // This is limit introduced by Valve
2016-01-22 10:13:02 +01:00
2015-10-31 05:27:30 +01:00
private readonly Bot Bot;
2018-12-30 22:14:45 +01:00
private readonly ConcurrentHashSet<ulong> HandledTradeOfferIDs = new ConcurrentHashSet<ulong>();
2017-08-01 12:41:57 +02:00
private readonly SemaphoreSlim TradesSemaphore = new SemaphoreSlim(1, 1);
2016-03-15 04:51:51 +01:00
2016-11-20 00:03:27 +01:00
private bool ParsingScheduled;
2015-10-25 06:16:50 +01:00
2019-01-10 23:44:32 +01:00
internal Trading([NotNull] Bot bot) => Bot = bot ?? throw new ArgumentNullException(nameof(bot));
2015-10-25 06:16:50 +01:00
public void Dispose() => TradesSemaphore.Dispose();
internal static (Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary<ulong, uint>> FullState, Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary<ulong, uint>> TradableState) GetDividedInventoryState(IReadOnlyCollection<Steam.Asset> inventory) {
if ((inventory == null) || (inventory.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(inventory));
2018-12-15 00:27:15 +01:00
return (null, null);
}
Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary<ulong, uint>> fullState = new Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary<ulong, uint>>();
Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary<ulong, uint>> tradableState = new Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary<ulong, uint>>();
foreach (Steam.Asset item in inventory) {
(uint RealAppID, Steam.Asset.EType Type) key = (item.RealAppID, item.Type);
if (fullState.TryGetValue(key, out Dictionary<ulong, uint> fullSet)) {
if (fullSet.TryGetValue(item.ClassID, out uint amount)) {
fullSet[item.ClassID] = amount + item.Amount;
} else {
fullSet[item.ClassID] = item.Amount;
}
} else {
fullState[key] = new Dictionary<ulong, uint> { { item.ClassID, item.Amount } };
}
if (!item.Tradable) {
continue;
}
if (tradableState.TryGetValue(key, out Dictionary<ulong, uint> tradableSet)) {
if (tradableSet.TryGetValue(item.ClassID, out uint amount)) {
tradableSet[item.ClassID] = amount + item.Amount;
} else {
tradableSet[item.ClassID] = item.Amount;
}
} else {
tradableState[key] = new Dictionary<ulong, uint> { { item.ClassID, item.Amount } };
}
}
return (fullState, tradableState);
}
Implement ETradingPreferences.MatchActively This will probably need a lot more tests, tweaking and bugfixing, but basic logic is: - MatchActively added to TradingPreferences with value of 16 - User must also use SteamTradeMatcher, can't use MatchEverything - User must have statistics enabled and be eligible for being listed (no requirement of having 100 items minimum) Once all requirements are passed, statistics module will communicate with the listing and fetch match everything bots: - The matching will start in 1h since ASF start and will repeat every day (right now it starts in 1 minute to aid debugging). - Each matching is composed of up to 10 rounds maximum. - In each round ASF will fetch our inventory and inventory of listed bots in order to find MatchableTypes items to be matched. If match is found, offer is being sent and confirmed automatically. - Each set (composition of item type + appID it's from) can be matched in a single round only once, this is to minimize "items no longer available" as much as possible and also avoid a need to wait for each bot to react before sending all trades. - Round ends when we try to match a total of 20 bots, or we hit no items to match in consecutive 10 tries with 10 different bots. - If last round resulted in at least a single trade being sent, next round starts within 5 minutes since last one, otherwise matching ends and repeats the next day. We'll see how it works in practice, expect a lot of follow-up commits, unless I won't have anything to fix or improve.
2018-11-29 18:35:58 +01:00
internal static Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary<ulong, uint>> GetInventoryState(IReadOnlyCollection<Steam.Asset> inventory) {
if ((inventory == null) || (inventory.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(inventory));
2018-12-15 00:27:15 +01:00
Implement ETradingPreferences.MatchActively This will probably need a lot more tests, tweaking and bugfixing, but basic logic is: - MatchActively added to TradingPreferences with value of 16 - User must also use SteamTradeMatcher, can't use MatchEverything - User must have statistics enabled and be eligible for being listed (no requirement of having 100 items minimum) Once all requirements are passed, statistics module will communicate with the listing and fetch match everything bots: - The matching will start in 1h since ASF start and will repeat every day (right now it starts in 1 minute to aid debugging). - Each matching is composed of up to 10 rounds maximum. - In each round ASF will fetch our inventory and inventory of listed bots in order to find MatchableTypes items to be matched. If match is found, offer is being sent and confirmed automatically. - Each set (composition of item type + appID it's from) can be matched in a single round only once, this is to minimize "items no longer available" as much as possible and also avoid a need to wait for each bot to react before sending all trades. - Round ends when we try to match a total of 20 bots, or we hit no items to match in consecutive 10 tries with 10 different bots. - If last round resulted in at least a single trade being sent, next round starts within 5 minutes since last one, otherwise matching ends and repeats the next day. We'll see how it works in practice, expect a lot of follow-up commits, unless I won't have anything to fix or improve.
2018-11-29 18:35:58 +01:00
return null;
}
Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary<ulong, uint>> state = new Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary<ulong, uint>>();
Implement ETradingPreferences.MatchActively This will probably need a lot more tests, tweaking and bugfixing, but basic logic is: - MatchActively added to TradingPreferences with value of 16 - User must also use SteamTradeMatcher, can't use MatchEverything - User must have statistics enabled and be eligible for being listed (no requirement of having 100 items minimum) Once all requirements are passed, statistics module will communicate with the listing and fetch match everything bots: - The matching will start in 1h since ASF start and will repeat every day (right now it starts in 1 minute to aid debugging). - Each matching is composed of up to 10 rounds maximum. - In each round ASF will fetch our inventory and inventory of listed bots in order to find MatchableTypes items to be matched. If match is found, offer is being sent and confirmed automatically. - Each set (composition of item type + appID it's from) can be matched in a single round only once, this is to minimize "items no longer available" as much as possible and also avoid a need to wait for each bot to react before sending all trades. - Round ends when we try to match a total of 20 bots, or we hit no items to match in consecutive 10 tries with 10 different bots. - If last round resulted in at least a single trade being sent, next round starts within 5 minutes since last one, otherwise matching ends and repeats the next day. We'll see how it works in practice, expect a lot of follow-up commits, unless I won't have anything to fix or improve.
2018-11-29 18:35:58 +01:00
foreach (Steam.Asset item in inventory) {
(uint RealAppID, Steam.Asset.EType Type) key = (item.RealAppID, item.Type);
if (state.TryGetValue(key, out Dictionary<ulong, uint> set)) {
Implement ETradingPreferences.MatchActively This will probably need a lot more tests, tweaking and bugfixing, but basic logic is: - MatchActively added to TradingPreferences with value of 16 - User must also use SteamTradeMatcher, can't use MatchEverything - User must have statistics enabled and be eligible for being listed (no requirement of having 100 items minimum) Once all requirements are passed, statistics module will communicate with the listing and fetch match everything bots: - The matching will start in 1h since ASF start and will repeat every day (right now it starts in 1 minute to aid debugging). - Each matching is composed of up to 10 rounds maximum. - In each round ASF will fetch our inventory and inventory of listed bots in order to find MatchableTypes items to be matched. If match is found, offer is being sent and confirmed automatically. - Each set (composition of item type + appID it's from) can be matched in a single round only once, this is to minimize "items no longer available" as much as possible and also avoid a need to wait for each bot to react before sending all trades. - Round ends when we try to match a total of 20 bots, or we hit no items to match in consecutive 10 tries with 10 different bots. - If last round resulted in at least a single trade being sent, next round starts within 5 minutes since last one, otherwise matching ends and repeats the next day. We'll see how it works in practice, expect a lot of follow-up commits, unless I won't have anything to fix or improve.
2018-11-29 18:35:58 +01:00
if (set.TryGetValue(item.ClassID, out uint amount)) {
set[item.ClassID] = amount + item.Amount;
} else {
set[item.ClassID] = item.Amount;
}
} else {
state[key] = new Dictionary<ulong, uint> { { item.ClassID, item.Amount } };
Implement ETradingPreferences.MatchActively This will probably need a lot more tests, tweaking and bugfixing, but basic logic is: - MatchActively added to TradingPreferences with value of 16 - User must also use SteamTradeMatcher, can't use MatchEverything - User must have statistics enabled and be eligible for being listed (no requirement of having 100 items minimum) Once all requirements are passed, statistics module will communicate with the listing and fetch match everything bots: - The matching will start in 1h since ASF start and will repeat every day (right now it starts in 1 minute to aid debugging). - Each matching is composed of up to 10 rounds maximum. - In each round ASF will fetch our inventory and inventory of listed bots in order to find MatchableTypes items to be matched. If match is found, offer is being sent and confirmed automatically. - Each set (composition of item type + appID it's from) can be matched in a single round only once, this is to minimize "items no longer available" as much as possible and also avoid a need to wait for each bot to react before sending all trades. - Round ends when we try to match a total of 20 bots, or we hit no items to match in consecutive 10 tries with 10 different bots. - If last round resulted in at least a single trade being sent, next round starts within 5 minutes since last one, otherwise matching ends and repeats the next day. We'll see how it works in practice, expect a lot of follow-up commits, unless I won't have anything to fix or improve.
2018-11-29 18:35:58 +01:00
}
}
return state;
Implement ETradingPreferences.MatchActively This will probably need a lot more tests, tweaking and bugfixing, but basic logic is: - MatchActively added to TradingPreferences with value of 16 - User must also use SteamTradeMatcher, can't use MatchEverything - User must have statistics enabled and be eligible for being listed (no requirement of having 100 items minimum) Once all requirements are passed, statistics module will communicate with the listing and fetch match everything bots: - The matching will start in 1h since ASF start and will repeat every day (right now it starts in 1 minute to aid debugging). - Each matching is composed of up to 10 rounds maximum. - In each round ASF will fetch our inventory and inventory of listed bots in order to find MatchableTypes items to be matched. If match is found, offer is being sent and confirmed automatically. - Each set (composition of item type + appID it's from) can be matched in a single round only once, this is to minimize "items no longer available" as much as possible and also avoid a need to wait for each bot to react before sending all trades. - Round ends when we try to match a total of 20 bots, or we hit no items to match in consecutive 10 tries with 10 different bots. - If last round resulted in at least a single trade being sent, next round starts within 5 minutes since last one, otherwise matching ends and repeats the next day. We'll see how it works in practice, expect a lot of follow-up commits, unless I won't have anything to fix or improve.
2018-11-29 18:35:58 +01:00
}
internal static HashSet<Steam.Asset> GetTradableItemsFromInventory(IReadOnlyCollection<Steam.Asset> inventory, IDictionary<ulong, uint> classIDs) {
Implement ETradingPreferences.MatchActively This will probably need a lot more tests, tweaking and bugfixing, but basic logic is: - MatchActively added to TradingPreferences with value of 16 - User must also use SteamTradeMatcher, can't use MatchEverything - User must have statistics enabled and be eligible for being listed (no requirement of having 100 items minimum) Once all requirements are passed, statistics module will communicate with the listing and fetch match everything bots: - The matching will start in 1h since ASF start and will repeat every day (right now it starts in 1 minute to aid debugging). - Each matching is composed of up to 10 rounds maximum. - In each round ASF will fetch our inventory and inventory of listed bots in order to find MatchableTypes items to be matched. If match is found, offer is being sent and confirmed automatically. - Each set (composition of item type + appID it's from) can be matched in a single round only once, this is to minimize "items no longer available" as much as possible and also avoid a need to wait for each bot to react before sending all trades. - Round ends when we try to match a total of 20 bots, or we hit no items to match in consecutive 10 tries with 10 different bots. - If last round resulted in at least a single trade being sent, next round starts within 5 minutes since last one, otherwise matching ends and repeats the next day. We'll see how it works in practice, expect a lot of follow-up commits, unless I won't have anything to fix or improve.
2018-11-29 18:35:58 +01:00
if ((inventory == null) || (inventory.Count == 0) || (classIDs == null) || (classIDs.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(inventory) + " || " + nameof(classIDs));
2018-12-15 00:27:15 +01:00
Implement ETradingPreferences.MatchActively This will probably need a lot more tests, tweaking and bugfixing, but basic logic is: - MatchActively added to TradingPreferences with value of 16 - User must also use SteamTradeMatcher, can't use MatchEverything - User must have statistics enabled and be eligible for being listed (no requirement of having 100 items minimum) Once all requirements are passed, statistics module will communicate with the listing and fetch match everything bots: - The matching will start in 1h since ASF start and will repeat every day (right now it starts in 1 minute to aid debugging). - Each matching is composed of up to 10 rounds maximum. - In each round ASF will fetch our inventory and inventory of listed bots in order to find MatchableTypes items to be matched. If match is found, offer is being sent and confirmed automatically. - Each set (composition of item type + appID it's from) can be matched in a single round only once, this is to minimize "items no longer available" as much as possible and also avoid a need to wait for each bot to react before sending all trades. - Round ends when we try to match a total of 20 bots, or we hit no items to match in consecutive 10 tries with 10 different bots. - If last round resulted in at least a single trade being sent, next round starts within 5 minutes since last one, otherwise matching ends and repeats the next day. We'll see how it works in practice, expect a lot of follow-up commits, unless I won't have anything to fix or improve.
2018-11-29 18:35:58 +01:00
return null;
}
HashSet<Steam.Asset> result = new HashSet<Steam.Asset>();
foreach (Steam.Asset item in inventory.Where(item => item.Tradable)) {
Implement ETradingPreferences.MatchActively This will probably need a lot more tests, tweaking and bugfixing, but basic logic is: - MatchActively added to TradingPreferences with value of 16 - User must also use SteamTradeMatcher, can't use MatchEverything - User must have statistics enabled and be eligible for being listed (no requirement of having 100 items minimum) Once all requirements are passed, statistics module will communicate with the listing and fetch match everything bots: - The matching will start in 1h since ASF start and will repeat every day (right now it starts in 1 minute to aid debugging). - Each matching is composed of up to 10 rounds maximum. - In each round ASF will fetch our inventory and inventory of listed bots in order to find MatchableTypes items to be matched. If match is found, offer is being sent and confirmed automatically. - Each set (composition of item type + appID it's from) can be matched in a single round only once, this is to minimize "items no longer available" as much as possible and also avoid a need to wait for each bot to react before sending all trades. - Round ends when we try to match a total of 20 bots, or we hit no items to match in consecutive 10 tries with 10 different bots. - If last round resulted in at least a single trade being sent, next round starts within 5 minutes since last one, otherwise matching ends and repeats the next day. We'll see how it works in practice, expect a lot of follow-up commits, unless I won't have anything to fix or improve.
2018-11-29 18:35:58 +01:00
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 static bool IsEmptyForMatching(IReadOnlyDictionary<(uint AppID, Steam.Asset.EType Type), Dictionary<ulong, uint>> fullState, IReadOnlyDictionary<(uint AppID, Steam.Asset.EType Type), Dictionary<ulong, uint>> tradableState) {
if ((fullState == null) || (tradableState == null)) {
ASF.ArchiLogger.LogNullError(nameof(fullState) + " || " + nameof(tradableState));
2018-12-15 00:27:15 +01:00
return false;
}
foreach (KeyValuePair<(uint AppID, Steam.Asset.EType Type), Dictionary<ulong, uint>> tradableSet in tradableState) {
2018-12-09 21:26:22 +01:00
if (!fullState.TryGetValue(tradableSet.Key, out Dictionary<ulong, uint> fullSet) || (fullSet == null) || (fullSet.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(fullSet));
2018-12-15 00:27:15 +01:00
2018-12-09 21:26:22 +01:00
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<ulong, uint> fullSet, IReadOnlyDictionary<ulong, uint> tradableSet) {
if ((fullSet == null) || (tradableSet == null)) {
ASF.ArchiLogger.LogNullError(nameof(fullSet) + " || " + nameof(tradableSet));
2018-12-15 00:27:15 +01:00
2018-12-09 21:26:22 +01:00
return false;
}
foreach (KeyValuePair<ulong, uint> tradableItem in tradableSet) {
switch (tradableItem.Value) {
case 0:
2018-12-15 00:27:15 +01:00
2018-12-09 21:26:22 +01:00
// 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));
2018-12-15 00:27:15 +01:00
2018-12-09 21:26:22 +01:00
return false;
case 1:
2018-12-15 00:27:15 +01:00
2018-12-09 21:26:22 +01:00
// 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));
2018-12-15 00:27:15 +01:00
return false;
2018-12-09 21:26:22 +01:00
}
if (fullAmount > 1) {
// If we have a single tradable item but more than 1 in total, this is matchable
return false;
2018-12-09 21:26:22 +01:00
}
// A single exclusive tradable item is not matchable, continue
continue;
default:
2018-12-15 00:27:15 +01:00
2018-12-09 21:26:22 +01:00
// Any other combination of tradable items is always matchable
return false;
}
}
// We didn't find any matchable combinations, so this inventory is empty
return true;
}
internal static bool IsFairTypesExchange(IReadOnlyCollection<Steam.Asset> itemsToGive, IReadOnlyCollection<Steam.Asset> itemsToReceive) {
if ((itemsToGive == null) || (itemsToGive.Count == 0) || (itemsToReceive == null) || (itemsToReceive.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(itemsToGive) + " || " + nameof(itemsToReceive));
2018-12-15 00:27:15 +01:00
return false;
}
Dictionary<uint, Dictionary<Steam.Asset.EType, uint>> itemsToGivePerGame = new Dictionary<uint, Dictionary<Steam.Asset.EType, uint>>();
2018-12-15 00:27:15 +01:00
foreach (Steam.Asset item in itemsToGive) {
if (itemsToGivePerGame.TryGetValue(item.RealAppID, out Dictionary<Steam.Asset.EType, uint> itemsPerType)) {
itemsPerType[item.Type] = itemsPerType.TryGetValue(item.Type, out uint amount) ? amount + item.Amount : item.Amount;
} else {
itemsPerType = new Dictionary<Steam.Asset.EType, uint> { [item.Type] = item.Amount };
itemsToGivePerGame[item.RealAppID] = itemsPerType;
}
}
Dictionary<uint, Dictionary<Steam.Asset.EType, uint>> itemsToReceivePerGame = new Dictionary<uint, Dictionary<Steam.Asset.EType, uint>>();
2018-12-15 00:27:15 +01:00
foreach (Steam.Asset item in itemsToReceive) {
if (itemsToReceivePerGame.TryGetValue(item.RealAppID, out Dictionary<Steam.Asset.EType, uint> itemsPerType)) {
itemsPerType[item.Type] = itemsPerType.TryGetValue(item.Type, out uint amount) ? amount + item.Amount : item.Amount;
} else {
itemsPerType = new Dictionary<Steam.Asset.EType, uint> { [item.Type] = item.Amount };
itemsToReceivePerGame[item.RealAppID] = itemsPerType;
}
}
// Ensure that amount of items to give is at least amount of items to receive (per game and per type)
foreach (KeyValuePair<uint, Dictionary<Steam.Asset.EType, uint>> itemsPerGame in itemsToGivePerGame) {
if (!itemsToReceivePerGame.TryGetValue(itemsPerGame.Key, out Dictionary<Steam.Asset.EType, uint> otherItemsPerType)) {
return false;
}
foreach (KeyValuePair<Steam.Asset.EType, uint> itemsPerType in itemsPerGame.Value) {
if (!otherItemsPerType.TryGetValue(itemsPerType.Key, out uint otherAmount)) {
return false;
}
if (itemsPerType.Value > otherAmount) {
return false;
}
}
}
return true;
}
2018-12-30 22:14:45 +01:00
internal void OnDisconnected() => HandledTradeOfferIDs.Clear();
internal async Task OnNewTrade() {
// We aim to have a maximum of 2 tasks, one already working, and one waiting in the queue
2016-11-20 00:03:27 +01:00
// This way we can call this function as many times as needed e.g. because of Steam events
2016-03-24 14:18:07 +01:00
lock (TradesSemaphore) {
2016-11-20 00:03:27 +01:00
if (ParsingScheduled) {
2016-04-26 18:01:19 +02:00
return;
2016-03-24 14:18:07 +01:00
}
2016-11-20 00:03:27 +01:00
ParsingScheduled = true;
2016-03-10 01:20:17 +01:00
}
2015-11-01 02:04:44 +01:00
2016-03-24 14:18:07 +01:00
await TradesSemaphore.WaitAsync().ConfigureAwait(false);
2015-11-01 02:04:44 +01:00
2016-11-20 00:03:27 +01:00
try {
lock (TradesSemaphore) {
ParsingScheduled = false;
}
2016-03-10 01:20:17 +01:00
2018-12-08 01:45:13 +01:00
using (await Bot.Actions.GetTradingLock().ConfigureAwait(false)) {
await ParseActiveTrades().ConfigureAwait(false);
}
2016-11-20 00:03:27 +01:00
} finally {
TradesSemaphore.Release();
}
2015-10-25 06:16:50 +01:00
}
2018-08-01 23:11:15 +02:00
private static Dictionary<(uint AppID, Steam.Asset.EType Type), List<uint>> GetInventorySets(IReadOnlyCollection<Steam.Asset> inventory) {
if ((inventory == null) || (inventory.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(inventory));
2018-12-15 00:27:15 +01:00
2018-08-01 23:11:15 +02:00
return null;
}
Implement ETradingPreferences.MatchActively This will probably need a lot more tests, tweaking and bugfixing, but basic logic is: - MatchActively added to TradingPreferences with value of 16 - User must also use SteamTradeMatcher, can't use MatchEverything - User must have statistics enabled and be eligible for being listed (no requirement of having 100 items minimum) Once all requirements are passed, statistics module will communicate with the listing and fetch match everything bots: - The matching will start in 1h since ASF start and will repeat every day (right now it starts in 1 minute to aid debugging). - Each matching is composed of up to 10 rounds maximum. - In each round ASF will fetch our inventory and inventory of listed bots in order to find MatchableTypes items to be matched. If match is found, offer is being sent and confirmed automatically. - Each set (composition of item type + appID it's from) can be matched in a single round only once, this is to minimize "items no longer available" as much as possible and also avoid a need to wait for each bot to react before sending all trades. - Round ends when we try to match a total of 20 bots, or we hit no items to match in consecutive 10 tries with 10 different bots. - If last round resulted in at least a single trade being sent, next round starts within 5 minutes since last one, otherwise matching ends and repeats the next day. We'll see how it works in practice, expect a lot of follow-up commits, unless I won't have anything to fix or improve.
2018-11-29 18:35:58 +01:00
Dictionary<(uint AppID, Steam.Asset.EType Type), Dictionary<ulong, uint>> sets = GetInventoryState(inventory);
2018-12-15 00:27:15 +01:00
2018-10-05 03:37:49 +02:00
return sets.ToDictionary(set => set.Key, set => set.Value.Values.OrderBy(amount => amount).ToList());
2018-08-01 23:11:15 +02:00
}
private static bool IsTradeNeutralOrBetter(HashSet<Steam.Asset> inventory, IReadOnlyCollection<Steam.Asset> itemsToGive, IReadOnlyCollection<Steam.Asset> itemsToReceive) {
2017-07-23 10:27:20 +02:00
if ((inventory == null) || (inventory.Count == 0) || (itemsToGive == null) || (itemsToGive.Count == 0) || (itemsToReceive == null) || (itemsToReceive.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(inventory) + " || " + nameof(itemsToGive) + " || " + nameof(itemsToReceive));
2018-12-15 00:27:15 +01:00
2017-07-23 10:27:20 +02:00
return false;
}
2018-08-01 23:11:15 +02:00
// Input of this function is items we're expected to give/receive and our inventory (limited to realAppIDs of itemsToGive/itemsToReceive)
// The objective is to determine whether the new state is beneficial (or at least neutral) towards us
// There are a lot of factors involved here - different realAppIDs, different item types, possibility of user overpaying and more
// All of those cases should be verified by our unit tests to ensure that the logic here matches all possible cases, especially those that were incorrectly handled previously
// Firstly we get initial sets state of our inventory
Dictionary<(uint AppID, Steam.Asset.EType Type), List<uint>> initialSets = GetInventorySets(inventory);
// Once we have initial state, we remove items that we're supposed to give from our inventory
// This loop is a bit more complex due to the fact that we might have a mix of the same item splitted into different amounts
foreach (Steam.Asset itemToGive in itemsToGive) {
uint amountToGive = itemToGive.Amount;
HashSet<Steam.Asset> itemsToRemove = new HashSet<Steam.Asset>();
// Keep in mind that ClassID is unique only within appID/contextID scope - we can do it like this because we're not dealing with non-Steam items here (otherwise we'd need to check appID and contextID too)
foreach (Steam.Asset item in inventory.Where(item => item.ClassID == itemToGive.ClassID)) {
if (amountToGive >= item.Amount) {
itemsToRemove.Add(item);
amountToGive -= item.Amount;
} else {
item.Amount -= amountToGive;
amountToGive = 0;
}
if (amountToGive == 0) {
break;
}
}
2017-07-23 10:27:20 +02:00
2018-08-01 23:11:15 +02:00
if (amountToGive > 0) {
ASF.ArchiLogger.LogNullError(nameof(amountToGive));
2018-12-15 00:27:15 +01:00
2018-08-01 23:11:15 +02:00
return false;
2018-04-21 22:10:34 +02:00
}
2017-09-27 04:19:10 +02:00
2018-08-01 23:11:15 +02:00
if (itemsToRemove.Count > 0) {
inventory.ExceptWith(itemsToRemove);
2017-07-23 10:27:20 +02:00
}
}
2018-08-01 23:11:15 +02:00
// Now we can add items that we're supposed to receive, this one doesn't require advanced amounts logic since we can just add items regardless
foreach (Steam.Asset itemToReceive in itemsToReceive) {
inventory.Add(itemToReceive);
2017-07-23 10:27:20 +02:00
}
2018-08-01 23:11:15 +02:00
// Now we can get final sets state of our inventory after the exchange
Dictionary<(uint AppID, Steam.Asset.EType Type), List<uint>> finalSets = GetInventorySets(inventory);
// Once we have both states, we can check overall fairness
foreach (KeyValuePair<(uint AppID, Steam.Asset.EType Type), List<uint>> finalSet in finalSets) {
List<uint> beforeAmounts = initialSets[finalSet.Key];
List<uint> afterAmounts = finalSet.Value;
// If amount of unique items in the set decreases, this is always a bad trade (e.g. 1 1 -> 0 2)
if (afterAmounts.Count < beforeAmounts.Count) {
return false;
2018-04-21 22:10:34 +02:00
}
2017-09-27 04:19:10 +02:00
2018-08-01 23:11:15 +02:00
// If amount of unique items in the set increases, this is always a good trade (e.g. 0 2 -> 1 1)
if (afterAmounts.Count > beforeAmounts.Count) {
continue;
2017-07-23 10:27:20 +02:00
}
2018-08-01 23:11:15 +02:00
// At this point we're sure that amount of unique items stays the same, so we can evaluate actual sets
2018-10-05 03:37:49 +02:00
// We make use of the fact that our amounts are already sorted in ascending order, so we can just take the first value instead of calculating ourselves
uint beforeSets = beforeAmounts[0];
uint afterSets = afterAmounts[0];
2018-08-01 23:11:15 +02:00
// If amount of our sets for this game decreases, this is always a bad trade (e.g. 2 2 2 -> 3 2 1)
if (afterSets < beforeSets) {
return false;
}
// If amount of our sets for this game increases, this is always a good trade (e.g. 3 2 1 -> 2 2 2)
if (afterSets > beforeSets) {
continue;
}
// At this point we're sure that both number of unique items in the set stays the same, as well as number of our actual sets
2018-10-05 03:42:59 +02:00
// We need to ensure set progress here and keep in mind overpaying, so we'll calculate neutrality as a difference in amounts at appropriate indexes
2018-10-05 15:54:22 +02:00
// Neutrality can't reach value below 0 at any single point of calculation, as that would imply a loss of progress even if we'd end up with a positive value by the end
int neutrality = 0;
for (byte i = 0; i < afterAmounts.Count; i++) {
neutrality += (int) (afterAmounts[i] - beforeAmounts[i]);
if (neutrality < 0) {
return false;
}
2018-08-01 23:11:15 +02:00
}
2017-07-23 10:27:20 +02:00
}
2018-08-01 23:11:15 +02:00
// If we didn't find any reason above to reject this trade, it's at least neutral+ for us - it increases our progress towards badge completion
return true;
2017-07-23 10:27:20 +02:00
}
2015-10-25 06:16:50 +01:00
private async Task ParseActiveTrades() {
2018-12-10 21:21:08 +01:00
HashSet<Steam.TradeOffer> tradeOffers = await Bot.ArchiWebHandler.GetActiveTradeOffers().ConfigureAwait(false);
2018-12-15 00:27:15 +01:00
2016-05-13 06:32:42 +02:00
if ((tradeOffers == null) || (tradeOffers.Count == 0)) {
2015-11-01 02:04:44 +01:00
return;
}
2015-10-25 06:16:50 +01:00
2018-12-30 22:14:45 +01:00
if (HandledTradeOfferIDs.Count > 0) {
HandledTradeOfferIDs.IntersectWith(tradeOffers.Select(tradeOffer => tradeOffer.TradeOfferID));
2018-12-10 21:21:08 +01:00
}
2018-12-30 22:14:45 +01:00
IEnumerable<Task<(ParseTradeResult TradeResult, bool RequiresMobileConfirmation)>> tasks = tradeOffers.Where(tradeOffer => !HandledTradeOfferIDs.Contains(tradeOffer.TradeOfferID)).Select(ParseTrade);
2018-12-10 21:21:08 +01:00
IList<(ParseTradeResult TradeResult, bool RequiresMobileConfirmation)> results = await Utilities.InParallel(tasks).ConfigureAwait(false);
if (Bot.HasMobileAuthenticator) {
2018-12-10 21:21:08 +01:00
HashSet<ulong> mobileTradeOfferIDs = results.Where(result => (result.TradeResult != null) && (result.TradeResult.Result == ParseTradeResult.EResult.Accepted) && result.RequiresMobileConfirmation).Select(result => result.TradeResult.TradeOfferID).ToHashSet();
2018-12-15 00:27:15 +01:00
2018-10-13 00:17:45 +02:00
if (mobileTradeOfferIDs.Count > 0) {
2018-12-12 21:17:12 +01:00
if (!await Bot.Actions.AcceptConfirmations(true, Steam.ConfirmationDetails.EType.Trade, mobileTradeOfferIDs, true).ConfigureAwait(false)) {
2018-12-30 22:14:45 +01:00
HandledTradeOfferIDs.ExceptWith(mobileTradeOfferIDs);
2018-10-13 01:26:09 +02:00
return;
}
}
2016-06-19 07:37:31 +02:00
}
2016-08-15 21:47:31 +02:00
if (results.Any(result => (result.TradeResult != null) && (result.TradeResult.Result == ParseTradeResult.EResult.Accepted) && (!result.RequiresMobileConfirmation || Bot.HasMobileAuthenticator) && (result.TradeResult.ReceivingItemTypes?.Any(receivedItemType => Bot.BotConfig.LootableTypes.Contains(receivedItemType)) == true)) && Bot.BotConfig.SendOnFarmingFinished) {
2016-08-15 21:47:31 +02:00
// If we finished a trade, perform a loot if user wants to do so
await Bot.Actions.SendTradeOffer(wantedTypes: Bot.BotConfig.LootableTypes).ConfigureAwait(false);
2016-08-15 21:47:31 +02:00
}
await Core.OnBotTradeOfferResults(Bot, results.Select(result => result.TradeResult).ToHashSet()).ConfigureAwait(false);
2015-10-25 06:16:50 +01:00
}
2018-10-13 00:17:45 +02:00
private async Task<(ParseTradeResult TradeResult, bool RequiresMobileConfirmation)> ParseTrade(Steam.TradeOffer tradeOffer) {
2016-05-30 01:57:06 +02:00
if (tradeOffer == null) {
Bot.ArchiLogger.LogNullError(nameof(tradeOffer));
2018-12-15 00:27:15 +01:00
2018-10-13 00:17:45 +02:00
return (null, false);
2016-05-30 01:57:06 +02:00
}
if (tradeOffer.State != Steam.TradeOffer.ETradeOfferState.Active) {
2017-01-06 15:32:12 +01:00
Bot.ArchiLogger.LogGenericError(string.Format(Strings.ErrorIsInvalid, tradeOffer.State));
2018-12-15 00:27:15 +01:00
2018-10-13 00:17:45 +02:00
return (null, false);
2015-10-25 06:16:50 +01:00
}
2018-12-30 22:14:45 +01:00
if (!HandledTradeOfferIDs.Add(tradeOffer.TradeOfferID)) {
// We've already seen this trade, this should not happen
Bot.ArchiLogger.LogGenericError(string.Format(Strings.IgnoringTrade, tradeOffer.TradeOfferID));
2018-12-15 00:27:15 +01:00
2018-12-31 02:11:10 +01:00
return (new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.Ignored, tradeOffer.ItemsToReceive), false);
2018-12-10 22:11:15 +01:00
}
ParseTradeResult result = await ShouldAcceptTrade(tradeOffer).ConfigureAwait(false);
2018-12-15 00:27:15 +01:00
if (result == null) {
Bot.ArchiLogger.LogNullError(nameof(result));
2018-12-15 00:27:15 +01:00
2018-10-13 00:17:45 +02:00
return (null, false);
}
switch (result.Result) {
case ParseTradeResult.EResult.Ignored:
case ParseTradeResult.EResult.Rejected:
bool accept = await Core.OnBotTradeOffer(Bot, tradeOffer).ConfigureAwait(false);
if (accept) {
result.Result = ParseTradeResult.EResult.Accepted;
}
break;
}
switch (result.Result) {
2018-10-13 00:17:45 +02:00
case ParseTradeResult.EResult.Accepted:
2017-01-06 15:32:12 +01:00
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.AcceptingTrade, tradeOffer.TradeOfferID));
2018-04-21 21:58:18 +02:00
2018-10-13 00:17:45 +02:00
(bool success, bool requiresMobileConfirmation) = await Bot.ArchiWebHandler.AcceptTradeOffer(tradeOffer.TradeOfferID).ConfigureAwait(false);
2018-12-30 22:14:45 +01:00
if (!success) {
result.Result = ParseTradeResult.EResult.TryAgain;
goto case ParseTradeResult.EResult.TryAgain;
}
if (tradeOffer.ItemsToReceive.Sum(item => item.Amount) > tradeOffer.ItemsToGive.Sum(item => item.Amount)) {
Bot.ArchiLogger.LogGenericTrace(string.Format(Strings.BotAcceptedDonationTrade, tradeOffer.TradeOfferID));
}
2018-12-30 22:14:45 +01:00
return (result, requiresMobileConfirmation);
case ParseTradeResult.EResult.Blacklisted:
2018-12-30 23:21:44 +01:00
case ParseTradeResult.EResult.Rejected when Bot.BotConfig.BotBehaviour.HasFlag(BotConfig.EBotBehaviour.RejectInvalidTrades):
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.RejectingTrade, tradeOffer.TradeOfferID));
2018-12-30 22:14:45 +01:00
if (!await Bot.ArchiWebHandler.DeclineTradeOffer(tradeOffer.TradeOfferID).ConfigureAwait(false)) {
result.Result = ParseTradeResult.EResult.TryAgain;
goto case ParseTradeResult.EResult.TryAgain;
}
2018-12-15 00:27:15 +01:00
2018-10-13 00:17:45 +02:00
return (result, false);
2018-12-30 22:14:45 +01:00
case ParseTradeResult.EResult.Ignored:
2018-12-30 23:21:44 +01:00
case ParseTradeResult.EResult.Rejected:
2018-12-10 21:21:08 +01:00
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.IgnoringTrade, tradeOffer.TradeOfferID));
2018-12-15 00:27:15 +01:00
2018-12-10 21:21:08 +01:00
return (result, false);
2018-12-30 22:14:45 +01:00
case ParseTradeResult.EResult.TryAgain:
HandledTradeOfferIDs.Remove(tradeOffer.TradeOfferID);
goto case ParseTradeResult.EResult.Ignored;
2017-06-19 08:23:01 +02:00
default:
Bot.ArchiLogger.LogGenericError(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(result.Result), result.Result));
2018-12-15 00:27:15 +01:00
2018-10-13 00:17:45 +02:00
return (null, false);
2016-06-19 13:59:56 +02:00
}
2015-10-25 06:16:50 +01:00
}
2016-04-01 20:18:21 +02:00
2019-01-10 23:44:32 +01:00
[ItemCanBeNull]
private async Task<ParseTradeResult> ShouldAcceptTrade(Steam.TradeOffer tradeOffer) {
2016-04-01 20:18:21 +02:00
if (tradeOffer == null) {
Bot.ArchiLogger.LogNullError(nameof(tradeOffer));
2018-12-15 00:27:15 +01:00
return null;
2016-04-01 20:18:21 +02:00
}
2017-03-28 21:27:01 +02:00
if (tradeOffer.OtherSteamID64 != 0) {
// Always accept trades from SteamMasterID
if (Bot.Access.IsMaster(tradeOffer.OtherSteamID64)) {
2018-10-13 00:29:58 +02:00
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.Accepted, tradeOffer.ItemsToReceive);
2017-03-28 21:27:01 +02:00
}
// Always deny trades from blacklisted steamIDs
2017-03-28 21:27:01 +02:00
if (Bot.IsBlacklistedFromTrades(tradeOffer.OtherSteamID64)) {
2018-12-30 22:14:45 +01:00
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.Blacklisted, tradeOffer.ItemsToReceive);
2017-03-28 21:27:01 +02:00
}
2016-10-27 22:01:38 +02:00
}
2016-10-21 21:33:55 +02:00
// Check if it's donation trade
2018-04-21 21:52:04 +02:00
switch (tradeOffer.ItemsToGive.Count) {
case 0 when tradeOffer.ItemsToReceive.Count == 0:
2018-12-15 00:27:15 +01:00
2018-12-30 22:14:45 +01:00
// If it's steam issue, try again later
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.TryAgain, tradeOffer.ItemsToReceive);
2018-04-21 21:52:04 +02:00
case 0:
2018-12-15 00:27:15 +01:00
2018-04-21 21:52:04 +02:00
// Otherwise react accordingly, depending on our preference
bool acceptDonations = Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.AcceptDonations);
bool acceptBotTrades = !Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.DontAcceptBotTrades);
// If we accept donations and bot trades, accept it right away
if (acceptDonations && acceptBotTrades) {
2018-10-13 00:29:58 +02:00
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.Accepted, tradeOffer.ItemsToReceive);
2018-04-21 21:52:04 +02:00
}
2018-04-21 21:52:04 +02:00
// If we don't accept donations, neither bot trades, deny it right away
if (!acceptDonations && !acceptBotTrades) {
2018-12-30 23:21:44 +01:00
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.Rejected, tradeOffer.ItemsToReceive);
2018-04-21 21:52:04 +02:00
}
2016-10-21 21:33:55 +02:00
2018-04-21 21:52:04 +02:00
// Otherwise we either accept donations but not bot trades, or we accept bot trades but not donations
2018-09-23 02:17:17 +02:00
bool isBotTrade = (tradeOffer.OtherSteamID64 != 0) && Bot.Bots.Values.Any(bot => bot.SteamID == tradeOffer.OtherSteamID64);
2018-12-15 00:27:15 +01:00
2018-12-30 23:21:44 +01:00
return new ParseTradeResult(tradeOffer.TradeOfferID, (acceptDonations && !isBotTrade) || (acceptBotTrades && isBotTrade) ? ParseTradeResult.EResult.Accepted : ParseTradeResult.EResult.Rejected, tradeOffer.ItemsToReceive);
2016-04-01 20:18:21 +02:00
}
// If we don't have SteamTradeMatcher enabled, this is the end for us
if (!Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.SteamTradeMatcher)) {
2018-12-30 23:21:44 +01:00
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.Rejected, tradeOffer.ItemsToReceive);
}
2018-12-30 23:21:44 +01:00
// Decline trade if we're giving more count-wise, this is a very naive pre-check, it'll be strengthened in more detailed fair types exchange next
2016-05-06 23:31:00 +02:00
if (tradeOffer.ItemsToGive.Count > tradeOffer.ItemsToReceive.Count) {
2018-12-30 23:21:44 +01:00
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.Rejected, tradeOffer.ItemsToReceive);
}
2018-04-23 22:54:27 +02:00
// Decline trade if we're requested to handle any not-accepted item type or if it's not fair games/types exchange
if (!tradeOffer.IsValidSteamItemsRequest(Bot.BotConfig.MatchableTypes) || !IsFairTypesExchange(tradeOffer.ItemsToGive, tradeOffer.ItemsToReceive)) {
2018-12-30 23:21:44 +01:00
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.Rejected, tradeOffer.ItemsToReceive);
}
2016-04-01 20:18:21 +02:00
// At this point we're sure that STM trade is valid
2016-06-27 01:45:41 +02:00
// Fetch trade hold duration
byte? holdDuration = await Bot.GetTradeHoldDuration(tradeOffer.OtherSteamID64, tradeOffer.TradeOfferID).ConfigureAwait(false);
2018-12-15 00:27:15 +01:00
2016-06-27 01:45:41 +02:00
if (!holdDuration.HasValue) {
2018-12-30 22:14:45 +01:00
// If we can't get trade hold duration, try again later
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.TryAgain, tradeOffer.ItemsToReceive);
2016-06-27 01:45:41 +02:00
}
2016-06-27 01:45:41 +02:00
// 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 => ((item.Type == Steam.Asset.EType.FoilTradingCard) || (item.Type == Steam.Asset.EType.TradingCard)) && CardsFarmer.SalesBlacklist.Contains(item.RealAppID))) {
2018-12-30 23:21:44 +01:00
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.Rejected, tradeOffer.ItemsToReceive);
}
}
2016-10-21 21:33:55 +02:00
// If we're matching everything, this is enough for us
if (Bot.BotConfig.TradingPreferences.HasFlag(BotConfig.ETradingPreferences.MatchEverything)) {
2018-10-13 00:29:58 +02:00
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.Accepted, tradeOffer.ItemsToReceive);
2016-10-21 21:33:55 +02:00
}
// Get sets we're interested in
HashSet<(uint AppID, Steam.Asset.EType Type)> wantedSets = new HashSet<(uint AppID, Steam.Asset.EType Type)>();
foreach (Steam.Asset item in tradeOffer.ItemsToGive) {
wantedSets.Add((item.RealAppID, item.Type));
}
// Now check if it's worth for us to do the trade
HashSet<Steam.Asset> inventory = await Bot.ArchiWebHandler.GetInventory(Bot.SteamID, wantedSets: wantedSets).ConfigureAwait(false);
2018-12-15 00:27:15 +01:00
2016-05-13 06:32:42 +02:00
if ((inventory == null) || (inventory.Count == 0)) {
2018-12-30 22:14:45 +01:00
// If we can't check our inventory when not using MatchEverything, this is a temporary failure, try again later
2017-03-21 15:57:34 +01:00
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.ErrorIsEmpty, nameof(inventory)));
2018-12-15 00:27:15 +01:00
2018-12-30 22:14:45 +01:00
return new ParseTradeResult(tradeOffer.TradeOfferID, ParseTradeResult.EResult.TryAgain, tradeOffer.ItemsToReceive);
}
bool accept = IsTradeNeutralOrBetter(inventory, tradeOffer.ItemsToGive, tradeOffer.ItemsToReceive);
2018-12-30 23:21:44 +01:00
// We're now sure whether the trade is neutral+ for us or not
return new ParseTradeResult(tradeOffer.TradeOfferID, accept ? ParseTradeResult.EResult.Accepted : ParseTradeResult.EResult.Rejected, tradeOffer.ItemsToReceive);
}
public sealed class ParseTradeResult {
[PublicAPI]
public readonly ulong TradeOfferID;
2018-10-13 01:13:58 +02:00
internal readonly HashSet<Steam.Asset.EType> ReceivingItemTypes;
[PublicAPI]
public EResult Result { get; internal set; }
2018-12-30 22:14:45 +01:00
2018-10-13 00:29:58 +02:00
internal ParseTradeResult(ulong tradeOfferID, EResult result, IReadOnlyCollection<Steam.Asset> itemsToReceive = null) {
2018-10-13 00:17:45 +02:00
if ((tradeOfferID == 0) || (result == EResult.Unknown)) {
throw new ArgumentNullException(nameof(tradeOfferID) + " || " + nameof(result));
}
2018-10-13 00:17:45 +02:00
TradeOfferID = tradeOfferID;
Result = result;
2018-10-13 00:29:58 +02:00
if ((itemsToReceive != null) && (itemsToReceive.Count > 0)) {
2018-10-13 01:13:58 +02:00
ReceivingItemTypes = itemsToReceive.Select(item => item.Type).ToHashSet();
2018-10-13 00:29:58 +02:00
}
}
public enum EResult : byte {
Unknown,
2018-10-13 00:17:45 +02:00
Accepted,
2018-12-30 22:14:45 +01:00
Blacklisted,
Ignored,
2018-12-30 23:21:44 +01:00
Rejected,
2018-12-30 22:14:45 +01:00
TryAgain
}
}
2015-10-25 06:16:50 +01:00
}
2018-08-01 23:11:15 +02:00
}