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));