diff --git a/ArchiSteamFarm/ArchiHandler.cs b/ArchiSteamFarm/ArchiHandler.cs index 983b3efe5041b..0a705c98f324e 100644 --- a/ArchiSteamFarm/ArchiHandler.cs +++ b/ArchiSteamFarm/ArchiHandler.cs @@ -90,9 +90,23 @@ internal void DeclineClanInvite(ulong clanID) { Client.Send(request); } - internal void PlayGames(params ulong[] gameIDs) { + internal void PlayGames(params uint[] gameIDs) { var request = new ClientMsgProtobuf(EMsg.ClientGamesPlayed); - foreach (ulong gameID in gameIDs) { + foreach (uint gameID in gameIDs) { + if (gameID == 0) { + continue; + } + + request.Body.games_played.Add(new CMsgClientGamesPlayed.GamePlayed { + game_id = new GameID(gameID), + }); + } + Client.Send(request); + } + + internal void PlayGames(ICollection gameIDs) { + var request = new ClientMsgProtobuf(EMsg.ClientGamesPlayed); + foreach (uint gameID in gameIDs) { if (gameID == 0) { continue; } diff --git a/ArchiSteamFarm/Bot.cs b/ArchiSteamFarm/Bot.cs index 2204dcb9bed40..01f0f380c5d7e 100755 --- a/ArchiSteamFarm/Bot.cs +++ b/ArchiSteamFarm/Bot.cs @@ -254,10 +254,6 @@ internal async Task OnFarmingFinished() { } } - internal void PlayGame(params ulong[] gameIDs) { - ArchiHandler.PlayGames(gameIDs); - } - private void HandleCallbacks() { TimeSpan timeSpan = TimeSpan.FromMilliseconds(CallbackSleep); while (IsRunning) { @@ -291,7 +287,7 @@ private void ResponseStatus(ulong steamID, string botName = null) { } if (bot.CardsFarmer.CurrentGame > 0) { - SendMessageToUser(steamID, "Bot " + bot.BotName + " is currently farming appID " + bot.CardsFarmer.CurrentGame + " and has total of " + bot.CardsFarmer.GamesLeft + " games left to farm"); + SendMessageToUser(steamID, "Bot " + bot.BotName + " is currently farming appID " + bot.CardsFarmer.CurrentGame + " and has total of " + bot.CardsFarmer.GamesLeftCount + " games left to farm"); } SendMessageToUser(steamID, "Currently " + Bots.Count + " bots are running"); } diff --git a/ArchiSteamFarm/CardsFarmer.cs b/ArchiSteamFarm/CardsFarmer.cs index dbacd099fa514..2b5df78982803 100755 --- a/ArchiSteamFarm/CardsFarmer.cs +++ b/ArchiSteamFarm/CardsFarmer.cs @@ -23,7 +23,10 @@ limitations under the License. */ using HtmlAgilityPack; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -31,12 +34,13 @@ namespace ArchiSteamFarm { internal class CardsFarmer { private const byte StatusCheckSleep = 5; // In minutes, how long to wait before checking the appID again + private readonly ConcurrentDictionary GamesToFarm = new ConcurrentDictionary(); private readonly ManualResetEvent FarmResetEvent = new ManualResetEvent(false); private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1); private readonly Bot Bot; internal uint CurrentGame { get; private set; } = 0; - internal int GamesLeft { get; private set; } = 0; + internal int GamesLeftCount { get; private set; } = 0; private volatile bool NowFarming = false; @@ -44,6 +48,77 @@ internal CardsFarmer(Bot bot) { Bot = bot; } + internal static List GetGamesToFarmSolo(ConcurrentDictionary gamesToFarm) { + if (gamesToFarm == null) { + return null; + } + + List result = new List(); + foreach (KeyValuePair keyValue in gamesToFarm) { + if (keyValue.Value >= 2) { + result.Add(keyValue.Key); + } + } + + return result; + } + + internal static uint GetAnyGameToFarm(ConcurrentDictionary gamesToFarm) { + if (gamesToFarm == null) { + return 0; + } + + foreach (uint appID in gamesToFarm.Keys) { + return appID; + } + + return 0; + } + + internal bool FarmMultiple() { + if (GamesToFarm == null || GamesToFarm.Count == 0) { + return true; + } + + double maxHour = -1; + + foreach (KeyValuePair keyValue in GamesToFarm) { + if (keyValue.Value > maxHour) { + maxHour = keyValue.Value; + } + } + + Logging.LogGenericInfo(Bot.BotName, "Now farming: " + string.Join(", ", GamesToFarm.Keys)); + if (Farm(maxHour, GamesToFarm.Keys)) { + return true; + } else { + GamesLeftCount = 0; + CurrentGame = 0; + NowFarming = false; + return false; + } + } + + internal async Task FarmSolo(uint appID) { + if (appID == 0) { + return false; + } + + CurrentGame = appID; + Logging.LogGenericInfo(Bot.BotName, "Now farming: " + appID); + if (await Farm(appID).ConfigureAwait(false)) { + double hours; + GamesToFarm.TryRemove(appID, out hours); + GamesLeftCount--; + return true; + } else { + GamesLeftCount = 0; + CurrentGame = 0; + NowFarming = false; + return false; + } + } + internal async Task StartFarming() { await StopFarming().ConfigureAwait(false); @@ -71,7 +146,6 @@ internal async Task StartFarming() { } // Find APPIDs we need to farm - List appIDs = new List(); for (var page = 1; page <= maxPages; page++) { Logging.LogGenericInfo(Bot.BotName, "Checking page: " + page + "/" + maxPages); @@ -90,13 +164,11 @@ internal async Task StartFarming() { foreach (HtmlNode badgesPageNode in badgesPageNodes) { string steamLink = badgesPageNode.GetAttributeValue("href", null); if (steamLink == null) { - Logging.LogGenericError(Bot.BotName, "Couldn't get steamLink for one of the games: " + badgesPageNode.OuterHtml); continue; } uint appID = (uint) Utilities.OnlyNumbers(steamLink); if (appID == 0) { - Logging.LogGenericError(Bot.BotName, "Couldn't get appID for one of the games: " + badgesPageNode.OuterHtml); continue; } @@ -104,29 +176,86 @@ internal async Task StartFarming() { continue; } - appIDs.Add(appID); + // We assume that every game has at least 2 hours played, until we actually check them + GamesToFarm.AddOrUpdate(appID, 2, (key, value) => 2); + } + } + + // If we have restricted card drops, actually do check all games that are left to farm + if (Bot.CardDropsRestricted) { + foreach (uint appID in GamesToFarm.Keys) { + Logging.LogGenericInfo(Bot.BotName, "Checking hours of appID: " + appID); + HtmlDocument appPage = await Bot.ArchiWebHandler.GetGameCardsPage(appID).ConfigureAwait(false); + if (appPage == null) { + continue; + } + + HtmlNode appNode = appPage.DocumentNode.SelectSingleNode("//div[@class='badge_title_stats_playtime']"); + if (appNode == null) { + continue; + } + + string hoursString = appNode.InnerText; + if (string.IsNullOrEmpty(hoursString)) { + continue; + } + + hoursString = Regex.Match(hoursString, @"[0-9\.,]+").Value; + double hours; + + if (string.IsNullOrEmpty(hoursString)) { + hours = 0; + } else { + hours = double.Parse(hoursString, CultureInfo.InvariantCulture); + } + + GamesToFarm[appID] = hours; } } Logging.LogGenericInfo(Bot.BotName, "Farming in progress..."); - NowFarming = appIDs.Count > 0; + + GamesLeftCount = GamesToFarm.Count; + NowFarming = GamesLeftCount > 0; Semaphore.Release(); - GamesLeft = appIDs.Count; - - // Start farming - while (appIDs.Count > 0) { - uint appID = appIDs[0]; - CurrentGame = appID; - Logging.LogGenericInfo(Bot.BotName, "Now farming: " + appID); - if (await Farm(appID).ConfigureAwait(false)) { - appIDs.Remove(appID); - GamesLeft--; - } else { - GamesLeft = 0; - CurrentGame = 0; - NowFarming = false; - return; + // Now the algorithm used for farming depends on whether account is restricted or not + if (Bot.CardDropsRestricted) { + // If we have restricted card drops, we use complex algorithm, which prioritizes farming solo titles >= 2 hours, then all at once, until any game hits mentioned 2 hours + Logging.LogGenericInfo(Bot.BotName, "Chosen farming algorithm: Complex"); + while (GamesLeftCount > 0) { + List gamesToFarmSolo = GetGamesToFarmSolo(GamesToFarm); + if (gamesToFarmSolo.Count > 0) { + while (gamesToFarmSolo.Count > 0) { + uint appID = gamesToFarmSolo[0]; + bool success = await FarmSolo(appID).ConfigureAwait(false); + if (success) { + Logging.LogGenericInfo(Bot.BotName, "Done farming: " + appID); + gamesToFarmSolo.Remove(appID); + } else { + return; + } + } + } else { + bool success = FarmMultiple(); + if (success) { + Logging.LogGenericInfo(Bot.BotName, "Done farming: " + string.Join(", ", GamesToFarm.Keys)); + } else { + return; + } + } + } + } else { + // If we have unrestricted card drops, we use simple algorithm and farm cards one-by-one + Logging.LogGenericInfo(Bot.BotName, "Chosen farming algorithm: Simple"); + while (GamesLeftCount > 0) { + uint appID = GetAnyGameToFarm(GamesToFarm); + bool success = await FarmSolo(appID).ConfigureAwait(false); + if (success) { + Logging.LogGenericInfo(Bot.BotName, "Done farming: " + appID); + } else { + return; + } } } @@ -167,8 +296,8 @@ internal async Task StopFarming() { return result; } - private async Task Farm(ulong appID) { - Bot.PlayGame(appID); + private async Task Farm(uint appID) { + Bot.ArchiHandler.PlayGames(appID); bool success = true; bool? keepFarming = await ShouldFarm(appID).ConfigureAwait(false); @@ -181,9 +310,42 @@ private async Task Farm(ulong appID) { keepFarming = await ShouldFarm(appID).ConfigureAwait(false); } - Bot.PlayGame(0); + Bot.ArchiHandler.PlayGames(0); Logging.LogGenericInfo(Bot.BotName, "Stopped farming: " + appID); return success; } + + private bool Farm(double maxHour, ICollection appIDs) { + if (maxHour >= 2) { + return true; + } + + Bot.ArchiHandler.PlayGames(appIDs); + + bool success = true; + while (maxHour < 2) { + Logging.LogGenericInfo(Bot.BotName, "Still farming: " + string.Join(", ", appIDs)); + if (FarmResetEvent.WaitOne(1000 * 60 * StatusCheckSleep)) { + success = false; + break; + } + + // Don't forget to update our GamesToFarm hours + double timePlayed = StatusCheckSleep / 60.0; + foreach (KeyValuePair keyValue in GamesToFarm) { + if (!appIDs.Contains(keyValue.Key)) { + continue; + } + + GamesToFarm[keyValue.Key] = keyValue.Value + timePlayed; + } + + maxHour += timePlayed; + } + + Bot.ArchiHandler.PlayGames(0); + Logging.LogGenericInfo(Bot.BotName, "Stopped farming: " + string.Join(", ", appIDs)); + return success; + } } } diff --git a/ArchiSteamFarm/config/example.xml b/ArchiSteamFarm/config/example.xml index fd4612d595d7d..3cbeb09904809 100644 --- a/ArchiSteamFarm/config/example.xml +++ b/ArchiSteamFarm/config/example.xml @@ -46,7 +46,7 @@ - +