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> extraTasks = 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];
uint appID;
if (!uint.TryParse(appIDString, out 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;
}
// 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, that's fine
if (progressMatch.Success) {
if (!ushort.TryParse(progressMatch.Value, out cardsRemaining)) {
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']");
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;
}
ushort cardsEarned;
if (!ushort.TryParse(cardsEarnedMatch.Value, out 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;
Match hoursMatch = Regex.Match(hoursString, @"[0-9\.,]+");
// This might fail if we have exactly 0.0 hours played, that's fine
if (hoursMatch.Success) {
if (!float.TryParse(hoursMatch.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out hours)) {
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 = 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 {
extraTasks.Add(CheckGame(appID, name, hours));
}
}
// If we have any pending tasks, wait for them
await Task.WhenAll(extraTasks).ConfigureAwait(false);
}