private async Task CheckPage(HtmlDocument htmlDocument) { if (htmlDocument == null) { Bot.ArchiLogger.LogNullError(nameof(htmlDocument)); return; } HtmlNodeCollection htmlNodes = htmlDocument.DocumentNode.SelectNodes("//div[@class='badge_row_inner']"); if (htmlNodes == null) { // No eligible badges whatsoever return; } HashSet <Task> backgroundTasks = new HashSet <Task>(); foreach (HtmlNode htmlNode in htmlNodes) { HtmlNode statsNode = htmlNode.SelectSingleNode(".//div[@class='badge_title_stats_content']"); HtmlNode appIDNode = statsNode?.SelectSingleNode(".//div[@class='card_drop_info_dialog']"); if (appIDNode == null) { // It's just a badge, nothing more continue; } string appIDText = appIDNode.GetAttributeValue("id", null); if (string.IsNullOrEmpty(appIDText)) { Bot.ArchiLogger.LogNullError(nameof(appIDText)); continue; } string[] appIDSplitted = appIDText.Split('_'); if (appIDSplitted.Length < 5) { Bot.ArchiLogger.LogNullError(nameof(appIDSplitted)); continue; } appIDText = appIDSplitted[4]; if (!uint.TryParse(appIDText, out uint appID) || (appID == 0)) { Bot.ArchiLogger.LogNullError(nameof(appID)); continue; } if (GlobalConfig.SalesBlacklist.Contains(appID) || Program.GlobalConfig.Blacklist.Contains(appID) || Bot.IsBlacklistedFromIdling(appID) || (Bot.BotConfig.IdlePriorityQueueOnly && !Bot.IsPriorityIdling(appID))) { // We're configured to ignore this appID, so skip it continue; } if (IgnoredAppIDs.TryGetValue(appID, out DateTime ignoredUntil)) { if (ignoredUntil < DateTime.UtcNow) { // This game served its time as being ignored IgnoredAppIDs.TryRemove(appID, out _); } else { // This game is still ignored continue; } } // Cards HtmlNode progressNode = statsNode.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 f****d 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 // Luckily for us, it seems to happen only with some specific games if (!UntrustedAppIDs.Contains(appID)) { continue; } // To save us on extra work, check cards earned so far first HtmlNode cardsEarnedNode = statsNode.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 = statsNode.SelectSingleNode(".//div[@class='badge_title_stats_playtime']"); if (timeNode == null) { Bot.ArchiLogger.LogNullError(nameof(timeNode)); continue; } string hoursText = timeNode.InnerText; if (string.IsNullOrEmpty(hoursText)) { Bot.ArchiLogger.LogNullError(nameof(hoursText)); continue; } float hours = 0.0F; Match hoursMatch = Regex.Match(hoursText, @"[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 = statsNode.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)); // Levels byte badgeLevel = 0; HtmlNode levelNode = htmlNode.SelectSingleNode(".//div[@class='badge_info_description']/div[2]"); if (levelNode != null) { // There is no levelNode if we didn't craft that badge yet (level 0) string levelText = levelNode.InnerText; if (string.IsNullOrEmpty(levelText)) { Bot.ArchiLogger.LogNullError(nameof(levelText)); continue; } int levelIndex = levelText.IndexOf("Level ", StringComparison.OrdinalIgnoreCase); if (levelIndex < 0) { Bot.ArchiLogger.LogNullError(nameof(levelIndex)); continue; } levelIndex += 6; if (levelText.Length <= levelIndex) { Bot.ArchiLogger.LogNullError(nameof(levelIndex)); continue; } levelText = levelText.Substring(levelIndex, 1); if (!byte.TryParse(levelText, out badgeLevel) || (badgeLevel == 0) || (badgeLevel > 5)) { Bot.ArchiLogger.LogNullError(nameof(badgeLevel)); continue; } } // Done with parsing, we have two possible cases here // Either we have decent info about appID, name, hours, cardsRemaining (cardsRemaining > 0) and level // 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, badgeLevel)); } else { Task task = CheckGame(appID, name, hours, badgeLevel); 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); } }