This commit is contained in:
JustArchi
2017-07-23 10:27:20 +02:00
parent 54d1b8211b
commit 7bb3b38ea4
9 changed files with 377 additions and 305 deletions

View File

@@ -29,7 +29,6 @@
<PackageReference Include="HtmlAgilityPack" Version="1.5.2-beta3" />
<PackageReference Include="Humanizer" Version="2.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="Nito.AsyncEx.Coordination" Version="5.0.0-pre-02" />
<PackageReference Include="NLog" Version="5.0.0-beta09" />
<PackageReference Include="SteamKit2" Version="2.0.0-Alpha5" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.4.0-preview2-25405-01" />

View File

@@ -81,7 +81,7 @@ namespace ArchiSteamFarm {
private readonly Timer HeartBeatTimer;
private readonly SemaphoreSlim InitializationSemaphore = new SemaphoreSlim(1);
private readonly SemaphoreSlim LootingSemaphore = new SemaphoreSlim(1);
private readonly ConcurrentHashSet<uint> OwnedPackageIDs = new ConcurrentHashSet<uint>();
private readonly ConcurrentDictionary<uint, (EPaymentMethod PaymentMethod, DateTime TimeCreated)> OwnedPackageIDs = new ConcurrentDictionary<uint, (EPaymentMethod PaymentMethod, DateTime TimeCreated)>();
private readonly Statistics Statistics;
private readonly SteamApps SteamApps;
private readonly SteamClient SteamClient;
@@ -313,10 +313,38 @@ namespace ArchiSteamFarm {
return null;
}
internal async Task<uint> GetAppIDForIdling(uint appID, bool allowRecursiveDiscovery = true) {
internal async Task<(uint PlayableAppID, DateTime IgnoredUntil)> GetAppDataForIdling(uint appID, bool allowRecursiveDiscovery = true) {
if (appID == 0) {
ArchiLogger.LogNullError(nameof(appID));
return 0;
return (0, DateTime.MinValue);
}
if (!BotConfig.IdleRefundableGames) {
if (!Program.GlobalDatabase.AppIDsToPackageIDs.TryGetValue(appID, out ConcurrentHashSet<uint> packageIDs)) {
return (0, DateTime.MinValue);
}
if (packageIDs.Count > 0) {
DateTime mostRecent = DateTime.MinValue;
// ReSharper disable once LoopCanBePartlyConvertedToQuery - C# 7.0 out can't be used within LINQ query yet | https://github.com/dotnet/roslyn/issues/15619
foreach (uint packageID in packageIDs) {
if (!OwnedPackageIDs.TryGetValue(packageID, out (EPaymentMethod PaymentMethod, DateTime TimeCreated) packageData)) {
continue;
}
if ((packageData.PaymentMethod != EPaymentMethod.ActivationCode) && (packageData.TimeCreated > mostRecent)) {
mostRecent = packageData.TimeCreated;
}
}
if (mostRecent != DateTime.MinValue) {
DateTime playableIn = mostRecent.AddDays(14);
if (playableIn > DateTime.UtcNow) {
return (0, playableIn);
}
}
}
}
AsyncJobMultiple<SteamApps.PICSProductInfoCallback>.ResultSet productInfoResultSet;
@@ -325,7 +353,7 @@ namespace ArchiSteamFarm {
productInfoResultSet = await SteamApps.PICSGetProductInfo(appID, null, false);
} catch (Exception e) {
ArchiLogger.LogGenericException(e);
return appID;
return (0, DateTime.MinValue);
}
// ReSharper disable once LoopCanBePartlyConvertedToQuery - C# 7.0 out can't be used within LINQ query yet | https://github.com/dotnet/roslyn/issues/15619
@@ -353,7 +381,7 @@ namespace ArchiSteamFarm {
break;
case "PRELOADONLY":
case "PRERELEASE":
return 0;
return (0, DateTime.MinValue);
default:
ArchiLogger.LogGenericWarning(string.Format(Strings.WarningUnknownValuePleaseReport, nameof(releaseState), releaseState));
break;
@@ -362,7 +390,7 @@ namespace ArchiSteamFarm {
string type = commonProductInfo["type"].Value;
if (string.IsNullOrEmpty(type)) {
return appID;
return (appID, DateTime.MinValue);
}
// We must convert this to uppercase, since Valve doesn't stick to any convention and we can have a case mismatch
@@ -373,7 +401,7 @@ namespace ArchiSteamFarm {
case "GAME":
case "MOVIE":
case "VIDEO":
return appID;
return (appID, DateTime.MinValue);
// Types that can't be idled
case "ADVERTISING":
@@ -390,12 +418,12 @@ namespace ArchiSteamFarm {
}
if (!allowRecursiveDiscovery) {
return 0;
return (0, DateTime.MinValue);
}
string listOfDlc = productInfo["extended"]["listofdlc"].Value;
if (string.IsNullOrEmpty(listOfDlc)) {
return appID;
return (appID, DateTime.MinValue);
}
string[] dlcAppIDsString = listOfDlc.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
@@ -405,16 +433,81 @@ namespace ArchiSteamFarm {
break;
}
dlcAppID = await GetAppIDForIdling(dlcAppID, false).ConfigureAwait(false);
if (dlcAppID != 0) {
return dlcAppID;
(uint PlayableAppID, DateTime IgnoredUntil) dlcAppData = await GetAppDataForIdling(dlcAppID, false).ConfigureAwait(false);
if (dlcAppData.PlayableAppID != 0) {
return (dlcAppData.PlayableAppID, DateTime.MinValue);
}
}
return appID;
return (appID, DateTime.MinValue);
}
return appID;
if (!productInfoResultSet.Complete || productInfoResultSet.Failed) {
return (0, DateTime.MinValue);
}
return (appID, DateTime.MinValue);
}
internal async Task<Dictionary<uint, HashSet<uint>>> GetAppIDsToPackageIDs(IEnumerable<uint> packageIDs) {
AsyncJobMultiple<SteamApps.PICSProductInfoCallback>.ResultSet productInfoResultSet;
try {
productInfoResultSet = await SteamApps.PICSGetProductInfo(Enumerable.Empty<uint>(), packageIDs);
} catch (Exception e) {
ArchiLogger.LogGenericException(e);
return null;
}
Dictionary<uint, HashSet<uint>> result = new Dictionary<uint, HashSet<uint>>();
foreach (KeyValuePair<uint, SteamApps.PICSProductInfoCallback.PICSProductInfo> productInfoPackages in productInfoResultSet.Results.SelectMany(productInfoResult => productInfoResult.Packages)) {
KeyValue productInfo = productInfoPackages.Value.KeyValues;
if (productInfo == KeyValue.Invalid) {
ArchiLogger.LogNullError(nameof(productInfo));
return null;
}
KeyValue apps = productInfo["appids"];
if (apps == KeyValue.Invalid) {
continue;
}
if (apps.Children.Count == 0) {
if (!result.TryGetValue(0, out HashSet<uint> packages)) {
packages = new HashSet<uint>();
result[0] = packages;
}
packages.Add(productInfoPackages.Key);
continue;
}
foreach (string app in apps.Children.Select(app => app.Value)) {
if (!uint.TryParse(app, out uint appID) || (appID == 0)) {
ArchiLogger.LogNullError(nameof(appID));
return null;
}
if (!result.TryGetValue(appID, out HashSet<uint> packages)) {
packages = new HashSet<uint>();
result[appID] = packages;
}
packages.Add(productInfoPackages.Key);
}
}
foreach (uint packageID in productInfoResultSet.Results.SelectMany(productInfoResult => productInfoResult.UnknownPackages)) {
if (!result.TryGetValue(0, out HashSet<uint> packages)) {
packages = new HashSet<uint>();
result[0] = packages;
}
packages.Add(packageID);
}
return result;
}
internal static async Task InitializeSteamConfiguration(ProtocolTypes protocolTypes, uint cellID, InMemoryServerListProvider serverListProvider) {
@@ -1461,7 +1554,7 @@ namespace ArchiSteamFarm {
Trading.OnDisconnected();
FirstTradeSent = false;
HandledGifts.ClearAndTrim();
HandledGifts.Clear();
// If we initiated disconnect, do not attempt to reconnect
if (callback.UserInitiated) {
@@ -1615,7 +1708,14 @@ namespace ArchiSteamFarm {
return;
}
OwnedPackageIDs.ReplaceWith(callback.LicenseList.Select(license => license.PackageID));
OwnedPackageIDs.Clear();
foreach (SteamApps.LicenseListCallback.License license in callback.LicenseList) {
OwnedPackageIDs[license.PackageID] = (license.PaymentMethod, license.TimeCreated);
}
if ((OwnedPackageIDs.Count > 0) && !BotConfig.IdleRefundableGames) {
Program.GlobalDatabase.RefreshPackageIDs(this, OwnedPackageIDs.Keys).Forget();
}
await Task.Delay(1000).ConfigureAwait(false); // Wait a second for eventual PlayingSessionStateCallback or SharedLibraryLockStatusCallback
@@ -2972,7 +3072,7 @@ namespace ArchiSteamFarm {
foreach (string game in games) {
// Check if this is gameID
if (uint.TryParse(game, out uint gameID) && (gameID != 0)) {
if (OwnedPackageIDs.Contains(gameID)) {
if (OwnedPackageIDs.ContainsKey(gameID)) {
response.Append(FormatBotResponse(string.Format(Strings.BotOwnedAlready, gameID)));
continue;
}
@@ -3375,7 +3475,7 @@ namespace ArchiSteamFarm {
Bot thisBot = this;
bool alreadyHandled = false;
foreach (Bot bot in Bots.Where(bot => (bot.Value != currentBot) && (!redeemFlags.HasFlag(ERedeemFlags.SkipInitial) || (bot.Value != thisBot)) && bot.Value.IsConnectedAndLoggedOn && bot.Value.IsOperator(steamID) && ((items.Count == 0) || items.Keys.Any(packageID => !bot.Value.OwnedPackageIDs.Contains(packageID)))).OrderBy(bot => bot.Key).Select(bot => bot.Value)) {
foreach (Bot bot in Bots.Where(bot => (bot.Value != currentBot) && (!redeemFlags.HasFlag(ERedeemFlags.SkipInitial) || (bot.Value != thisBot)) && bot.Value.IsConnectedAndLoggedOn && bot.Value.IsOperator(steamID) && ((items.Count == 0) || items.Keys.Any(packageID => !bot.Value.OwnedPackageIDs.ContainsKey(packageID)))).OrderBy(bot => bot.Key).Select(bot => bot.Value)) {
await LimitGiftsRequestsAsync().ConfigureAwait(false);
ArchiHandler.PurchaseResponseCallback otherResult = await bot.ArchiHandler.RedeemKey(key).ConfigureAwait(false);

View File

@@ -85,6 +85,11 @@ namespace ArchiSteamFarm {
internal readonly bool HandleOfflineMessages;
#pragma warning restore 649
#pragma warning disable 649
[JsonProperty(Required = Required.DisallowNull)]
internal readonly bool IdleRefundableGames = true;
#pragma warning restore 649
#pragma warning disable 649
[JsonProperty(Required = Required.DisallowNull)]
internal readonly bool IsBotAccount;

View File

@@ -338,10 +338,10 @@ namespace ArchiSteamFarm {
continue;
}
if (IgnoredAppIDs.TryGetValue(appID, out DateTime lastPICSReport)) {
if (lastPICSReport.AddHours(HoursToIgnore) < DateTime.UtcNow) {
if (IgnoredAppIDs.TryGetValue(appID, out DateTime ignoredUntil)) {
if (ignoredUntil < DateTime.UtcNow) {
// This game served its time as being ignored
IgnoredAppIDs.TryRemove(appID, out lastPICSReport);
IgnoredAppIDs.TryRemove(appID, out _);
} else {
// This game is still ignored
continue;
@@ -555,20 +555,37 @@ namespace ArchiSteamFarm {
// If we have restricted card drops, we use complex algorithm
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.ChosenFarmingAlgorithm, "Complex"));
while (GamesToFarm.Count > 0) {
HashSet<Game> gamesToFarmSolo = GamesToFarm.Count > 1 ? new HashSet<Game>(GamesToFarm.Where(game => game.HoursPlayed >= HoursToBump)) : new HashSet<Game>(GamesToFarm);
if (gamesToFarmSolo.Count > 0) {
while (gamesToFarmSolo.Count > 0) {
Game game = gamesToFarmSolo.First();
if (await FarmSolo(game).ConfigureAwait(false)) {
gamesToFarmSolo.Remove(game);
HashSet<Game> playableGamesToFarmSolo = new HashSet<Game>();
foreach (Game game in GamesToFarm.Where(game => game.HoursPlayed >= HoursToBump)) {
if (await IsPlayableGame(game).ConfigureAwait(false)) {
playableGamesToFarmSolo.Add(game);
}
}
if (playableGamesToFarmSolo.Count > 0) {
while (playableGamesToFarmSolo.Count > 0) {
Game playableGame = playableGamesToFarmSolo.First();
if (await FarmSolo(playableGame).ConfigureAwait(false)) {
playableGamesToFarmSolo.Remove(playableGame);
} else {
NowFarming = false;
return;
}
}
} else {
if (FarmMultiple(GamesToFarm.OrderByDescending(game => game.HoursPlayed).Take(ArchiHandler.MaxGamesPlayedConcurrently))) {
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.IdlingFinishedForGames, string.Join(", ", GamesToFarm.Select(game => game.AppID))));
HashSet<Game> playableGamesToFarmMultiple = new HashSet<Game>();
foreach (Game game in GamesToFarm.Where(game => game.HoursPlayed < HoursToBump).OrderByDescending(game => game.HoursPlayed)) {
if (await IsPlayableGame(game).ConfigureAwait(false)) {
playableGamesToFarmMultiple.Add(game);
}
if (playableGamesToFarmMultiple.Count >= ArchiHandler.MaxGamesPlayedConcurrently) {
break;
}
}
if (FarmMultiple(playableGamesToFarmMultiple)) {
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.IdlingFinishedForGames, string.Join(", ", playableGamesToFarmMultiple.Select(game => game.AppID))));
} else {
NowFarming = false;
return;
@@ -579,9 +596,20 @@ namespace ArchiSteamFarm {
// If we have unrestricted card drops, we use simple algorithm
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.ChosenFarmingAlgorithm, "Simple"));
while (GamesToFarm.Count > 0) {
Game game = GamesToFarm.First();
if (await FarmSolo(game).ConfigureAwait(false)) {
continue;
Game playableGame = null;
foreach (Game game in GamesToFarm) {
if (!await IsPlayableGame(game).ConfigureAwait(false)) {
continue;
}
playableGame = game;
break;
}
if (playableGame != null) {
if (await FarmSolo(playableGame).ConfigureAwait(false)) {
continue;
}
}
NowFarming = false;
@@ -590,7 +618,7 @@ namespace ArchiSteamFarm {
}
} while ((await IsAnythingToFarm().ConfigureAwait(false)).GetValueOrDefault());
CurrentGamesFarming.ClearAndTrim();
CurrentGamesFarming.Clear();
NowFarming = false;
Bot.ArchiLogger.LogGenericInfo(Strings.IdlingFinished);
@@ -605,37 +633,31 @@ namespace ArchiSteamFarm {
bool success = true;
uint appID = await Bot.GetAppIDForIdling(game.AppID).ConfigureAwait(false);
if (appID != 0) {
if (appID != game.AppID) {
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.WarningIdlingGameMismatch, game.AppID, game.GameName, appID));
if (game.AppID != game.PlayableAppID) {
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.WarningIdlingGameMismatch, game.AppID, game.GameName, game.PlayableAppID));
}
Bot.PlayGame(game.PlayableAppID, Bot.BotConfig.CustomGamePlayedWhileFarming);
DateTime endFarmingDate = DateTime.UtcNow.AddHours(Program.GlobalConfig.MaxFarmingTime);
bool? keepFarming = await ShouldFarm(game).ConfigureAwait(false);
while (keepFarming.GetValueOrDefault(true) && (DateTime.UtcNow < endFarmingDate)) {
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.StillIdling, game.AppID, game.GameName));
DateTime startFarmingPeriod = DateTime.UtcNow;
if (FarmResetEvent.Wait(60 * 1000 * Program.GlobalConfig.FarmingDelay)) {
FarmResetEvent.Reset();
success = KeepFarming;
}
Bot.PlayGame(appID, Bot.BotConfig.CustomGamePlayedWhileFarming);
DateTime endFarmingDate = DateTime.UtcNow.AddHours(Program.GlobalConfig.MaxFarmingTime);
// Don't forget to update our GamesToFarm hours
game.HoursPlayed += (float) DateTime.UtcNow.Subtract(startFarmingPeriod).TotalHours;
bool? keepFarming = await ShouldFarm(game).ConfigureAwait(false);
while (keepFarming.GetValueOrDefault(true) && (DateTime.UtcNow < endFarmingDate)) {
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.StillIdling, game.AppID, game.GameName));
DateTime startFarmingPeriod = DateTime.UtcNow;
if (FarmResetEvent.Wait(60 * 1000 * Program.GlobalConfig.FarmingDelay)) {
FarmResetEvent.Reset();
success = KeepFarming;
}
// Don't forget to update our GamesToFarm hours
game.HoursPlayed += (float) DateTime.UtcNow.Subtract(startFarmingPeriod).TotalHours;
if (!success) {
break;
}
keepFarming = await ShouldFarm(game).ConfigureAwait(false);
if (!success) {
break;
}
} else {
IgnoredAppIDs[game.AppID] = DateTime.UtcNow;
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.IdlingGameNotPossible, game.AppID, game.GameName));
keepFarming = await ShouldFarm(game).ConfigureAwait(false);
}
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.StoppedIdling, game.AppID, game.GameName));
@@ -699,7 +721,7 @@ namespace ArchiSteamFarm {
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.NowIdlingList, string.Join(", ", CurrentGamesFarming.Select(game => game.AppID))));
bool result = FarmHours(CurrentGamesFarming);
CurrentGamesFarming.ClearAndTrim();
CurrentGamesFarming.Clear();
return result;
}
@@ -714,7 +736,7 @@ namespace ArchiSteamFarm {
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.NowIdling, game.AppID, game.GameName));
bool result = await Farm(game).ConfigureAwait(false);
CurrentGamesFarming.ClearAndTrim();
CurrentGamesFarming.Clear();
if (!result) {
return false;
@@ -783,7 +805,7 @@ namespace ArchiSteamFarm {
}
}
GamesToFarm.ClearAndTrim();
GamesToFarm.Clear();
List<Task> tasks = new List<Task>();
Task mainTask = CheckPage(htmlDocument);
@@ -830,6 +852,18 @@ namespace ArchiSteamFarm {
return true;
}
private async Task<bool> IsPlayableGame(Game game) {
(uint PlayableAppID, DateTime IgnoredUntil) appData = await Bot.GetAppDataForIdling(game.AppID).ConfigureAwait(false);
if (appData.PlayableAppID != 0) {
game.PlayableAppID = appData.PlayableAppID;
return true;
}
IgnoredAppIDs[game.AppID] = appData.IgnoredUntil != DateTime.MaxValue ? appData.IgnoredUntil : DateTime.UtcNow.AddHours(HoursToIgnore);
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.IdlingGameNotPossible, game.AppID, game.GameName));
return false;
}
private async Task<bool?> ShouldFarm(Game game) {
if (game == null) {
Bot.ArchiLogger.LogNullError(nameof(game));
@@ -911,6 +945,8 @@ namespace ArchiSteamFarm {
[JsonProperty]
internal float HoursPlayed { get; set; }
internal uint PlayableAppID { get; set; }
internal Game(uint appID, string gameName, float hoursPlayed, ushort cardsRemaining, byte badgeLevel) {
if ((appID == 0) || string.IsNullOrEmpty(gameName) || (hoursPlayed < 0) || (cardsRemaining == 0)) {
throw new ArgumentOutOfRangeException(nameof(appID) + " || " + nameof(gameName) + " || " + nameof(hoursPlayed) + " || " + nameof(cardsRemaining));
@@ -921,6 +957,8 @@ namespace ArchiSteamFarm {
HoursPlayed = hoursPlayed;
CardsRemaining = cardsRemaining;
BadgeLevel = badgeLevel;
PlayableAppID = appID;
}
public override bool Equals(object obj) {

View File

@@ -1,52 +0,0 @@
/*
_ _ _ ____ _ _____
/ \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
/ _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
/ ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
/_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
Copyright 2015-2017 Łukasz "JustArchi" Domeradzki
Contact: JustArchi@JustArchi.net
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
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.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using Nito.AsyncEx;
namespace ArchiSteamFarm {
internal sealed class ConcurrentEnumerator<T> : IEnumerator<T> {
public T Current => Enumerator.Current;
private readonly IEnumerator<T> Enumerator;
private readonly IDisposable Lock;
object IEnumerator.Current => Current;
internal ConcurrentEnumerator(ICollection<T> collection, AsyncReaderWriterLock rwLock) {
if ((collection == null) || (rwLock == null)) {
throw new ArgumentNullException(nameof(collection) + " || " + nameof(rwLock));
}
Lock = rwLock.ReaderLock();
Enumerator = collection.GetEnumerator();
}
public void Dispose() => Lock.Dispose();
public bool MoveNext() => Enumerator.MoveNext();
public void Reset() => Enumerator.Reset();
}
}

View File

@@ -23,168 +23,103 @@
*/
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Nito.AsyncEx;
namespace ArchiSteamFarm {
internal sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ISet<T> {
public int Count {
get {
using (Lock.ReaderLock()) {
return HashSet.Count;
}
}
}
internal sealed class ConcurrentHashSet<T> : ISet<T> {
public int Count => BackingCollection.Count;
public bool IsReadOnly => false;
private readonly HashSet<T> HashSet = new HashSet<T>();
private readonly AsyncReaderWriterLock Lock = new AsyncReaderWriterLock();
private readonly ConcurrentDictionary<T, bool> BackingCollection = new ConcurrentDictionary<T, bool>();
public bool Add(T item) {
using (Lock.WriterLock()) {
return HashSet.Add(item);
}
}
public void Clear() {
using (Lock.WriterLock()) {
HashSet.Clear();
}
}
public bool Contains(T item) {
using (Lock.ReaderLock()) {
return HashSet.Contains(item);
}
}
public void CopyTo(T[] array, int arrayIndex) {
using (Lock.ReaderLock()) {
HashSet.CopyTo(array, arrayIndex);
}
}
public bool Add(T item) => BackingCollection.TryAdd(item, true);
public void Clear() => BackingCollection.Clear();
public bool Contains(T item) => BackingCollection.ContainsKey(item);
public void CopyTo(T[] array, int arrayIndex) => BackingCollection.Keys.CopyTo(array, arrayIndex);
public void ExceptWith(IEnumerable<T> other) {
using (Lock.WriterLock()) {
HashSet.ExceptWith(other);
foreach (T item in other) {
Remove(item);
}
}
public IEnumerator<T> GetEnumerator() => new ConcurrentEnumerator<T>(HashSet, Lock);
public IEnumerator<T> GetEnumerator() => BackingCollection.Keys.GetEnumerator();
public void IntersectWith(IEnumerable<T> other) {
using (Lock.WriterLock()) {
HashSet.IntersectWith(other);
ICollection<T> collection = other as ICollection<T> ?? other.ToList();
foreach (T item in this.Where(item => !collection.Contains(item))) {
Remove(item);
}
}
public bool IsProperSubsetOf(IEnumerable<T> other) {
using (Lock.ReaderLock()) {
return HashSet.IsProperSubsetOf(other);
}
ICollection<T> collection = other as ICollection<T> ?? other.ToList();
return (collection.Count != Count) && IsSubsetOf(collection);
}
public bool IsProperSupersetOf(IEnumerable<T> other) {
using (Lock.ReaderLock()) {
return HashSet.IsProperSupersetOf(other);
}
ICollection<T> collection = other as ICollection<T> ?? other.ToList();
return (collection.Count != Count) && IsSupersetOf(collection);
}
public bool IsSubsetOf(IEnumerable<T> other) {
using (Lock.ReaderLock()) {
return HashSet.IsSubsetOf(other);
}
ICollection<T> collection = other as ICollection<T> ?? other.ToList();
return this.AsParallel().All(collection.Contains);
}
public bool IsSupersetOf(IEnumerable<T> other) {
using (Lock.ReaderLock()) {
return HashSet.IsSupersetOf(other);
}
}
public bool Overlaps(IEnumerable<T> other) {
using (Lock.ReaderLock()) {
return HashSet.Overlaps(other);
}
}
public bool Remove(T item) {
using (Lock.WriterLock()) {
return HashSet.Remove(item);
}
}
public bool IsSupersetOf(IEnumerable<T> other) => other.AsParallel().All(Contains);
public bool Overlaps(IEnumerable<T> other) => other.AsParallel().Any(Contains);
public bool Remove(T item) => BackingCollection.TryRemove(item, out _);
public bool SetEquals(IEnumerable<T> other) {
using (Lock.ReaderLock()) {
return HashSet.SetEquals(other);
}
ICollection<T> collection = other as ICollection<T> ?? other.ToList();
return (collection.Count == Count) && collection.AsParallel().All(Contains);
}
public void SymmetricExceptWith(IEnumerable<T> other) {
using (Lock.WriterLock()) {
HashSet.SymmetricExceptWith(other);
ICollection<T> collection = other as ICollection<T> ?? other.ToList();
HashSet<T> removed = new HashSet<T>();
foreach (T item in collection.Where(Contains)) {
removed.Add(item);
Remove(item);
}
foreach (T item in collection.Where(item => !removed.Contains(item))) {
Add(item);
}
}
public void UnionWith(IEnumerable<T> other) {
using (Lock.WriterLock()) {
HashSet.UnionWith(other);
foreach (T otherElement in other) {
Add(otherElement);
}
}
void ICollection<T>.Add(T item) => Add(item);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
internal bool AddRange(IEnumerable<T> items) {
using (Lock.WriterLock()) {
// We use Count() and not Any() because we must ensure full loop pass
return items.Count(item => HashSet.Add(item)) > 0;
// We use Count() and not Any() because we must ensure full loop pass
internal bool AddRange(IEnumerable<T> items) => items.Count(Add) > 0;
// We use Count() and not Any() because we must ensure full loop pass
internal bool RemoveRange(IEnumerable<T> items) => items.Count(Remove) > 0;
internal bool ReplaceIfNeededWith(ICollection<T> other) {
if (SetEquals(other)) {
return false;
}
ReplaceWith(other);
return true;
}
internal void ClearAndTrim() {
using (Lock.WriterLock()) {
HashSet.Clear();
HashSet.TrimExcess();
}
}
internal bool RemoveRange(IEnumerable<T> items) {
using (Lock.WriterLock()) {
// We use Count() and not Any() because we must ensure full loop pass
return items.Count(item => HashSet.Remove(item)) > 0;
}
}
internal bool ReplaceIfNeededWith(ICollection<T> items) {
using (Lock.WriterLock()) {
if (HashSet.SetEquals(items)) {
return false;
}
HashSet.Clear();
foreach (T item in items) {
HashSet.Add(item);
}
HashSet.TrimExcess();
return true;
}
}
internal void ReplaceWith(IEnumerable<T> items) {
using (Lock.WriterLock()) {
HashSet.Clear();
foreach (T item in items) {
HashSet.Add(item);
}
HashSet.TrimExcess();
internal void ReplaceWith(IEnumerable<T> other) {
BackingCollection.Clear();
foreach (T item in other) {
BackingCollection[item] = true;
}
}
}

View File

@@ -23,11 +23,19 @@
*/
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace ArchiSteamFarm {
internal sealed class GlobalDatabase : IDisposable {
[JsonProperty(Required = Required.DisallowNull)]
internal readonly ConcurrentDictionary<uint, ConcurrentHashSet<uint>> AppIDsToPackageIDs = new ConcurrentDictionary<uint, ConcurrentHashSet<uint>>();
[JsonProperty(Required = Required.DisallowNull)]
internal readonly Guid Guid = Guid.NewGuid();
@@ -36,6 +44,8 @@ namespace ArchiSteamFarm {
private readonly object FileLock = new object();
private readonly SemaphoreSlim PackagesRefreshSemaphore = new SemaphoreSlim(1);
internal uint CellID {
get => _CellID;
set {
@@ -96,6 +106,42 @@ namespace ArchiSteamFarm {
return globalDatabase;
}
internal async Task RefreshPackageIDs(Bot bot, ICollection<uint> packageIDs) {
if ((bot == null) || (packageIDs == null) || (packageIDs.Count == 0)) {
ASF.ArchiLogger.LogNullError(nameof(bot) + " || " + nameof(packageIDs));
return;
}
await PackagesRefreshSemaphore.WaitAsync().ConfigureAwait(false);
try {
HashSet<uint> missingPackageIDs = new HashSet<uint>(packageIDs.AsParallel().Where(packageID => AppIDsToPackageIDs.Values.All(packages => !packages.Contains(packageID))));
if (missingPackageIDs.Count == 0) {
return;
}
Dictionary<uint, HashSet<uint>> appIDsToPackageIDs = await bot.GetAppIDsToPackageIDs(missingPackageIDs);
if ((appIDsToPackageIDs == null) || (appIDsToPackageIDs.Count == 0)) {
return;
}
foreach (KeyValuePair<uint, HashSet<uint>> appIDtoPackageID in appIDsToPackageIDs) {
if (!AppIDsToPackageIDs.TryGetValue(appIDtoPackageID.Key, out ConcurrentHashSet<uint> packages)) {
packages = new ConcurrentHashSet<uint>();
AppIDsToPackageIDs[appIDtoPackageID.Key] = packages;
}
foreach (uint package in appIDtoPackageID.Value) {
packages.Add(package);
}
}
Save();
} finally {
PackagesRefreshSemaphore.Release();
}
}
private void OnServerListUpdated(object sender, EventArgs e) => Save();
private void Save() {

View File

@@ -45,7 +45,7 @@ namespace ArchiSteamFarm {
public void Dispose() => TradesSemaphore.Dispose();
internal void OnDisconnected() => IgnoredTrades.ClearAndTrim();
internal void OnDisconnected() => IgnoredTrades.Clear();
internal async Task OnNewTrade() {
// We aim to have a maximum of 2 tasks, one already parsing, and one waiting in the queue
@@ -71,6 +71,76 @@ namespace ArchiSteamFarm {
}
}
private static bool IsTradeNeutralOrBetter(HashSet<Steam.Item> inventory, HashSet<Steam.Item> itemsToGive, HashSet<Steam.Item> itemsToReceive) {
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));
return false;
}
// Now let's create a map which maps items to their amount in our EQ
// This has to be done as we might have multiple items of given ClassID with multiple amounts
Dictionary<ulong, uint> itemAmounts = new Dictionary<ulong, uint>();
foreach (Steam.Item item in inventory) {
if (itemAmounts.TryGetValue(item.ClassID, out uint amount)) {
itemAmounts[item.ClassID] = amount + item.Amount;
} else {
itemAmounts[item.ClassID] = item.Amount;
}
}
// Calculate our value of items to give on per-game basis
Dictionary<(Steam.Item.EType Type, uint AppID), List<uint>> itemAmountToGivePerGame = new Dictionary<(Steam.Item.EType Type, uint AppID), List<uint>>();
Dictionary<ulong, uint> itemAmountsToGive = new Dictionary<ulong, uint>(itemAmounts);
foreach (Steam.Item item in itemsToGive) {
if (!itemAmountToGivePerGame.TryGetValue((item.Type, item.RealAppID), out List<uint> amountsToGive)) {
amountsToGive = new List<uint>();
itemAmountToGivePerGame[(item.Type, item.RealAppID)] = amountsToGive;
}
if (!itemAmountsToGive.TryGetValue(item.ClassID, out uint amount)) {
amountsToGive.Add(0);
continue;
}
amountsToGive.Add(amount);
itemAmountsToGive[item.ClassID] = amount - 1; // We're giving one, so we have one less
}
// Sort all the lists of amounts to give on per-game basis ascending
foreach (List<uint> amountsToGive in itemAmountToGivePerGame.Values) {
amountsToGive.Sort();
}
// Calculate our value of items to receive on per-game basis
Dictionary<(Steam.Item.EType Type, uint AppID), List<uint>> itemAmountToReceivePerGame = new Dictionary<(Steam.Item.EType Type, uint AppID), List<uint>>();
Dictionary<ulong, uint> itemAmountsToReceive = new Dictionary<ulong, uint>(itemAmounts);
foreach (Steam.Item item in itemsToReceive) {
if (!itemAmountToReceivePerGame.TryGetValue((item.Type, item.RealAppID), out List<uint> amountsToReceive)) {
amountsToReceive = new List<uint>();
itemAmountToReceivePerGame[(item.Type, item.RealAppID)] = amountsToReceive;
}
if (!itemAmountsToReceive.TryGetValue(item.ClassID, out uint amount)) {
amountsToReceive.Add(0);
continue;
}
amountsToReceive.Add(amount);
itemAmountsToReceive[item.ClassID] = amount + 1; // We're getting one, so we have one more
}
// Sort all the lists of amounts to receive on per-game basis ascending
foreach (List<uint> amountsToReceive in itemAmountToReceivePerGame.Values) {
amountsToReceive.Sort();
}
// Calculate final neutrality result
// This is quite complex operation of taking minimum difference from all differences on per-game basis
// When calculating per-game difference, we sum only amounts at proper indexes, because user might be overpaying
int difference = itemAmountToGivePerGame.Min(kv => kv.Value.Select((t, i) => (int) (t - itemAmountToReceivePerGame[kv.Key][i])).Sum());
return difference > 0;
}
private async Task ParseActiveTrades() {
HashSet<Steam.TradeOffer> tradeOffers = await Bot.ArchiWebHandler.GetActiveTradeOffers().ConfigureAwait(false);
if ((tradeOffers == null) || (tradeOffers.Count == 0)) {
@@ -264,76 +334,6 @@ namespace ArchiSteamFarm {
return new ParseTradeResult(tradeOffer.TradeOfferID, accept ? ParseTradeResult.EResult.AcceptedWithItemLose : (Bot.BotConfig.IsBotAccount ? ParseTradeResult.EResult.RejectedPermanently : ParseTradeResult.EResult.RejectedTemporarily));
}
private static bool IsTradeNeutralOrBetter(HashSet<Steam.Item> inventory, HashSet<Steam.Item> itemsToGive, HashSet<Steam.Item> itemsToReceive) {
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));
return false;
}
// Now let's create a map which maps items to their amount in our EQ
// This has to be done as we might have multiple items of given ClassID with multiple amounts
Dictionary<ulong, uint> itemAmounts = new Dictionary<ulong, uint>();
foreach (Steam.Item item in inventory) {
if (itemAmounts.TryGetValue(item.ClassID, out uint amount)) {
itemAmounts[item.ClassID] = amount + item.Amount;
} else {
itemAmounts[item.ClassID] = item.Amount;
}
}
// Calculate our value of items to give on per-game basis
Dictionary<(Steam.Item.EType Type, uint AppID), List<uint>> itemAmountToGivePerGame = new Dictionary<(Steam.Item.EType Type, uint AppID), List<uint>>();
Dictionary<ulong, uint> itemAmountsToGive = new Dictionary<ulong, uint>(itemAmounts);
foreach (Steam.Item item in itemsToGive) {
if (!itemAmountToGivePerGame.TryGetValue((item.Type, item.RealAppID), out List<uint> amountsToGive)) {
amountsToGive = new List<uint>();
itemAmountToGivePerGame[(item.Type, item.RealAppID)] = amountsToGive;
}
if (!itemAmountsToGive.TryGetValue(item.ClassID, out uint amount)) {
amountsToGive.Add(0);
continue;
}
amountsToGive.Add(amount);
itemAmountsToGive[item.ClassID] = amount - 1; // We're giving one, so we have one less
}
// Sort all the lists of amounts to give on per-game basis ascending
foreach (List<uint> amountsToGive in itemAmountToGivePerGame.Values) {
amountsToGive.Sort();
}
// Calculate our value of items to receive on per-game basis
Dictionary<(Steam.Item.EType Type, uint AppID), List<uint>> itemAmountToReceivePerGame = new Dictionary<(Steam.Item.EType Type, uint AppID), List<uint>>();
Dictionary<ulong, uint> itemAmountsToReceive = new Dictionary<ulong, uint>(itemAmounts);
foreach (Steam.Item item in itemsToReceive) {
if (!itemAmountToReceivePerGame.TryGetValue((item.Type, item.RealAppID), out List<uint> amountsToReceive)) {
amountsToReceive = new List<uint>();
itemAmountToReceivePerGame[(item.Type, item.RealAppID)] = amountsToReceive;
}
if (!itemAmountsToReceive.TryGetValue(item.ClassID, out uint amount)) {
amountsToReceive.Add(0);
continue;
}
amountsToReceive.Add(amount);
itemAmountsToReceive[item.ClassID] = amount + 1; // We're getting one, so we have one more
}
// Sort all the lists of amounts to receive on per-game basis ascending
foreach (List<uint> amountsToReceive in itemAmountToReceivePerGame.Values) {
amountsToReceive.Sort();
}
// Calculate final neutrality result
// This is quite complex operation of taking minimum difference from all differences on per-game basis
// When calculating per-game difference, we sum only amounts at proper indexes, because user might be overpaying
int difference = itemAmountToGivePerGame.Min(kv => kv.Value.Select((t, i) => (int) (t - itemAmountToReceivePerGame[kv.Key][i])).Sum());
return difference > 0;
}
private sealed class ParseTradeResult {
internal readonly EResult Result;

View File

@@ -10,6 +10,7 @@
"FarmOffline": false,
"GamesPlayedWhileIdle": [],
"HandleOfflineMessages": false,
"IdleRefundableGames": true,
"IsBotAccount": false,
"LootableTypes": [
1,