From 80abd3ed69e341aebf621e5c9ba55fc625ee94c7 Mon Sep 17 00:00:00 2001 From: stackia Date: Sat, 30 Jul 2016 05:06:40 +0800 Subject: [PATCH 1/6] Add an option to set farming order --- .gitignore | 1 + ArchiSteamFarm/BotConfig.cs | 8 +++ ArchiSteamFarm/CardsFarmer.cs | 100 ++++++++++++++++++++++++++-------- ConfigGenerator/BotConfig.cs | 9 +++ 4 files changed, 94 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index c2507219c..5db8c130f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ out/ ## files generated by popular Visual Studio add-ons. # User-specific files +.vs/ *.suo *.user *.sln.docstates diff --git a/ArchiSteamFarm/BotConfig.cs b/ArchiSteamFarm/BotConfig.cs index eee136b67..946530497 100644 --- a/ArchiSteamFarm/BotConfig.cs +++ b/ArchiSteamFarm/BotConfig.cs @@ -34,6 +34,12 @@ namespace ArchiSteamFarm { [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] [SuppressMessage("ReSharper", "ConvertToConstant.Global")] internal sealed class BotConfig { + internal enum EFarmingOrder : byte { + Unordered, + MostCardDropRemainingFirst, + FewestCardDropRemainingFirst + } + [JsonProperty(Required = Required.DisallowNull)] internal readonly bool Enabled = false; @@ -113,6 +119,8 @@ namespace ArchiSteamFarm { [JsonProperty(Required = Required.DisallowNull)] internal readonly HashSet GamesPlayedWhileIdle = new HashSet(); + [JsonProperty(Required = Required.DisallowNull)] + internal readonly EFarmingOrder FarmingOrder = EFarmingOrder.Unordered; internal static BotConfig Load(string filePath) { if (string.IsNullOrEmpty(filePath)) { diff --git a/ArchiSteamFarm/CardsFarmer.cs b/ArchiSteamFarm/CardsFarmer.cs index fae23db06..7a2f9ee29 100755 --- a/ArchiSteamFarm/CardsFarmer.cs +++ b/ArchiSteamFarm/CardsFarmer.cs @@ -24,7 +24,6 @@ using HtmlAgilityPack; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -35,10 +34,17 @@ using Newtonsoft.Json; namespace ArchiSteamFarm { internal sealed class CardsFarmer : IDisposable { + internal class Game { + public uint AppID; + public float HoursPlayed; + public byte CardsRemaining; + } + internal const byte MaxGamesPlayedConcurrently = 32; // This is limit introduced by Steam Network [JsonProperty] - internal readonly ConcurrentDictionary GamesToFarm = new ConcurrentDictionary(); + internal readonly List GamesToFarm = new List(); + [JsonProperty] internal readonly ConcurrentHashSet CurrentGamesFarming = new ConcurrentHashSet(); @@ -143,7 +149,8 @@ namespace ArchiSteamFarm { } } else { if (FarmMultiple()) { - Logging.LogGenericInfo("Done farming: " + string.Join(", ", GamesToFarm.Keys), Bot.BotName); + Logging.LogGenericInfo("Done farming: " + string.Join(", ", GamesToFarm.Select(g => g.AppID)), + Bot.BotName); } else { NowFarming = false; return; @@ -153,7 +160,7 @@ namespace ArchiSteamFarm { } else { // If we have unrestricted card drops, we use simple algorithm Logging.LogGenericInfo("Chosen farming algorithm: Simple", Bot.BotName); while (GamesToFarm.Count > 0) { - uint appID = GamesToFarm.Keys.FirstOrDefault(); + uint appID = GamesToFarm.First().AppID; if (await FarmSolo(appID).ConfigureAwait(false)) { continue; } @@ -218,7 +225,7 @@ namespace ArchiSteamFarm { return; } - if (Bot.BotConfig.CardDropsRestricted && (GamesToFarm.Count > 0) && (GamesToFarm.Values.Min() < 2)) { + if (Bot.BotConfig.CardDropsRestricted && (GamesToFarm.Count > 0) && (GamesToFarm.Min(g => g.HoursPlayed) < 2)) { // If we have Complex algorithm and some games to boost, it's also worth to make a check // That's because we would check for new games after our current round anyway await StopFarming().ConfigureAwait(false); @@ -226,15 +233,15 @@ namespace ArchiSteamFarm { } } - private static HashSet GetGamesToFarmSolo(ConcurrentDictionary gamesToFarm) { + private static HashSet GetGamesToFarmSolo(IEnumerable gamesToFarm) { if (gamesToFarm == null) { Logging.LogNullError(nameof(gamesToFarm)); return null; } HashSet result = new HashSet(); - foreach (KeyValuePair keyValue in gamesToFarm.Where(keyValue => keyValue.Value >= 2)) { - result.Add(keyValue.Key); + foreach (Game game in gamesToFarm.Where(g => g.HoursPlayed >= 2)) { + result.Add(game.AppID); } return result; @@ -297,12 +304,34 @@ namespace ArchiSteamFarm { return; } + List games = new List(htmlNodes.Count); foreach (HtmlNode htmlNode in htmlNodes) { HtmlNode farmingNode = htmlNode.SelectSingleNode(".//a[@class='btn_green_white_innerfade btn_small_thin']"); if (farmingNode == null) { continue; // This game is not needed for farming } + HtmlNode progressNode = htmlNode.SelectSingleNode(".//span[@class='progress_info_bold']"); + if (progressNode == null) { + continue; // e.g. Holiday Sale 2015 + } + + string progress = progressNode.InnerText; + if (string.IsNullOrEmpty(progress)) { + Logging.LogNullError(nameof(progress), Bot.BotName); + return; + } + + byte cardsRemaining = 0; + + Match progressMatch = Regex.Match(progress, @"\d+"); + if (progressMatch.Success) { + if (!byte.TryParse(progressMatch.Value, out cardsRemaining)) { + Logging.LogNullError(nameof(cardsRemaining), Bot.BotName); + return; + } + } + string steamLink = farmingNode.GetAttributeValue("href", null); if (string.IsNullOrEmpty(steamLink)) { Logging.LogNullError(nameof(steamLink), Bot.BotName); @@ -347,16 +376,36 @@ namespace ArchiSteamFarm { float hours = 0; - Match match = Regex.Match(hoursString, @"[0-9\.,]+"); - if (match.Success) { - if (!float.TryParse(match.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out hours)) { + Match hoursMatch = Regex.Match(hoursString, @"[0-9\.,]+"); + if (hoursMatch.Success) { + if (!float.TryParse(hoursMatch.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out hours)) { Logging.LogNullError(nameof(hours), Bot.BotName); return; } } - GamesToFarm[appID] = hours; + games.Add(new Game { + AppID = appID, + HoursPlayed = hours, + CardsRemaining = cardsRemaining + }); } + + IEnumerable gamesToFarm; + switch (Bot.BotConfig.FarmingOrder) { + case BotConfig.EFarmingOrder.MostCardDropRemainingFirst: + gamesToFarm = games.OrderByDescending(g => g.CardsRemaining); + break; + + case BotConfig.EFarmingOrder.FewestCardDropRemainingFirst: + gamesToFarm = games.OrderBy(g => g.CardsRemaining); + break; + + default: + gamesToFarm = games; + break; + } + GamesToFarm.AddRange(gamesToFarm); } private async Task CheckPage(byte page) { @@ -424,10 +473,10 @@ namespace ArchiSteamFarm { } float maxHour = 0; - foreach (KeyValuePair game in GamesToFarm) { - CurrentGamesFarming.Add(game.Key); - if (game.Value > maxHour) { - maxHour = game.Value; + foreach (Game game in GamesToFarm) { + CurrentGamesFarming.Add(game.AppID); + if (game.HoursPlayed > maxHour) { + maxHour = game.HoursPlayed; } if (CurrentGamesFarming.Count >= MaxGamesPlayedConcurrently) { @@ -463,13 +512,14 @@ namespace ArchiSteamFarm { if (!result) { return false; } - - float hours; - if (!GamesToFarm.TryRemove(appID, out hours)) { + + Game game = GamesToFarm.FirstOrDefault(g => g.AppID == appID); + if (game == null) { return false; } + GamesToFarm.Remove(game); - TimeSpan timeSpan = TimeSpan.FromHours(hours); + TimeSpan timeSpan = TimeSpan.FromHours(game.HoursPlayed); Logging.LogGenericInfo("Done farming: " + appID + " after " + timeSpan.ToString(@"hh\:mm") + " hours of playtime!", Bot.BotName); return true; } @@ -490,13 +540,14 @@ namespace ArchiSteamFarm { Logging.LogGenericInfo("Still farming: " + appID, Bot.BotName); DateTime startFarmingPeriod = DateTime.Now; - if (FarmResetEvent.Wait(60 * 1000 * Program.GlobalConfig.FarmingDelay)) { + if (FarmResetEvent.Wait(60*1000*Program.GlobalConfig.FarmingDelay)) { FarmResetEvent.Reset(); success = KeepFarming; } // Don't forget to update our GamesToFarm hours - GamesToFarm[appID] += (float) DateTime.Now.Subtract(startFarmingPeriod).TotalHours; + Game game = GamesToFarm.First(g => g.AppID == appID); + game.HoursPlayed += (float) DateTime.Now.Subtract(startFarmingPeriod).TotalHours; if (!success) { break; @@ -522,7 +573,7 @@ namespace ArchiSteamFarm { Logging.LogGenericInfo("Still farming: " + string.Join(", ", appIDs), Bot.BotName); DateTime startFarmingPeriod = DateTime.Now; - if (FarmResetEvent.Wait(60 * 1000 * Program.GlobalConfig.FarmingDelay)) { + if (FarmResetEvent.Wait(60*1000*Program.GlobalConfig.FarmingDelay)) { FarmResetEvent.Reset(); success = KeepFarming; } @@ -530,7 +581,8 @@ namespace ArchiSteamFarm { // Don't forget to update our GamesToFarm hours float timePlayed = (float) DateTime.Now.Subtract(startFarmingPeriod).TotalHours; foreach (uint appID in appIDs) { - GamesToFarm[appID] += timePlayed; + Game game = GamesToFarm.First(g => g.AppID == appID); + game.HoursPlayed += timePlayed; } if (!success) { diff --git a/ConfigGenerator/BotConfig.cs b/ConfigGenerator/BotConfig.cs index e573b215a..ab610a577 100644 --- a/ConfigGenerator/BotConfig.cs +++ b/ConfigGenerator/BotConfig.cs @@ -41,6 +41,12 @@ namespace ConfigGenerator { ProtectedDataForCurrentUser } + internal enum EFarmingOrder : byte { + Unordered, + MostCardDropRemainingFirst, + FewestCardDropRemainingFirst + } + [JsonProperty(Required = Required.DisallowNull)] public bool Enabled { get; set; } = false; @@ -120,6 +126,9 @@ namespace ConfigGenerator { [JsonProperty(Required = Required.DisallowNull)] public List GamesPlayedWhileIdle { get; set; } = new List(); + [JsonProperty(Required = Required.DisallowNull)] + public EFarmingOrder FarmingOrder { get; set; } = EFarmingOrder.Unordered; + internal static BotConfig Load(string filePath) { if (string.IsNullOrEmpty(filePath)) { Logging.LogNullError(nameof(filePath)); From 2c54f6b05158bd03cc1310dfcb5683c21ced4cda Mon Sep 17 00:00:00 2001 From: stackia Date: Sat, 30 Jul 2016 05:34:57 +0800 Subject: [PATCH 2/6] Use ConcurrentHashSet for GamesToFarm and fix game didn't get sorted after first badge page --- ArchiSteamFarm/CardsFarmer.cs | 45 +++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/ArchiSteamFarm/CardsFarmer.cs b/ArchiSteamFarm/CardsFarmer.cs index 7a2f9ee29..4ad636b2a 100755 --- a/ArchiSteamFarm/CardsFarmer.cs +++ b/ArchiSteamFarm/CardsFarmer.cs @@ -43,7 +43,7 @@ namespace ArchiSteamFarm { internal const byte MaxGamesPlayedConcurrently = 32; // This is limit introduced by Steam Network [JsonProperty] - internal readonly List GamesToFarm = new List(); + internal readonly ConcurrentHashSet GamesToFarm = new ConcurrentHashSet(); [JsonProperty] @@ -278,6 +278,7 @@ namespace ArchiSteamFarm { CheckPage(htmlDocument); if (maxPages == 1) { + SortGamesToFarm(); return GamesToFarm.Count > 0; } @@ -290,9 +291,30 @@ namespace ArchiSteamFarm { } await Task.WhenAll(tasks).ConfigureAwait(false); + SortGamesToFarm(); return GamesToFarm.Count > 0; } + private void SortGamesToFarm() { + List gamesToFarm; + switch (Bot.BotConfig.FarmingOrder) { + case BotConfig.EFarmingOrder.MostCardDropRemainingFirst: + gamesToFarm = GamesToFarm.OrderByDescending(g => g.CardsRemaining).ToList(); + break; + + case BotConfig.EFarmingOrder.FewestCardDropRemainingFirst: + gamesToFarm = GamesToFarm.OrderBy(g => g.CardsRemaining).ToList(); + break; + + default: + return; + } + GamesToFarm.Clear(); + foreach (var game in gamesToFarm) { + GamesToFarm.Add(game); + } + } + private void CheckPage(HtmlDocument htmlDocument) { if (htmlDocument == null) { Logging.LogNullError(nameof(htmlDocument), Bot.BotName); @@ -303,8 +325,7 @@ namespace ArchiSteamFarm { if (htmlNodes == null) { // For example a page full of non-games badges return; } - - List games = new List(htmlNodes.Count); + foreach (HtmlNode htmlNode in htmlNodes) { HtmlNode farmingNode = htmlNode.SelectSingleNode(".//a[@class='btn_green_white_innerfade btn_small_thin']"); if (farmingNode == null) { @@ -384,28 +405,12 @@ namespace ArchiSteamFarm { } } - games.Add(new Game { + GamesToFarm.Add(new Game { AppID = appID, HoursPlayed = hours, CardsRemaining = cardsRemaining }); } - - IEnumerable gamesToFarm; - switch (Bot.BotConfig.FarmingOrder) { - case BotConfig.EFarmingOrder.MostCardDropRemainingFirst: - gamesToFarm = games.OrderByDescending(g => g.CardsRemaining); - break; - - case BotConfig.EFarmingOrder.FewestCardDropRemainingFirst: - gamesToFarm = games.OrderBy(g => g.CardsRemaining); - break; - - default: - gamesToFarm = games; - break; - } - GamesToFarm.AddRange(gamesToFarm); } private async Task CheckPage(byte page) { From 561f8f61df2efba2da1903a194e4723e3dad27b6 Mon Sep 17 00:00:00 2001 From: stackia Date: Sat, 30 Jul 2016 05:46:21 +0800 Subject: [PATCH 3/6] Check argument range in Game constructor --- ArchiSteamFarm/CardsFarmer.cs | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/ArchiSteamFarm/CardsFarmer.cs b/ArchiSteamFarm/CardsFarmer.cs index 4ad636b2a..dc762479a 100755 --- a/ArchiSteamFarm/CardsFarmer.cs +++ b/ArchiSteamFarm/CardsFarmer.cs @@ -34,10 +34,21 @@ using Newtonsoft.Json; namespace ArchiSteamFarm { internal sealed class CardsFarmer : IDisposable { - internal class Game { - public uint AppID; - public float HoursPlayed; - public byte CardsRemaining; + internal sealed class Game { + internal readonly uint AppID; + internal float HoursPlayed; + internal byte CardsRemaining; + + internal Game(uint appID, float hoursPlayed, byte cardsRemaining) { + if (appID <= 0) + throw new ArgumentOutOfRangeException(nameof(appID)); + if (hoursPlayed < 0) + throw new ArgumentOutOfRangeException(nameof(hoursPlayed)); + + AppID = appID; + HoursPlayed = hoursPlayed; + CardsRemaining = cardsRemaining; + } } internal const byte MaxGamesPlayedConcurrently = 32; // This is limit introduced by Steam Network @@ -405,11 +416,7 @@ namespace ArchiSteamFarm { } } - GamesToFarm.Add(new Game { - AppID = appID, - HoursPlayed = hours, - CardsRemaining = cardsRemaining - }); + GamesToFarm.Add(new Game(appID, hours, cardsRemaining)); } } @@ -468,6 +475,9 @@ namespace ArchiSteamFarm { } } + Game game = GamesToFarm.First(g => g.AppID == appID); + game.CardsRemaining = cardsRemaining; + Logging.LogGenericInfo("Status for " + appID + ": " + cardsRemaining + " cards remaining", Bot.BotName); return cardsRemaining > 0; } From 4bc7fded2d5dcfbe4f1cf2afe69cbabfd82cca8d Mon Sep 17 00:00:00 2001 From: stackia Date: Sat, 30 Jul 2016 05:49:15 +0800 Subject: [PATCH 4/6] Use new ConcurrentHashSet.AddRange --- ArchiSteamFarm/CardsFarmer.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ArchiSteamFarm/CardsFarmer.cs b/ArchiSteamFarm/CardsFarmer.cs index dc762479a..d52eaf12d 100755 --- a/ArchiSteamFarm/CardsFarmer.cs +++ b/ArchiSteamFarm/CardsFarmer.cs @@ -321,9 +321,7 @@ namespace ArchiSteamFarm { return; } GamesToFarm.Clear(); - foreach (var game in gamesToFarm) { - GamesToFarm.Add(game); - } + GamesToFarm.AddRange(gamesToFarm); } private void CheckPage(HtmlDocument htmlDocument) { From 7baa54377a63cc312e30ad111325f26892dd8422 Mon Sep 17 00:00:00 2001 From: stackia Date: Sat, 30 Jul 2016 05:50:51 +0800 Subject: [PATCH 5/6] Add [JsonProperty] on Game's properties --- ArchiSteamFarm/CardsFarmer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ArchiSteamFarm/CardsFarmer.cs b/ArchiSteamFarm/CardsFarmer.cs index d52eaf12d..37f1dfb90 100755 --- a/ArchiSteamFarm/CardsFarmer.cs +++ b/ArchiSteamFarm/CardsFarmer.cs @@ -35,8 +35,13 @@ using Newtonsoft.Json; namespace ArchiSteamFarm { internal sealed class CardsFarmer : IDisposable { internal sealed class Game { + [JsonProperty] internal readonly uint AppID; + + [JsonProperty] internal float HoursPlayed; + + [JsonProperty] internal byte CardsRemaining; internal Game(uint appID, float hoursPlayed, byte cardsRemaining) { From 8cc3fec43211e3a90c3e8ccab3ec0f0806529a57 Mon Sep 17 00:00:00 2001 From: stackia Date: Sat, 30 Jul 2016 05:56:31 +0800 Subject: [PATCH 6/6] Implement Equals method on Game so it properly works with HashSet --- ArchiSteamFarm/CardsFarmer.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ArchiSteamFarm/CardsFarmer.cs b/ArchiSteamFarm/CardsFarmer.cs index 37f1dfb90..1608a2d61 100755 --- a/ArchiSteamFarm/CardsFarmer.cs +++ b/ArchiSteamFarm/CardsFarmer.cs @@ -54,6 +54,20 @@ namespace ArchiSteamFarm { HoursPlayed = hoursPlayed; CardsRemaining = cardsRemaining; } + + private bool Equals(Game other) { + return AppID == other.AppID; + } + + public override bool Equals(object obj) { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is Game && Equals((Game) obj); + } + + public override int GetHashCode() { + return (int) AppID; + } } internal const byte MaxGamesPlayedConcurrently = 32; // This is limit introduced by Steam Network