Merge pull request #312 from stackia/master

Add an option to set farming order
This commit is contained in:
Łukasz Domeradzki
2016-07-30 00:09:44 +02:00
committed by GitHub
4 changed files with 127 additions and 25 deletions

1
.gitignore vendored
View File

@@ -23,6 +23,7 @@ out/
## files generated by popular Visual Studio add-ons.
# User-specific files
.vs/
*.suo
*.user
*.sln.docstates

View File

@@ -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<uint> GamesPlayedWhileIdle = new HashSet<uint>();
[JsonProperty(Required = Required.DisallowNull)]
internal readonly EFarmingOrder FarmingOrder = EFarmingOrder.Unordered;
internal static BotConfig Load(string filePath) {
if (string.IsNullOrEmpty(filePath)) {

View File

@@ -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,47 @@ 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) {
if (appID <= 0)
throw new ArgumentOutOfRangeException(nameof(appID));
if (hoursPlayed < 0)
throw new ArgumentOutOfRangeException(nameof(hoursPlayed));
AppID = appID;
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
[JsonProperty]
internal readonly ConcurrentDictionary<uint, float> GamesToFarm = new ConcurrentDictionary<uint, float>();
internal readonly ConcurrentHashSet<Game> GamesToFarm = new ConcurrentHashSet<Game>();
[JsonProperty]
internal readonly ConcurrentHashSet<uint> CurrentGamesFarming = new ConcurrentHashSet<uint>();
@@ -143,7 +179,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 +190,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 +255,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 +263,15 @@ namespace ArchiSteamFarm {
}
}
private static HashSet<uint> GetGamesToFarmSolo(ConcurrentDictionary<uint, float> gamesToFarm) {
private static HashSet<uint> GetGamesToFarmSolo(IEnumerable<Game> gamesToFarm) {
if (gamesToFarm == null) {
Logging.LogNullError(nameof(gamesToFarm));
return null;
}
HashSet<uint> result = new HashSet<uint>();
foreach (KeyValuePair<uint, float> 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;
@@ -271,6 +308,7 @@ namespace ArchiSteamFarm {
CheckPage(htmlDocument);
if (maxPages == 1) {
SortGamesToFarm();
return GamesToFarm.Count > 0;
}
@@ -283,9 +321,28 @@ namespace ArchiSteamFarm {
}
await Task.WhenAll(tasks).ConfigureAwait(false);
SortGamesToFarm();
return GamesToFarm.Count > 0;
}
private void SortGamesToFarm() {
List<Game> 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();
GamesToFarm.AddRange(gamesToFarm);
}
private void CheckPage(HtmlDocument htmlDocument) {
if (htmlDocument == null) {
Logging.LogNullError(nameof(htmlDocument), Bot.BotName);
@@ -296,13 +353,34 @@ namespace ArchiSteamFarm {
if (htmlNodes == null) { // For example a page full of non-games badges
return;
}
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,15 +425,15 @@ 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;
GamesToFarm.Add(new Game(appID, hours, cardsRemaining));
}
}
@@ -414,6 +492,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;
}
@@ -424,10 +505,10 @@ namespace ArchiSteamFarm {
}
float maxHour = 0;
foreach (KeyValuePair<uint, float> 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 +544,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 +572,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 +605,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 +613,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) {

View File

@@ -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<uint> GamesPlayedWhileIdle { get; set; } = new List<uint>();
[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));