mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2026-01-01 06:00:46 +00:00
898 lines
28 KiB
C#
Executable File
898 lines
28 KiB
C#
Executable File
/*
|
|
_ _ _ ____ _ _____
|
|
/ \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
|
/ _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
|
/ ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
|
/_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
|
|
|
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.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using ArchiSteamFarm.Localization;
|
|
using HtmlAgilityPack;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace ArchiSteamFarm {
|
|
internal sealed class CardsFarmer : IDisposable {
|
|
private const byte HoursToBump = 2; // How many hours are required for restricted accounts
|
|
private const byte HoursToIgnore = 24; // How many hours we ignore unreleased appIDs and don't bother checking them again
|
|
|
|
private static readonly ConcurrentDictionary<uint, DateTime> IgnoredAppIDs = new ConcurrentDictionary<uint, DateTime>(); // Reserved for unreleased games
|
|
private static readonly HashSet<uint> UntrustedAppIDs = new HashSet<uint> { 440, 570, 730 };
|
|
|
|
[JsonProperty]
|
|
internal readonly ConcurrentHashSet<Game> CurrentGamesFarming = new ConcurrentHashSet<Game>();
|
|
|
|
[JsonProperty]
|
|
internal readonly ConcurrentHashSet<Game> GamesToFarm = new ConcurrentHashSet<Game>();
|
|
|
|
[JsonProperty]
|
|
internal TimeSpan TimeRemaining => new TimeSpan(
|
|
Bot.BotConfig.CardDropsRestricted ? (int) Math.Ceiling(GamesToFarm.Count / (float) ArchiHandler.MaxGamesPlayedConcurrently) * HoursToBump : 0,
|
|
30 * GamesToFarm.Sum(game => game.CardsRemaining),
|
|
0
|
|
);
|
|
|
|
private readonly Bot Bot;
|
|
private readonly SemaphoreSlim EventSemaphore = new SemaphoreSlim(1);
|
|
private readonly SemaphoreSlim FarmingSemaphore = new SemaphoreSlim(1);
|
|
private readonly ManualResetEventSlim FarmResetEvent = new ManualResetEventSlim(false);
|
|
private readonly Timer IdleFarmingTimer;
|
|
|
|
[JsonProperty]
|
|
internal bool Paused { get; private set; }
|
|
|
|
private bool KeepFarming;
|
|
private bool NowFarming;
|
|
private bool ParsingScheduled;
|
|
private bool StickyPause;
|
|
|
|
internal CardsFarmer(Bot bot) {
|
|
Bot = bot ?? throw new ArgumentNullException(nameof(bot));
|
|
|
|
if (Program.GlobalConfig.IdleFarmingPeriod > 0) {
|
|
IdleFarmingTimer = new Timer(
|
|
async e => await CheckGamesForFarming().ConfigureAwait(false),
|
|
null,
|
|
TimeSpan.FromHours(Program.GlobalConfig.IdleFarmingPeriod) + TimeSpan.FromSeconds(Program.LoadBalancingDelay * Bot.Bots.Count), // Delay
|
|
TimeSpan.FromHours(Program.GlobalConfig.IdleFarmingPeriod) // Period
|
|
);
|
|
}
|
|
}
|
|
|
|
public void Dispose() {
|
|
// Those are objects that are always being created if constructor doesn't throw exception
|
|
EventSemaphore.Dispose();
|
|
FarmingSemaphore.Dispose();
|
|
FarmResetEvent.Dispose();
|
|
|
|
// Those are objects that might be null and the check should be in-place
|
|
IdleFarmingTimer?.Dispose();
|
|
}
|
|
|
|
internal void OnDisconnected() => StopFarming().Forget();
|
|
|
|
internal async Task OnNewGameAdded() {
|
|
// We aim to have a maximum of 2 tasks, one already parsing, and one waiting in the queue
|
|
// This way we can call this function as many times as needed e.g. because of Steam events
|
|
lock (EventSemaphore) {
|
|
if (ParsingScheduled) {
|
|
return;
|
|
}
|
|
|
|
ParsingScheduled = true;
|
|
}
|
|
|
|
await EventSemaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
try {
|
|
lock (EventSemaphore) {
|
|
ParsingScheduled = false;
|
|
}
|
|
|
|
// If we're not farming yet, obviously it's worth it to make a check
|
|
if (!NowFarming) {
|
|
await StartFarming().ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
// If we have Complex algorithm and some games to boost, it's also worth to make a re-check, but only in this case
|
|
// That's because we would check for new games after our current round anyway, and having extra games in the queue right away doesn't change anything
|
|
// Therefore, there is no need for extra restart of CardsFarmer if we have no games under HoursToBump hours in current round
|
|
if (Bot.BotConfig.CardDropsRestricted && (GamesToFarm.Count > 0) && (GamesToFarm.Min(game => game.HoursPlayed) < HoursToBump)) {
|
|
await StopFarming().ConfigureAwait(false);
|
|
await StartFarming().ConfigureAwait(false);
|
|
}
|
|
} finally {
|
|
EventSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
internal async Task OnNewItemsNotification() {
|
|
if (NowFarming) {
|
|
FarmResetEvent.Set();
|
|
return;
|
|
}
|
|
|
|
// If we're not farming, and we got new items, it's likely to be a booster pack or likewise
|
|
// In this case, perform a loot if user wants to do so
|
|
await Bot.LootIfNeeded().ConfigureAwait(false);
|
|
}
|
|
|
|
internal async Task Pause(bool sticky) {
|
|
if (sticky) {
|
|
StickyPause = true;
|
|
}
|
|
|
|
Paused = true;
|
|
if (NowFarming) {
|
|
await StopFarming().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
internal async Task Resume(bool userAction) {
|
|
if (StickyPause) {
|
|
if (!userAction) {
|
|
Bot.ArchiLogger.LogGenericInfo(Strings.IgnoredStickyPauseEnabled);
|
|
return;
|
|
}
|
|
|
|
StickyPause = false;
|
|
}
|
|
|
|
Paused = false;
|
|
if (!NowFarming) {
|
|
await StartFarming().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
internal void SetInitialState(bool paused) => StickyPause = Paused = paused;
|
|
|
|
internal async Task StartFarming() {
|
|
if (NowFarming || Paused || !Bot.IsPlayingPossible) {
|
|
return;
|
|
}
|
|
|
|
if (!Bot.CanReceiveSteamCards) {
|
|
await Bot.OnFarmingFinished(false).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
await FarmingSemaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
try {
|
|
if (NowFarming || Paused || !Bot.IsPlayingPossible) {
|
|
return;
|
|
}
|
|
|
|
bool? isAnythingToFarm = await IsAnythingToFarm().ConfigureAwait(false);
|
|
if (isAnythingToFarm == null) {
|
|
return;
|
|
}
|
|
|
|
if (!isAnythingToFarm.Value) {
|
|
Bot.ArchiLogger.LogGenericInfo(Strings.NothingToIdle);
|
|
await Bot.OnFarmingFinished(false).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
if (GamesToFarm.Count == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(GamesToFarm));
|
|
return;
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.GamesToIdle, GamesToFarm.Count, GamesToFarm.Sum(game => game.CardsRemaining), TimeRemaining.ToHumanReadable()));
|
|
|
|
// This is the last moment for final check if we can farm
|
|
if (!Bot.IsPlayingPossible) {
|
|
Bot.ArchiLogger.LogGenericInfo(Strings.PlayingNotAvailable);
|
|
return;
|
|
}
|
|
|
|
if (Bot.PlayingWasBlocked) {
|
|
await Task.Delay(Bot.MinPlayingBlockedTTL * 1000).ConfigureAwait(false);
|
|
|
|
if (!Bot.IsPlayingPossible) {
|
|
Bot.ArchiLogger.LogGenericInfo(Strings.PlayingNotAvailable);
|
|
return;
|
|
}
|
|
}
|
|
|
|
KeepFarming = NowFarming = true;
|
|
Farm().Forget(); // Farm() will end when we're done farming, so don't wait for it
|
|
} finally {
|
|
FarmingSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
internal async Task StopFarming() {
|
|
if (!NowFarming) {
|
|
return;
|
|
}
|
|
|
|
await FarmingSemaphore.WaitAsync().ConfigureAwait(false);
|
|
|
|
try {
|
|
if (!NowFarming) {
|
|
return;
|
|
}
|
|
|
|
KeepFarming = false;
|
|
FarmResetEvent.Set();
|
|
|
|
for (byte i = 0; (i < 5) && NowFarming; i++) {
|
|
await Task.Delay(1000).ConfigureAwait(false);
|
|
}
|
|
|
|
if (NowFarming) {
|
|
NowFarming = false;
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericInfo(Strings.IdlingStopped);
|
|
Bot.OnFarmingStopped();
|
|
} finally {
|
|
FarmingSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
private async Task CheckGame(uint appID, string name, float hours) {
|
|
if ((appID == 0) || string.IsNullOrEmpty(name) || (hours < 0)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(appID) + " || " + nameof(name) + " || " + nameof(hours));
|
|
return;
|
|
}
|
|
|
|
ushort? cardsRemaining = await GetCardsRemaining(appID).ConfigureAwait(false);
|
|
if (!cardsRemaining.HasValue) {
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.WarningCouldNotCheckCardsStatus, appID, name));
|
|
return;
|
|
}
|
|
|
|
if (cardsRemaining.Value == 0) {
|
|
return;
|
|
}
|
|
|
|
GamesToFarm.Add(new Game(appID, name, hours, cardsRemaining.Value));
|
|
}
|
|
|
|
private async Task CheckGamesForFarming() {
|
|
if (NowFarming || Paused || !Bot.IsConnectedAndLoggedOn) {
|
|
return;
|
|
}
|
|
|
|
await StartFarming().ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task CheckPage(HtmlDocument htmlDocument) {
|
|
if (htmlDocument == null) {
|
|
Bot.ArchiLogger.LogNullError(nameof(htmlDocument));
|
|
return;
|
|
}
|
|
|
|
HtmlNodeCollection htmlNodes = htmlDocument.DocumentNode.SelectNodes("//div[@class='badge_title_stats_content']");
|
|
if (htmlNodes == null) {
|
|
// No eligible badges whatsoever
|
|
return;
|
|
}
|
|
|
|
HashSet<Task> backgroundTasks = new HashSet<Task>();
|
|
|
|
foreach (HtmlNode htmlNode in htmlNodes) {
|
|
HtmlNode appIDNode = htmlNode.SelectSingleNode(".//div[@class='card_drop_info_dialog']");
|
|
if (appIDNode == null) {
|
|
// It's just a badge, nothing more
|
|
continue;
|
|
}
|
|
|
|
string appIDString = appIDNode.GetAttributeValue("id", null);
|
|
if (string.IsNullOrEmpty(appIDString)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(appIDString));
|
|
continue;
|
|
}
|
|
|
|
string[] appIDSplitted = appIDString.Split('_');
|
|
if (appIDSplitted.Length < 5) {
|
|
Bot.ArchiLogger.LogNullError(nameof(appIDSplitted));
|
|
continue;
|
|
}
|
|
|
|
appIDString = appIDSplitted[4];
|
|
|
|
if (!uint.TryParse(appIDString, out uint appID) || (appID == 0)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(appID));
|
|
continue;
|
|
}
|
|
|
|
if (GlobalConfig.GlobalBlacklist.Contains(appID) || Program.GlobalConfig.Blacklist.Contains(appID)) {
|
|
// We have this appID blacklisted, so skip it
|
|
continue;
|
|
}
|
|
|
|
if (IgnoredAppIDs.TryGetValue(appID, out DateTime lastPICSReport)) {
|
|
if (lastPICSReport.AddHours(HoursToIgnore) < DateTime.UtcNow) {
|
|
// This game served its time as being ignored
|
|
IgnoredAppIDs.TryRemove(appID, out lastPICSReport);
|
|
} else {
|
|
// This game is still ignored
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Cards
|
|
HtmlNode progressNode = htmlNode.SelectSingleNode(".//span[@class='progress_info_bold']");
|
|
if (progressNode == null) {
|
|
Bot.ArchiLogger.LogNullError(nameof(progressNode));
|
|
continue;
|
|
}
|
|
|
|
string progressText = progressNode.InnerText;
|
|
if (string.IsNullOrEmpty(progressText)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(progressText));
|
|
continue;
|
|
}
|
|
|
|
ushort cardsRemaining = 0;
|
|
Match progressMatch = Regex.Match(progressText, @"\d+");
|
|
|
|
// This might fail if we have no card drops remaining, 0 is not printed in this case - that's fine
|
|
if (progressMatch.Success) {
|
|
if (!ushort.TryParse(progressMatch.Value, out cardsRemaining) || (cardsRemaining == 0)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(cardsRemaining));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (cardsRemaining == 0) {
|
|
// Normally we'd trust this information and simply skip the rest
|
|
// However, Steam is so fucked up that we can't simply assume that it's correct
|
|
// It's entirely possible that actual game page has different info, and badge page lied to us
|
|
// We can't check every single game though, as this will literally kill people with cards from games they don't own
|
|
if (!UntrustedAppIDs.Contains(appID)) {
|
|
continue;
|
|
}
|
|
|
|
// To save us on extra work, check cards earned so far first
|
|
HtmlNode cardsEarnedNode = htmlNode.SelectSingleNode(".//div[@class='card_drop_info_header']");
|
|
if (cardsEarnedNode == null) {
|
|
Bot.ArchiLogger.LogNullError(nameof(cardsEarnedNode));
|
|
continue;
|
|
}
|
|
|
|
string cardsEarnedText = cardsEarnedNode.InnerText;
|
|
if (string.IsNullOrEmpty(cardsEarnedText)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(cardsEarnedText));
|
|
continue;
|
|
}
|
|
|
|
Match cardsEarnedMatch = Regex.Match(cardsEarnedText, @"\d+");
|
|
if (!cardsEarnedMatch.Success) {
|
|
Bot.ArchiLogger.LogNullError(nameof(cardsEarnedMatch));
|
|
continue;
|
|
}
|
|
|
|
if (!ushort.TryParse(cardsEarnedMatch.Value, out ushort cardsEarned)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(cardsEarned));
|
|
continue;
|
|
}
|
|
|
|
if (cardsEarned > 0) {
|
|
// If we already earned some cards for this game, it's very likely that it's done
|
|
// Let's hope that trusting cardsRemaining AND cardsEarned is enough
|
|
// If I ever hear that it's not, I'll most likely need a doctor
|
|
continue;
|
|
}
|
|
|
|
// If we have no cardsRemaining and no cardsEarned, it's either:
|
|
// - A game we don't own physically, but we have cards from it in inventory
|
|
// - F2P game that we didn't spend any money in, but we have cards from it in inventory
|
|
// - Steam fuckup
|
|
// As you can guess, we must follow the rest of the logic in case of Steam fuckup
|
|
// Please kill me ;_;
|
|
}
|
|
|
|
// Hours
|
|
HtmlNode timeNode = htmlNode.SelectSingleNode(".//div[@class='badge_title_stats_playtime']");
|
|
if (timeNode == null) {
|
|
Bot.ArchiLogger.LogNullError(nameof(timeNode));
|
|
continue;
|
|
}
|
|
|
|
string hoursString = timeNode.InnerText;
|
|
if (string.IsNullOrEmpty(hoursString)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(hoursString));
|
|
continue;
|
|
}
|
|
|
|
float hours = 0.0F;
|
|
Match hoursMatch = Regex.Match(hoursString, @"[0-9\.,]+");
|
|
|
|
// This might fail if we have exactly 0.0 hours played, as it's not printed in that case - that's fine
|
|
if (hoursMatch.Success) {
|
|
if (!float.TryParse(hoursMatch.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out hours) || (hours <= 0.0F)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(hours));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Names
|
|
HtmlNode nameNode = htmlNode.SelectSingleNode("(.//div[@class='card_drop_info_body'])[last()]");
|
|
if (nameNode == null) {
|
|
Bot.ArchiLogger.LogNullError(nameof(nameNode));
|
|
continue;
|
|
}
|
|
|
|
string name = nameNode.InnerText;
|
|
if (string.IsNullOrEmpty(name)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(name));
|
|
continue;
|
|
}
|
|
|
|
// We handle two cases here - normal one, and no card drops remaining
|
|
int nameStartIndex = name.IndexOf(" by playing ", StringComparison.Ordinal);
|
|
if (nameStartIndex <= 0) {
|
|
nameStartIndex = name.IndexOf("You don't have any more drops remaining for ", StringComparison.Ordinal);
|
|
if (nameStartIndex <= 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(nameStartIndex));
|
|
continue;
|
|
}
|
|
|
|
nameStartIndex += 32; // + 12 below
|
|
}
|
|
|
|
nameStartIndex += 12;
|
|
|
|
int nameEndIndex = name.LastIndexOf('.');
|
|
if (nameEndIndex <= nameStartIndex) {
|
|
Bot.ArchiLogger.LogNullError(nameof(nameEndIndex));
|
|
continue;
|
|
}
|
|
|
|
name = WebUtility.HtmlDecode(name.Substring(nameStartIndex, nameEndIndex - nameStartIndex));
|
|
|
|
// We have two possible cases here
|
|
// Either we have decent info about appID, name, hours and cardsRemaining (cardsRemaining > 0)
|
|
// OR we strongly believe that Steam lied to us, in this case we will need to check game invidually (cardsRemaining == 0)
|
|
|
|
if (cardsRemaining > 0) {
|
|
GamesToFarm.Add(new Game(appID, name, hours, cardsRemaining));
|
|
} else {
|
|
Task task = CheckGame(appID, name, hours);
|
|
switch (Program.GlobalConfig.OptimizationMode) {
|
|
case GlobalConfig.EOptimizationMode.MinMemoryUsage:
|
|
await task.ConfigureAwait(false);
|
|
break;
|
|
default:
|
|
backgroundTasks.Add(task);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we have any background tasks, wait for them
|
|
if (backgroundTasks.Count > 0) {
|
|
await Task.WhenAll(backgroundTasks).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private async Task CheckPage(byte page) {
|
|
if (page == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(page));
|
|
return;
|
|
}
|
|
|
|
HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetBadgePage(page).ConfigureAwait(false);
|
|
if (htmlDocument == null) {
|
|
return;
|
|
}
|
|
|
|
await CheckPage(htmlDocument).ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task Farm() {
|
|
do {
|
|
// Now the algorithm used for farming depends on whether account is restricted or not
|
|
if (Bot.BotConfig.CardDropsRestricted) {
|
|
// If we have restricted card drops, we use complex algorithm
|
|
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.ChosenFarmingAlgorithm, "Complex"));
|
|
while (GamesToFarm.Count > 0) {
|
|
HashSet<Game> gamesToFarmSolo = GamesToFarm.Count > 1 ? new HashSet<Game>(GamesToFarm.Where(game => game.HoursPlayed >= HoursToBump)) : new HashSet<Game>(GamesToFarm);
|
|
if (gamesToFarmSolo.Count > 0) {
|
|
while (gamesToFarmSolo.Count > 0) {
|
|
Game game = gamesToFarmSolo.First();
|
|
if (await FarmSolo(game).ConfigureAwait(false)) {
|
|
gamesToFarmSolo.Remove(game);
|
|
} 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))));
|
|
} else {
|
|
NowFarming = false;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// 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;
|
|
}
|
|
|
|
NowFarming = false;
|
|
return;
|
|
}
|
|
}
|
|
} while ((await IsAnythingToFarm().ConfigureAwait(false)).GetValueOrDefault());
|
|
|
|
CurrentGamesFarming.ClearAndTrim();
|
|
NowFarming = false;
|
|
|
|
Bot.ArchiLogger.LogGenericInfo(Strings.IdlingFinished);
|
|
await Bot.OnFarmingFinished(true).ConfigureAwait(false);
|
|
}
|
|
|
|
private async Task<bool> Farm(Game game) {
|
|
if (game == null) {
|
|
Bot.ArchiLogger.LogNullError(nameof(game));
|
|
return false;
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
Bot.PlayGame(appID, 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;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
} else {
|
|
IgnoredAppIDs[game.AppID] = DateTime.UtcNow;
|
|
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.IdlingGameNotPossible, game.AppID, game.GameName));
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.StoppedIdling, game.AppID, game.GameName));
|
|
return success;
|
|
}
|
|
|
|
private bool FarmHours(ConcurrentHashSet<Game> games) {
|
|
if ((games == null) || (games.Count == 0)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(games));
|
|
return false;
|
|
}
|
|
|
|
float maxHour = games.Max(game => game.HoursPlayed);
|
|
if (maxHour < 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(maxHour));
|
|
return false;
|
|
}
|
|
|
|
if (maxHour >= HoursToBump) {
|
|
Bot.ArchiLogger.LogGenericError(string.Format(Strings.ErrorIsInvalid, nameof(maxHour)));
|
|
return true;
|
|
}
|
|
|
|
Bot.PlayGames(games.Select(game => game.AppID), Bot.BotConfig.CustomGamePlayedWhileFarming);
|
|
|
|
bool success = true;
|
|
while (maxHour < 2) {
|
|
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.StillIdlingList, string.Join(", ", games.Select(game => game.AppID))));
|
|
|
|
DateTime startFarmingPeriod = DateTime.UtcNow;
|
|
if (FarmResetEvent.Wait(60 * 1000 * Program.GlobalConfig.FarmingDelay)) {
|
|
FarmResetEvent.Reset();
|
|
success = KeepFarming;
|
|
}
|
|
|
|
// Don't forget to update our GamesToFarm hours
|
|
float timePlayed = (float) DateTime.UtcNow.Subtract(startFarmingPeriod).TotalHours;
|
|
foreach (Game game in games) {
|
|
game.HoursPlayed += timePlayed;
|
|
}
|
|
|
|
if (!success) {
|
|
break;
|
|
}
|
|
|
|
maxHour += timePlayed;
|
|
}
|
|
|
|
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.StoppedIdlingList, string.Join(", ", games.Select(game => game.AppID))));
|
|
return success;
|
|
}
|
|
|
|
private bool FarmMultiple(IEnumerable<Game> games) {
|
|
if (games == null) {
|
|
Bot.ArchiLogger.LogNullError(nameof(games));
|
|
return false;
|
|
}
|
|
|
|
CurrentGamesFarming.ReplaceWith(games);
|
|
|
|
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.NowIdlingList, string.Join(", ", CurrentGamesFarming.Select(game => game.AppID))));
|
|
|
|
bool result = FarmHours(CurrentGamesFarming);
|
|
CurrentGamesFarming.ClearAndTrim();
|
|
return result;
|
|
}
|
|
|
|
private async Task<bool> FarmSolo(Game game) {
|
|
if (game == null) {
|
|
Bot.ArchiLogger.LogNullError(nameof(game));
|
|
return true;
|
|
}
|
|
|
|
CurrentGamesFarming.Add(game);
|
|
|
|
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.NowIdling, game.AppID, game.GameName));
|
|
|
|
bool result = await Farm(game).ConfigureAwait(false);
|
|
CurrentGamesFarming.ClearAndTrim();
|
|
|
|
if (!result) {
|
|
return false;
|
|
}
|
|
|
|
GamesToFarm.Remove(game);
|
|
|
|
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.IdlingFinishedForGame, game.AppID, game.GameName, TimeSpan.FromHours(game.HoursPlayed).ToHumanReadable()));
|
|
return true;
|
|
}
|
|
|
|
private async Task<ushort?> GetCardsRemaining(uint appID) {
|
|
if (appID == 0) {
|
|
Bot.ArchiLogger.LogNullError(nameof(appID));
|
|
return 0;
|
|
}
|
|
|
|
HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetGameCardsPage(appID).ConfigureAwait(false);
|
|
|
|
HtmlNode progressNode = htmlDocument?.DocumentNode.SelectSingleNode("//span[@class='progress_info_bold']");
|
|
if (progressNode == null) {
|
|
return null;
|
|
}
|
|
|
|
string progress = progressNode.InnerText;
|
|
if (string.IsNullOrEmpty(progress)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(progress));
|
|
return null;
|
|
}
|
|
|
|
Match match = Regex.Match(progress, @"\d+");
|
|
if (!match.Success) {
|
|
return 0;
|
|
}
|
|
|
|
if (ushort.TryParse(match.Value, out ushort cardsRemaining) && (cardsRemaining != 0)) {
|
|
return cardsRemaining;
|
|
}
|
|
|
|
Bot.ArchiLogger.LogNullError(nameof(cardsRemaining));
|
|
return null;
|
|
}
|
|
|
|
private async Task<bool?> IsAnythingToFarm() {
|
|
// Find the number of badge pages
|
|
Bot.ArchiLogger.LogGenericInfo(Strings.CheckingFirstBadgePage);
|
|
HtmlDocument htmlDocument = await Bot.ArchiWebHandler.GetBadgePage(1).ConfigureAwait(false);
|
|
if (htmlDocument == null) {
|
|
Bot.ArchiLogger.LogGenericWarning(Strings.WarningCouldNotCheckBadges);
|
|
return null;
|
|
}
|
|
|
|
byte maxPages = 1;
|
|
|
|
HtmlNode htmlNode = htmlDocument.DocumentNode.SelectSingleNode("(//a[@class='pagelink'])[last()]");
|
|
if (htmlNode != null) {
|
|
string lastPage = htmlNode.InnerText;
|
|
if (string.IsNullOrEmpty(lastPage)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(lastPage));
|
|
return null;
|
|
}
|
|
|
|
if (!byte.TryParse(lastPage, out maxPages) || (maxPages == 0)) {
|
|
Bot.ArchiLogger.LogNullError(nameof(maxPages));
|
|
return null;
|
|
}
|
|
}
|
|
|
|
GamesToFarm.ClearAndTrim();
|
|
|
|
List<Task> tasks = new List<Task>();
|
|
Task mainTask = CheckPage(htmlDocument);
|
|
|
|
switch (Program.GlobalConfig.OptimizationMode) {
|
|
case GlobalConfig.EOptimizationMode.MinMemoryUsage:
|
|
await mainTask.ConfigureAwait(false);
|
|
break;
|
|
default:
|
|
tasks.Add(mainTask);
|
|
break;
|
|
}
|
|
|
|
if (maxPages > 1) {
|
|
Bot.ArchiLogger.LogGenericInfo(Strings.CheckingOtherBadgePages);
|
|
|
|
switch (Program.GlobalConfig.OptimizationMode) {
|
|
case GlobalConfig.EOptimizationMode.MinMemoryUsage:
|
|
for (byte page = 2; page <= maxPages; page++) {
|
|
await CheckPage(page).ConfigureAwait(false);
|
|
}
|
|
|
|
break;
|
|
default:
|
|
for (byte page = 2; page <= maxPages; page++) {
|
|
// We need a copy of variable being passed when in for loops, as loop will proceed before our task is launched
|
|
byte currentPage = page;
|
|
tasks.Add(CheckPage(currentPage));
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (tasks.Count > 0) {
|
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
}
|
|
|
|
if (GamesToFarm.Count == 0) {
|
|
return false;
|
|
}
|
|
|
|
SortGamesToFarm();
|
|
return true;
|
|
}
|
|
|
|
private async Task<bool?> ShouldFarm(Game game) {
|
|
if (game == null) {
|
|
Bot.ArchiLogger.LogNullError(nameof(game));
|
|
return false;
|
|
}
|
|
|
|
ushort? cardsRemaining = await GetCardsRemaining(game.AppID).ConfigureAwait(false);
|
|
if (!cardsRemaining.HasValue) {
|
|
Bot.ArchiLogger.LogGenericWarning(string.Format(Strings.WarningCouldNotCheckCardsStatus, game.AppID, game.GameName));
|
|
return null;
|
|
}
|
|
|
|
game.CardsRemaining = cardsRemaining.Value;
|
|
|
|
Bot.ArchiLogger.LogGenericInfo(string.Format(Strings.IdlingStatusForGame, game.AppID, game.GameName, game.CardsRemaining));
|
|
return game.CardsRemaining > 0;
|
|
}
|
|
|
|
private void SortGamesToFarm() {
|
|
IOrderedEnumerable<Game> gamesToFarm;
|
|
|
|
switch (Bot.BotConfig.FarmingOrder) {
|
|
case BotConfig.EFarmingOrder.Unordered:
|
|
return;
|
|
case BotConfig.EFarmingOrder.AppIDsAscending:
|
|
gamesToFarm = GamesToFarm.OrderBy(game => game.AppID);
|
|
break;
|
|
case BotConfig.EFarmingOrder.AppIDsDescending:
|
|
gamesToFarm = GamesToFarm.OrderByDescending(game => game.AppID);
|
|
break;
|
|
case BotConfig.EFarmingOrder.CardDropsAscending:
|
|
gamesToFarm = GamesToFarm.OrderBy(game => game.CardsRemaining);
|
|
break;
|
|
case BotConfig.EFarmingOrder.CardDropsDescending:
|
|
gamesToFarm = GamesToFarm.OrderByDescending(game => game.CardsRemaining);
|
|
break;
|
|
case BotConfig.EFarmingOrder.HoursAscending:
|
|
gamesToFarm = GamesToFarm.OrderBy(game => game.HoursPlayed);
|
|
break;
|
|
case BotConfig.EFarmingOrder.HoursDescending:
|
|
gamesToFarm = GamesToFarm.OrderByDescending(game => game.HoursPlayed);
|
|
break;
|
|
case BotConfig.EFarmingOrder.NamesAscending:
|
|
gamesToFarm = GamesToFarm.OrderBy(game => game.GameName);
|
|
break;
|
|
case BotConfig.EFarmingOrder.NamesDescending:
|
|
gamesToFarm = GamesToFarm.OrderByDescending(game => game.GameName);
|
|
break;
|
|
case BotConfig.EFarmingOrder.Random:
|
|
gamesToFarm = GamesToFarm.OrderBy(game => Utilities.RandomNext());
|
|
break;
|
|
default:
|
|
Bot.ArchiLogger.LogGenericError(string.Format(Strings.ErrorIsInvalid, nameof(Bot.BotConfig.FarmingOrder)));
|
|
return;
|
|
}
|
|
|
|
GamesToFarm.ReplaceWith(gamesToFarm.ToList()); // We must call ToList() here as we can't enumerate during replacing
|
|
}
|
|
|
|
internal sealed class Game {
|
|
[JsonProperty]
|
|
internal readonly uint AppID;
|
|
|
|
[JsonProperty]
|
|
internal readonly string GameName;
|
|
|
|
[JsonProperty]
|
|
internal ushort CardsRemaining { get; set; }
|
|
|
|
[JsonProperty]
|
|
internal float HoursPlayed { get; set; }
|
|
|
|
//internal string HeaderURL => "https://steamcdn-a.akamaihd.net/steam/apps/" + AppID + "/header.jpg";
|
|
|
|
internal Game(uint appID, string gameName, float hoursPlayed, ushort cardsRemaining) {
|
|
if ((appID == 0) || string.IsNullOrEmpty(gameName) || (hoursPlayed < 0) || (cardsRemaining == 0)) {
|
|
throw new ArgumentOutOfRangeException(nameof(appID) + " || " + nameof(gameName) + " || " + nameof(hoursPlayed) + " || " + nameof(cardsRemaining));
|
|
}
|
|
|
|
AppID = appID;
|
|
GameName = gameName;
|
|
HoursPlayed = hoursPlayed;
|
|
CardsRemaining = cardsRemaining;
|
|
}
|
|
|
|
public override bool Equals(object obj) {
|
|
if (obj == null) {
|
|
return false;
|
|
}
|
|
|
|
if (obj == this) {
|
|
return true;
|
|
}
|
|
|
|
Game game = obj as Game;
|
|
return (game != null) && Equals(game);
|
|
}
|
|
|
|
public override int GetHashCode() => (int) AppID;
|
|
|
|
private bool Equals(Game other) => AppID == other.AppID;
|
|
}
|
|
}
|
|
} |