mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2025-12-20 08:18:37 +00:00
Add an option to set farming order
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@ out/
|
|||||||
## files generated by popular Visual Studio add-ons.
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
|
||||||
# User-specific files
|
# User-specific files
|
||||||
|
.vs/
|
||||||
*.suo
|
*.suo
|
||||||
*.user
|
*.user
|
||||||
*.sln.docstates
|
*.sln.docstates
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ namespace ArchiSteamFarm {
|
|||||||
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
|
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
|
||||||
[SuppressMessage("ReSharper", "ConvertToConstant.Global")]
|
[SuppressMessage("ReSharper", "ConvertToConstant.Global")]
|
||||||
internal sealed class BotConfig {
|
internal sealed class BotConfig {
|
||||||
|
internal enum EFarmingOrder : byte {
|
||||||
|
Unordered,
|
||||||
|
MostCardDropRemainingFirst,
|
||||||
|
FewestCardDropRemainingFirst
|
||||||
|
}
|
||||||
|
|
||||||
[JsonProperty(Required = Required.DisallowNull)]
|
[JsonProperty(Required = Required.DisallowNull)]
|
||||||
internal readonly bool Enabled = false;
|
internal readonly bool Enabled = false;
|
||||||
|
|
||||||
@@ -113,6 +119,8 @@ namespace ArchiSteamFarm {
|
|||||||
[JsonProperty(Required = Required.DisallowNull)]
|
[JsonProperty(Required = Required.DisallowNull)]
|
||||||
internal readonly HashSet<uint> GamesPlayedWhileIdle = new HashSet<uint>();
|
internal readonly HashSet<uint> GamesPlayedWhileIdle = new HashSet<uint>();
|
||||||
|
|
||||||
|
[JsonProperty(Required = Required.DisallowNull)]
|
||||||
|
internal readonly EFarmingOrder FarmingOrder = EFarmingOrder.Unordered;
|
||||||
|
|
||||||
internal static BotConfig Load(string filePath) {
|
internal static BotConfig Load(string filePath) {
|
||||||
if (string.IsNullOrEmpty(filePath)) {
|
if (string.IsNullOrEmpty(filePath)) {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
|
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -35,10 +34,17 @@ using Newtonsoft.Json;
|
|||||||
|
|
||||||
namespace ArchiSteamFarm {
|
namespace ArchiSteamFarm {
|
||||||
internal sealed class CardsFarmer : IDisposable {
|
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
|
internal const byte MaxGamesPlayedConcurrently = 32; // This is limit introduced by Steam Network
|
||||||
|
|
||||||
[JsonProperty]
|
[JsonProperty]
|
||||||
internal readonly ConcurrentDictionary<uint, float> GamesToFarm = new ConcurrentDictionary<uint, float>();
|
internal readonly List<Game> GamesToFarm = new List<Game>();
|
||||||
|
|
||||||
|
|
||||||
[JsonProperty]
|
[JsonProperty]
|
||||||
internal readonly ConcurrentHashSet<uint> CurrentGamesFarming = new ConcurrentHashSet<uint>();
|
internal readonly ConcurrentHashSet<uint> CurrentGamesFarming = new ConcurrentHashSet<uint>();
|
||||||
@@ -143,7 +149,8 @@ namespace ArchiSteamFarm {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (FarmMultiple()) {
|
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 {
|
} else {
|
||||||
NowFarming = false;
|
NowFarming = false;
|
||||||
return;
|
return;
|
||||||
@@ -153,7 +160,7 @@ namespace ArchiSteamFarm {
|
|||||||
} else { // If we have unrestricted card drops, we use simple algorithm
|
} else { // If we have unrestricted card drops, we use simple algorithm
|
||||||
Logging.LogGenericInfo("Chosen farming algorithm: Simple", Bot.BotName);
|
Logging.LogGenericInfo("Chosen farming algorithm: Simple", Bot.BotName);
|
||||||
while (GamesToFarm.Count > 0) {
|
while (GamesToFarm.Count > 0) {
|
||||||
uint appID = GamesToFarm.Keys.FirstOrDefault();
|
uint appID = GamesToFarm.First().AppID;
|
||||||
if (await FarmSolo(appID).ConfigureAwait(false)) {
|
if (await FarmSolo(appID).ConfigureAwait(false)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -218,7 +225,7 @@ namespace ArchiSteamFarm {
|
|||||||
return;
|
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
|
// 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
|
// That's because we would check for new games after our current round anyway
|
||||||
await StopFarming().ConfigureAwait(false);
|
await StopFarming().ConfigureAwait(false);
|
||||||
@@ -226,15 +233,15 @@ namespace ArchiSteamFarm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HashSet<uint> GetGamesToFarmSolo(ConcurrentDictionary<uint, float> gamesToFarm) {
|
private static HashSet<uint> GetGamesToFarmSolo(IEnumerable<Game> gamesToFarm) {
|
||||||
if (gamesToFarm == null) {
|
if (gamesToFarm == null) {
|
||||||
Logging.LogNullError(nameof(gamesToFarm));
|
Logging.LogNullError(nameof(gamesToFarm));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
HashSet<uint> result = new HashSet<uint>();
|
HashSet<uint> result = new HashSet<uint>();
|
||||||
foreach (KeyValuePair<uint, float> keyValue in gamesToFarm.Where(keyValue => keyValue.Value >= 2)) {
|
foreach (Game game in gamesToFarm.Where(g => g.HoursPlayed >= 2)) {
|
||||||
result.Add(keyValue.Key);
|
result.Add(game.AppID);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -297,12 +304,34 @@ namespace ArchiSteamFarm {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Game> games = new List<Game>(htmlNodes.Count);
|
||||||
foreach (HtmlNode htmlNode in htmlNodes) {
|
foreach (HtmlNode htmlNode in htmlNodes) {
|
||||||
HtmlNode farmingNode = htmlNode.SelectSingleNode(".//a[@class='btn_green_white_innerfade btn_small_thin']");
|
HtmlNode farmingNode = htmlNode.SelectSingleNode(".//a[@class='btn_green_white_innerfade btn_small_thin']");
|
||||||
if (farmingNode == null) {
|
if (farmingNode == null) {
|
||||||
continue; // This game is not needed for farming
|
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);
|
string steamLink = farmingNode.GetAttributeValue("href", null);
|
||||||
if (string.IsNullOrEmpty(steamLink)) {
|
if (string.IsNullOrEmpty(steamLink)) {
|
||||||
Logging.LogNullError(nameof(steamLink), Bot.BotName);
|
Logging.LogNullError(nameof(steamLink), Bot.BotName);
|
||||||
@@ -347,16 +376,36 @@ namespace ArchiSteamFarm {
|
|||||||
|
|
||||||
float hours = 0;
|
float hours = 0;
|
||||||
|
|
||||||
Match match = Regex.Match(hoursString, @"[0-9\.,]+");
|
Match hoursMatch = Regex.Match(hoursString, @"[0-9\.,]+");
|
||||||
if (match.Success) {
|
if (hoursMatch.Success) {
|
||||||
if (!float.TryParse(match.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out hours)) {
|
if (!float.TryParse(hoursMatch.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out hours)) {
|
||||||
Logging.LogNullError(nameof(hours), Bot.BotName);
|
Logging.LogNullError(nameof(hours), Bot.BotName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GamesToFarm[appID] = hours;
|
games.Add(new Game {
|
||||||
|
AppID = appID,
|
||||||
|
HoursPlayed = hours,
|
||||||
|
CardsRemaining = cardsRemaining
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IEnumerable<Game> 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) {
|
private async Task CheckPage(byte page) {
|
||||||
@@ -424,10 +473,10 @@ namespace ArchiSteamFarm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float maxHour = 0;
|
float maxHour = 0;
|
||||||
foreach (KeyValuePair<uint, float> game in GamesToFarm) {
|
foreach (Game game in GamesToFarm) {
|
||||||
CurrentGamesFarming.Add(game.Key);
|
CurrentGamesFarming.Add(game.AppID);
|
||||||
if (game.Value > maxHour) {
|
if (game.HoursPlayed > maxHour) {
|
||||||
maxHour = game.Value;
|
maxHour = game.HoursPlayed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CurrentGamesFarming.Count >= MaxGamesPlayedConcurrently) {
|
if (CurrentGamesFarming.Count >= MaxGamesPlayedConcurrently) {
|
||||||
@@ -464,12 +513,13 @@ namespace ArchiSteamFarm {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
float hours;
|
Game game = GamesToFarm.FirstOrDefault(g => g.AppID == appID);
|
||||||
if (!GamesToFarm.TryRemove(appID, out hours)) {
|
if (game == null) {
|
||||||
return false;
|
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);
|
Logging.LogGenericInfo("Done farming: " + appID + " after " + timeSpan.ToString(@"hh\:mm") + " hours of playtime!", Bot.BotName);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -496,7 +546,8 @@ namespace ArchiSteamFarm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't forget to update our GamesToFarm hours
|
// 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) {
|
if (!success) {
|
||||||
break;
|
break;
|
||||||
@@ -530,7 +581,8 @@ namespace ArchiSteamFarm {
|
|||||||
// Don't forget to update our GamesToFarm hours
|
// Don't forget to update our GamesToFarm hours
|
||||||
float timePlayed = (float) DateTime.Now.Subtract(startFarmingPeriod).TotalHours;
|
float timePlayed = (float) DateTime.Now.Subtract(startFarmingPeriod).TotalHours;
|
||||||
foreach (uint appID in appIDs) {
|
foreach (uint appID in appIDs) {
|
||||||
GamesToFarm[appID] += timePlayed;
|
Game game = GamesToFarm.First(g => g.AppID == appID);
|
||||||
|
game.HoursPlayed += timePlayed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ namespace ConfigGenerator {
|
|||||||
ProtectedDataForCurrentUser
|
ProtectedDataForCurrentUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal enum EFarmingOrder : byte {
|
||||||
|
Unordered,
|
||||||
|
MostCardDropRemainingFirst,
|
||||||
|
FewestCardDropRemainingFirst
|
||||||
|
}
|
||||||
|
|
||||||
[JsonProperty(Required = Required.DisallowNull)]
|
[JsonProperty(Required = Required.DisallowNull)]
|
||||||
public bool Enabled { get; set; } = false;
|
public bool Enabled { get; set; } = false;
|
||||||
|
|
||||||
@@ -120,6 +126,9 @@ namespace ConfigGenerator {
|
|||||||
[JsonProperty(Required = Required.DisallowNull)]
|
[JsonProperty(Required = Required.DisallowNull)]
|
||||||
public List<uint> GamesPlayedWhileIdle { get; set; } = new List<uint>();
|
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) {
|
internal static BotConfig Load(string filePath) {
|
||||||
if (string.IsNullOrEmpty(filePath)) {
|
if (string.IsNullOrEmpty(filePath)) {
|
||||||
Logging.LogNullError(nameof(filePath));
|
Logging.LogNullError(nameof(filePath));
|
||||||
|
|||||||
Reference in New Issue
Block a user