From 7bb3b38ea449b8282692eae0a452f93b3663f7b5 Mon Sep 17 00:00:00 2001 From: JustArchi Date: Sun, 23 Jul 2017 10:27:20 +0200 Subject: [PATCH] Closes #522 --- ArchiSteamFarm/ArchiSteamFarm.csproj | 1 - ArchiSteamFarm/Bot.cs | 136 +++++++++++++++++--- ArchiSteamFarm/BotConfig.cs | 5 + ArchiSteamFarm/CardsFarmer.cs | 128 +++++++++++------- ArchiSteamFarm/ConcurrentEnumerator.cs | 52 -------- ArchiSteamFarm/ConcurrentHashSet.cs | 171 ++++++++----------------- ArchiSteamFarm/GlobalDatabase.cs | 46 +++++++ ArchiSteamFarm/Trading.cs | 142 ++++++++++---------- ArchiSteamFarm/config/example.json | 1 + 9 files changed, 377 insertions(+), 305 deletions(-) delete mode 100644 ArchiSteamFarm/ConcurrentEnumerator.cs diff --git a/ArchiSteamFarm/ArchiSteamFarm.csproj b/ArchiSteamFarm/ArchiSteamFarm.csproj index 34ec6373a..3abd48164 100644 --- a/ArchiSteamFarm/ArchiSteamFarm.csproj +++ b/ArchiSteamFarm/ArchiSteamFarm.csproj @@ -29,7 +29,6 @@ - diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index e308e1611..2e974342b 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -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 OwnedPackageIDs = new ConcurrentHashSet(); + private readonly ConcurrentDictionary OwnedPackageIDs = new ConcurrentDictionary(); private readonly Statistics Statistics; private readonly SteamApps SteamApps; private readonly SteamClient SteamClient; @@ -313,10 +313,38 @@ namespace ArchiSteamFarm { return null; } - internal async Task 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 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.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>> GetAppIDsToPackageIDs(IEnumerable packageIDs) { + AsyncJobMultiple.ResultSet productInfoResultSet; + + try { + productInfoResultSet = await SteamApps.PICSGetProductInfo(Enumerable.Empty(), packageIDs); + } catch (Exception e) { + ArchiLogger.LogGenericException(e); + return null; + } + + Dictionary> result = new Dictionary>(); + + foreach (KeyValuePair 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 packages)) { + packages = new HashSet(); + 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 packages)) { + packages = new HashSet(); + result[appID] = packages; + } + + packages.Add(productInfoPackages.Key); + } + } + + foreach (uint packageID in productInfoResultSet.Results.SelectMany(productInfoResult => productInfoResult.UnknownPackages)) { + if (!result.TryGetValue(0, out HashSet packages)) { + packages = new HashSet(); + 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); diff --git a/ArchiSteamFarm/BotConfig.cs b/ArchiSteamFarm/BotConfig.cs index e6d030a03..c9ec9a1d7 100644 --- a/ArchiSteamFarm/BotConfig.cs +++ b/ArchiSteamFarm/BotConfig.cs @@ -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; diff --git a/ArchiSteamFarm/CardsFarmer.cs b/ArchiSteamFarm/CardsFarmer.cs index 852e4115c..82d8b0e6a 100755 --- a/ArchiSteamFarm/CardsFarmer.cs +++ b/ArchiSteamFarm/CardsFarmer.cs @@ -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 gamesToFarmSolo = GamesToFarm.Count > 1 ? new HashSet(GamesToFarm.Where(game => game.HoursPlayed >= HoursToBump)) : new HashSet(GamesToFarm); - if (gamesToFarmSolo.Count > 0) { - while (gamesToFarmSolo.Count > 0) { - Game game = gamesToFarmSolo.First(); - if (await FarmSolo(game).ConfigureAwait(false)) { - gamesToFarmSolo.Remove(game); + HashSet playableGamesToFarmSolo = new HashSet(); + 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 playableGamesToFarmMultiple = new HashSet(); + 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 tasks = new List(); Task mainTask = CheckPage(htmlDocument); @@ -830,6 +852,18 @@ namespace ArchiSteamFarm { return true; } + private async Task 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 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) { diff --git a/ArchiSteamFarm/ConcurrentEnumerator.cs b/ArchiSteamFarm/ConcurrentEnumerator.cs deleted file mode 100644 index 034baade2..000000000 --- a/ArchiSteamFarm/ConcurrentEnumerator.cs +++ /dev/null @@ -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 : IEnumerator { - public T Current => Enumerator.Current; - - private readonly IEnumerator Enumerator; - private readonly IDisposable Lock; - - object IEnumerator.Current => Current; - - internal ConcurrentEnumerator(ICollection 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(); - } -} \ No newline at end of file diff --git a/ArchiSteamFarm/ConcurrentHashSet.cs b/ArchiSteamFarm/ConcurrentHashSet.cs index 1ff26a2e5..6365a2bd3 100644 --- a/ArchiSteamFarm/ConcurrentHashSet.cs +++ b/ArchiSteamFarm/ConcurrentHashSet.cs @@ -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 : IReadOnlyCollection, ISet { - public int Count { - get { - using (Lock.ReaderLock()) { - return HashSet.Count; - } - } - } - + internal sealed class ConcurrentHashSet : ISet { + public int Count => BackingCollection.Count; public bool IsReadOnly => false; - private readonly HashSet HashSet = new HashSet(); - private readonly AsyncReaderWriterLock Lock = new AsyncReaderWriterLock(); + private readonly ConcurrentDictionary BackingCollection = new ConcurrentDictionary(); - 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 other) { - using (Lock.WriterLock()) { - HashSet.ExceptWith(other); + foreach (T item in other) { + Remove(item); } } - public IEnumerator GetEnumerator() => new ConcurrentEnumerator(HashSet, Lock); + public IEnumerator GetEnumerator() => BackingCollection.Keys.GetEnumerator(); public void IntersectWith(IEnumerable other) { - using (Lock.WriterLock()) { - HashSet.IntersectWith(other); + ICollection collection = other as ICollection ?? other.ToList(); + foreach (T item in this.Where(item => !collection.Contains(item))) { + Remove(item); } } public bool IsProperSubsetOf(IEnumerable other) { - using (Lock.ReaderLock()) { - return HashSet.IsProperSubsetOf(other); - } + ICollection collection = other as ICollection ?? other.ToList(); + return (collection.Count != Count) && IsSubsetOf(collection); } public bool IsProperSupersetOf(IEnumerable other) { - using (Lock.ReaderLock()) { - return HashSet.IsProperSupersetOf(other); - } + ICollection collection = other as ICollection ?? other.ToList(); + return (collection.Count != Count) && IsSupersetOf(collection); } public bool IsSubsetOf(IEnumerable other) { - using (Lock.ReaderLock()) { - return HashSet.IsSubsetOf(other); - } + ICollection collection = other as ICollection ?? other.ToList(); + return this.AsParallel().All(collection.Contains); } - public bool IsSupersetOf(IEnumerable other) { - using (Lock.ReaderLock()) { - return HashSet.IsSupersetOf(other); - } - } - - public bool Overlaps(IEnumerable other) { - using (Lock.ReaderLock()) { - return HashSet.Overlaps(other); - } - } - - public bool Remove(T item) { - using (Lock.WriterLock()) { - return HashSet.Remove(item); - } - } + public bool IsSupersetOf(IEnumerable other) => other.AsParallel().All(Contains); + public bool Overlaps(IEnumerable other) => other.AsParallel().Any(Contains); + public bool Remove(T item) => BackingCollection.TryRemove(item, out _); public bool SetEquals(IEnumerable other) { - using (Lock.ReaderLock()) { - return HashSet.SetEquals(other); - } + ICollection collection = other as ICollection ?? other.ToList(); + return (collection.Count == Count) && collection.AsParallel().All(Contains); } public void SymmetricExceptWith(IEnumerable other) { - using (Lock.WriterLock()) { - HashSet.SymmetricExceptWith(other); + ICollection collection = other as ICollection ?? other.ToList(); + + HashSet removed = new HashSet(); + 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 other) { - using (Lock.WriterLock()) { - HashSet.UnionWith(other); + foreach (T otherElement in other) { + Add(otherElement); } } void ICollection.Add(T item) => Add(item); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - internal bool AddRange(IEnumerable 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 items) => items.Count(Add) > 0; + + // We use Count() and not Any() because we must ensure full loop pass + internal bool RemoveRange(IEnumerable items) => items.Count(Remove) > 0; + + internal bool ReplaceIfNeededWith(ICollection other) { + if (SetEquals(other)) { + return false; } + + ReplaceWith(other); + return true; } - internal void ClearAndTrim() { - using (Lock.WriterLock()) { - HashSet.Clear(); - HashSet.TrimExcess(); - } - } - - internal bool RemoveRange(IEnumerable 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 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 items) { - using (Lock.WriterLock()) { - HashSet.Clear(); - - foreach (T item in items) { - HashSet.Add(item); - } - - HashSet.TrimExcess(); + internal void ReplaceWith(IEnumerable other) { + BackingCollection.Clear(); + foreach (T item in other) { + BackingCollection[item] = true; } } } diff --git a/ArchiSteamFarm/GlobalDatabase.cs b/ArchiSteamFarm/GlobalDatabase.cs index ab43977f7..4219a5d53 100644 --- a/ArchiSteamFarm/GlobalDatabase.cs +++ b/ArchiSteamFarm/GlobalDatabase.cs @@ -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> AppIDsToPackageIDs = new ConcurrentDictionary>(); + [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 packageIDs) { + if ((bot == null) || (packageIDs == null) || (packageIDs.Count == 0)) { + ASF.ArchiLogger.LogNullError(nameof(bot) + " || " + nameof(packageIDs)); + return; + } + + await PackagesRefreshSemaphore.WaitAsync().ConfigureAwait(false); + + try { + HashSet missingPackageIDs = new HashSet(packageIDs.AsParallel().Where(packageID => AppIDsToPackageIDs.Values.All(packages => !packages.Contains(packageID)))); + if (missingPackageIDs.Count == 0) { + return; + } + + Dictionary> appIDsToPackageIDs = await bot.GetAppIDsToPackageIDs(missingPackageIDs); + if ((appIDsToPackageIDs == null) || (appIDsToPackageIDs.Count == 0)) { + return; + } + + foreach (KeyValuePair> appIDtoPackageID in appIDsToPackageIDs) { + if (!AppIDsToPackageIDs.TryGetValue(appIDtoPackageID.Key, out ConcurrentHashSet packages)) { + packages = new ConcurrentHashSet(); + 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() { diff --git a/ArchiSteamFarm/Trading.cs b/ArchiSteamFarm/Trading.cs index 49c307632..976099fc3 100644 --- a/ArchiSteamFarm/Trading.cs +++ b/ArchiSteamFarm/Trading.cs @@ -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 inventory, HashSet itemsToGive, HashSet 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 itemAmounts = new Dictionary(); + 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> itemAmountToGivePerGame = new Dictionary<(Steam.Item.EType Type, uint AppID), List>(); + Dictionary itemAmountsToGive = new Dictionary(itemAmounts); + foreach (Steam.Item item in itemsToGive) { + if (!itemAmountToGivePerGame.TryGetValue((item.Type, item.RealAppID), out List amountsToGive)) { + amountsToGive = new List(); + 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 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> itemAmountToReceivePerGame = new Dictionary<(Steam.Item.EType Type, uint AppID), List>(); + Dictionary itemAmountsToReceive = new Dictionary(itemAmounts); + foreach (Steam.Item item in itemsToReceive) { + if (!itemAmountToReceivePerGame.TryGetValue((item.Type, item.RealAppID), out List amountsToReceive)) { + amountsToReceive = new List(); + 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 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 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 inventory, HashSet itemsToGive, HashSet 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 itemAmounts = new Dictionary(); - 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> itemAmountToGivePerGame = new Dictionary<(Steam.Item.EType Type, uint AppID), List>(); - Dictionary itemAmountsToGive = new Dictionary(itemAmounts); - foreach (Steam.Item item in itemsToGive) { - if (!itemAmountToGivePerGame.TryGetValue((item.Type, item.RealAppID), out List amountsToGive)) { - amountsToGive = new List(); - 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 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> itemAmountToReceivePerGame = new Dictionary<(Steam.Item.EType Type, uint AppID), List>(); - Dictionary itemAmountsToReceive = new Dictionary(itemAmounts); - foreach (Steam.Item item in itemsToReceive) { - if (!itemAmountToReceivePerGame.TryGetValue((item.Type, item.RealAppID), out List amountsToReceive)) { - amountsToReceive = new List(); - 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 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; diff --git a/ArchiSteamFarm/config/example.json b/ArchiSteamFarm/config/example.json index 960f75877..86de9f978 100644 --- a/ArchiSteamFarm/config/example.json +++ b/ArchiSteamFarm/config/example.json @@ -10,6 +10,7 @@ "FarmOffline": false, "GamesPlayedWhileIdle": [], "HandleOfflineMessages": false, + "IdleRefundableGames": true, "IsBotAccount": false, "LootableTypes": [ 1,