Skip to content

Instantly share code, notes, and snippets.

@Ne3tCode
Last active July 8, 2024 11:36
Show Gist options
  • Save Ne3tCode/bd5512b8d6b677255bf3038cd23f01e7 to your computer and use it in GitHub Desktop.
Save Ne3tCode/bd5512b8d6b677255bf3038cd23f01e7 to your computer and use it in GitHub Desktop.
ASF bot list trade matcher
// ==UserScript==
// @name ASF STM (Nephrite Mod)
// @description ASF bot list trade matcher
// @license Apache-2.0
// @author Rudokhvist, Nephrite
// @match https://steamcommunity.com/id/*/badges*
// @match https://steamcommunity.com/profiles/*/badges*
// @version 2.14-custom
// @connect asf.justarchi.net
// @grant GM.xmlHttpRequest
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// ==/UserScript==
/* global BigInt */
(function () {
"use strict";
//configuration
const weblimiter = 50;
const errorLimiter = 30000;
const debug = false;
const maxErrors = 3;
const botCacheTime = 5 * 60000;
const maxBotsToScan = 100;
const bRandomizeBots = false;
const maxPagesLimit = 0;
const filterBackgroundColor = "rgba(23, 26, 33, 0.8)";
// Note: disables `maxPagesLimit` if not empty. ToDo: limit number of IDs
const appIdsToScanOverride = [];
//styles
const css = `
#asf_stm_filters_body {
max-height: calc(100vh - 95px);
overflow-y: auto;
display: grid;
grid-template-columns: auto;
}
.asf_stm_trashbusket_button {
font-size: 30px;
width: 30px;
text-align: center;
line-height: 30px;
vertical-align: bottom;
}
`;
const EType = {
TradingCardFoil: 3,
TradingCard: 5
};
//do not change
let myProfileLink = "";
let errors = 0;
let bots = null;
let myBadges = [];
let botBadges = [];
let maxPages = 1;
let stop = false;
let classIdsDB = JSON.parse(localStorage.getItem("Ryzhehvost.ASF.STM.ClassIDs"));
if (classIdsDB === null) {
classIdsDB = new Object();
}
function debugTime(name) {
if (debug) {
console.time(name);
}
}
function debugTimeEnd(name) {
if (debug) {
console.timeEnd(name);
}
}
function debugPrint(msg) {
if (debug) {
console.log(new Date().toLocaleTimeString("en-GB", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit", fractionalSecondDigits: 3 }) + " : " + msg);
}
}
function deepClone(object) {
return JSON.parse(JSON.stringify(object));
}
function getPartner(str) {
if (typeof BigInt !== "undefined") {
return (BigInt(str) % BigInt(4294967296)).toString();
} else {
let result = 0;
for (let i = 0; i < str.length; i++) {
result = (result * 10 + Number(str[i])) % 4294967296;
}
return result;
}
}
function enableButton() {
let buttonDiv = document.getElementById("asf_stm_button_div");
buttonDiv.setAttribute("class", "profile_small_header_additional");
buttonDiv.setAttribute("title", "Scan ASF STM");
let button = document.getElementById("asf_stm_button");
button.addEventListener("click", buttonPressedEvent, false);
}
function disableButton() {
let buttonDiv = document.getElementById("asf_stm_button_div");
buttonDiv.setAttribute("class", "profile_small_header_additional btn_disabled");
buttonDiv.setAttribute("title", "Scan is in process");
let button = document.getElementById("asf_stm_button");
button.removeEventListener("click", buttonPressedEvent, false);
}
function updateMessage(text) {
let message = document.getElementById("asf_stm_message");
message.textContent = text;
}
function hideMessage() {
let messageBox = document.getElementById("asf_stm_messagebox");
messageBox.setAttribute("style", "display: none;");
}
function hideThrobber() {
let throbber = document.getElementById("throbber");
throbber.setAttribute("style", "display: none;");
}
function updateProgress(index, total) {
const bar = document.getElementById("asf_stm_progress");
let progress = 0;
if (total > 0) {
progress = 100 * ((index + 1) / total);
}
bar.style.width = `${progress}%`;
bar.style.transitionDuration = "0.5s";
if (progress == 100) {
bar.style.transitionDuration = "0s";
}
}
function getClassIDs(index) {
updateMessage("Updating cards database for badge " + (index + 1) + " of " + myBadges.length);
debugPrint("getClassIDs for " + myBadges[index].appId);
for (let i = 0; i < myBadges[index].maxCards; i++) {
if (classIdsDB.hasOwnProperty(myBadges[index].appId) && classIdsDB[myBadges[index].appId].hasOwnProperty(myBadges[index].cards[i].item)) {
if (i == myBadges[index].maxCards - 1) {
//it's last card, so it means we have them all
index++;
if (index < myBadges.length) {
setTimeout(getClassIDs, 0, index); // because recursion
//getClassIDs(index);
} else {
//debugPrint(JSON.stringify(classIdsDB));
localStorage.setItem("Ryzhehvost.ASF.STM.ClassIDs", JSON.stringify(classIdsDB));
setTimeout(GetCards, weblimiter, 0, 0);
}
return;
}
} else {
break; //missing something, update needed
}
}
let searchUrl = "https://steamcommunity.com/market/search/render/?start=0&count=30&search_descriptions=0&appid=753&category_753_Game[]=tag_app_" + myBadges[index].appId + "&category_753_cardborder[]=tag_cardborder_" + myBadges[index].border + "&norender=1";
debugPrint(searchUrl);
let xhr = new XMLHttpRequest(); // ToDo: GM_xmlhttpRequest
xhr.open("GET", searchUrl, true);
xhr.responseType = "json";
xhr.onload = function () {
if (stop) {
updateMessage("Interrupted by user");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
let status = xhr.status;
if (status === 200) {
let searchResponse = xhr.response;
//debugPrint(JSON.stringify(searchResponse));
if (searchResponse.success == true && searchResponse.total_count >= myBadges[index].maxCards) {
let results = searchResponse.results;
errors = 0;
for (let cardnumber = 0; cardnumber < myBadges[index].maxCards; cardnumber++) {
debugPrint("looking for card " + myBadges[index].cards[cardnumber].item);
//debugPrint(myBadges[index].cards[cardnumber].item);
for (let i = 0; i < results.length; i++) {
//debugPrint(results[i].hash_name);
if (results[i].hash_name === myBadges[index].cards[cardnumber].item) {
debugPrint("found!");
let classid = results[i].asset_description.classid;
if (classIdsDB[myBadges[index].appId] === undefined) {
classIdsDB[myBadges[index].appId] = new Object();
}
let cardsClasses = classIdsDB[myBadges[index].appId];
cardsClasses[myBadges[index].cards[cardnumber].item] = classid;
break;
}
}
if (!(classIdsDB.hasOwnProperty(myBadges[index].appId) && classIdsDB[myBadges[index].appId].hasOwnProperty(myBadges[index].cards[cardnumber].item))) {
//still not found...
updateMessage('Error getting classid for card "' + myBadges[index].cards[cardnumber].item + '" from ' + myBadges[index].appId + ", please report this!");
// exclude this badge from matching
myBadges[index].maxSets = myBadges[index].lastSet = 0;
myBadges[index].cards.forEach((card) => {
card.count = 0;
});
errors++;
break;
}
}
index++;
} else {
updateMessage("Error getting card data for " + myBadges[index].appId + ", please report this!");
debugPrint("Error getting card data for " + myBadges[index].appId + ", please report this!");
localStorage.setItem("Ryzhehvost.ASF.STM.ClassIDs", JSON.stringify(classIdsDB));
errors++;
}
if (index < myBadges.length) {
setTimeout(getClassIDs, weblimiter + errorLimiter * errors, index);
} else {
//debugPrint(JSON.stringify(classIdsDB));
localStorage.setItem("Ryzhehvost.ASF.STM.ClassIDs", JSON.stringify(classIdsDB));
setTimeout(GetCards, weblimiter, 0, 0);
}
return;
} else {
errors++;
}
if ((status < 400 || status >= 500) && errors <= maxErrors) {
setTimeout(getClassIDs, weblimiter + errorLimiter * errors, index);
} else {
if (status != 200) {
updateMessage("Error getting classid, ERROR " + status);
} else {
//ToDo: should never happen
updateMessage("Error getting classid, malformed json");
}
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
};
xhr.onerror = function () {
if (stop) {
updateMessage("Interrupted by user");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
errors++;
if (errors <= maxErrors) {
setTimeout(getClassIDs, weblimiter + errorLimiter * errors, index);
return;
} else {
debugPrint("error");
updateMessage("Error getting classid");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
};
xhr.send();
}
function populateCards(item) {
let classList = "";
let htmlCards = "";
for (let j = 0; j < item.cards.length; j++) {
let itemIcon = item.cards[j].iconUrl;
let itemName = item.cards[j].item.substring(item.cards[j].item.indexOf("-") + 1);
for (let k = 0; k < item.cards[j].count; k++) {
if (classList != "") {
classList += ";";
}
classList += classIdsDB[item.appId][item.cards[j].item];
let cardTemplate = `
<div class="showcase_slot">
<img class="image-container" src="${itemIcon}/98x115">
<div class="commentthread_subscribe_hint" style="width: 98px;">
<a target="_blank" rel="noopener noreferrer" href="https://steamcommunity.com/market/listings/753/${encodeURIComponent(item.cards[j].item)}">${itemName}</a>
</div>
</div>
`;
htmlCards += cardTemplate;
}
}
return {
htmlCards: htmlCards,
classList: classList,
};
}
function getClasses(item) {
let classes = "";
for (let j = 0; j < item.cards.length; j++) {
for (let k = 0; k < item.cards[j].count; k++) {
if (classes != "") {
classes += ";";
}
classes += classIdsDB[item.appId][item.cards[j].item];
}
}
return classes;
}
function updateTrade(row) {
let index = row.id.split("_")[1];
let tradeLink = row.getElementsByClassName("full_trade_url")[0];
let splitUrl = tradeLink.href.split("&");
let them = "";
let you = "";
for (let i = 0; i < bots.Result[index].itemsToSend.length; i++) {
let appId = bots.Result[index].itemsToSend[i].appId;
let border = bots.Result[index].itemsToSend[i].border;
let checkBox = document.getElementById(`astm_${appId}_${border}`);
if (checkBox.checked) {
if (you != "") {
you += ";";
}
you = you + getClasses(bots.Result[index].itemsToSend[i]);
if (them != "") {
them += ";";
}
them = them + getClasses(bots.Result[index].itemsToReceive[i]);
}
}
splitUrl[3] = "them=" + them;
splitUrl[4] = "you=" + you;
tradeLink.href = splitUrl.join("&");
}
function checkRow(row) {
debugPrint("checkRow");
let matches = row.getElementsByClassName("badge_row");
let visible = false;
for (let i = 0; i < matches.length; i++) {
if (matches[i].parentElement.style.display != "none") {
visible = true;
break;
}
}
if (visible) {
row.style.display = "block";
updateTrade(row);
} else {
row.style.display = "none";
}
}
function addMatchRow(index) {
debugPrint("addMatchRow " + index);
let itemsToSend = bots.Result[index].itemsToSend;
let itemsToReceive = bots.Result[index].itemsToReceive;
// sort by game name
function compareNames(a, b) {
const nameA = a.title;
const nameB = b.title;
return nameA.localeCompare(nameB);
}
itemsToSend.sort(compareNames);
itemsToReceive.sort(compareNames);
let tradeUrl = "https://steamcommunity.com/tradeoffer/new/?partner=" + getPartner(bots.Result[index].SteamIDText) + "&token=" + bots.Result[index].TradeToken + "&source=stm";
let globalYou = "";
let globalThem = "";
let matches = "";
let any = "";
if (bots.Result[index].MatchEverything) {
any = `&nbsp;<sup><span class="avatar_block_status_in-game" style="font-size: 8px; cursor:help" title="This bots trades for any cards within same set">&nbsp;ANY&nbsp;</span></sup>`;
}
for (let i = 0; i < itemsToSend.length; i++) {
let appId = itemsToSend[i].appId;
let itemToReceive = itemsToReceive.find((a) => a.appId == appId);
let gameName = itemsToSend[i].title;
let display = "inline-block";
let badgeBorder = itemsToSend[i].border ? "?border=1" : "";
//remove placeholder
let filterWidget = document.getElementById("asf_stm_filters_body");
let placeholder = document.getElementById("asf_stm_placeholder");
if (placeholder != null) {
placeholder.parentNode.removeChild(placeholder);
}
//add filter
let checkBox = document.getElementById(`astm_${appId}_${itemsToSend[i].border}`);
if (checkBox == null) {
let newFilter = `<span style="margin-right: 20px; white-space: nowrap; display: inline-block;"><input type="checkbox" id="astm_${appId}_${itemsToSend[i].border}" checked="" /><label for="astm_${appId}_${itemsToSend[i].border}">${gameName}</label></span>`;
let spanTemplate = document.createElement("template");
spanTemplate.innerHTML = newFilter.trim();
filterWidget.appendChild(spanTemplate.content.firstChild);
} else {
if (!checkBox.checked) {
display = "none";
}
}
let sendResult = populateCards(itemsToSend[i]);
let receiveResult = populateCards(itemToReceive);
let tradeUrlApp = tradeUrl + "&them=" + receiveResult.classList + "&you=" + sendResult.classList;
let matchTemplate = `
<div class="asf_stm_appid_${appId}_${itemsToSend[i].border}" style="display:${display}">
<div class="badge_row is_link goo_untradable_note showcase_slot">
<div class="notLoggedInText">
<img style="background-color: var(--gpStoreDarkerGrey);" height=69 alt="${gameName}" src="https://steamcdn-a.akamaihd.net/steam/apps/${appId}/capsule_184x69.jpg"
onerror="this.onerror=null;this.src='https://store.akamai.steamstatic.com/public/images/gift/steam_logo_digitalgiftcard.png'">
<div>
<div title="View badge progress for this game">
<a target="_blank" rel="noopener noreferrer" href="https://steamcommunity.com/${myProfileLink}/gamecards/${appId}/${badgeBorder}">${gameName}</a>
</div>
</div>
<a href="${tradeUrlApp}" target="_blank" rel="noopener noreferrer">
<div class="btn_darkblue_white_innerfade btn_medium">
<span>Offer a trade</span>
</div>
</a>
<a class="btn_darkred_white_innerfade asf_stm_trashbusket_button" data-appid="${appId}" data-border="${itemsToSend[i].border}" title="Remove match">&#x1F5D1;</a>
</div>
<div class="showcase_slot">
<div class="showcase_slot profile_header">
<div class="badge_info_unlocked profile_xp_block_mid avatar_block_status_in-game badge_info_title badge_row_overlay" style="height: 15px;">You</div>
${sendResult.htmlCards}
</div>
<span class="showcase_slot badge_info_title booster_creator_actions">
<h1>&#10145;</h1>
</span>
</div>
<div class="showcase_slot profile_header">
<div class="badge_info_unlocked profile_xp_block_mid avatar_block_status_online badge_info_title badge_row_overlay ellipsis" style="height: 15px;">
${bots.Result[index].Nickname}
</div>
${receiveResult.htmlCards}
</div>
</div>
</div>
`;
if (checkBox == null || checkBox.checked) {
matches += matchTemplate;
if (globalYou != "") {
globalYou += ";";
}
globalYou += sendResult.classList;
if (globalThem != "") {
globalThem += ";";
}
globalThem += receiveResult.classList;
}
}
// Sort filters
let filtersContainer = document.querySelector("#asf_stm_filters_body");
let items = Array.from(filtersContainer.querySelectorAll("span"));
items.sort((nodeA, nodeB) => nodeA.textContent.localeCompare(nodeB.textContent));
filtersContainer.append(...items);
let tradeUrlFull = tradeUrl + "&them=" + globalThem + "&you=" + globalYou;
let rowTemplate = `
<div id="asfstmbot_${index}" class="badge_row">
<div class="badge_row_inner">
<div class="badge_title_row guide_showcase_contributors">
<div class="badge_title_stats">
<a class="full_trade_url" href="${tradeUrlFull}" target="_blank" rel="noopener noreferrer" >
<div class="btn_darkblue_white_innerfade btn_medium">
<span>Offer a trade for all</span>
</div>
</a>
<a class="btn_darkred_white_innerfade asf_stm_trashbusket_button" data-asfstmbot="1" title="Remove match">&#x1F5D1;</a>
</div>
<div style="float: left;" class="">
<div class="user_avatar playerAvatar online">
<a target="_blank" rel="noopener noreferrer" href="https://steamcommunity.com/profiles/${bots.Result[index].SteamIDText}">
<img src="https://avatars.cloudflare.steamstatic.com/${bots.Result[index].AvatarHash || "fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb"}.jpg" />
</a>
</div>
</div>
<div class="badge_title">
&nbsp;<a target="_blank" rel="noopener noreferrer" href="https://steamcommunity.com/profiles/${bots.Result[index].SteamIDText}">${bots.Result[index].Nickname}</a>${any}
&ensp;<span style="color: #8F98A0;">(${bots.Result[index].TotalInventoryCount} items)</span>
</div>
</div>
<div class="badge_title_rule"></div>
${matches}
</div>
</div>
`;
let template = document.createElement("template");
template.innerHTML = rowTemplate.trim();
let mainContentDiv = document.getElementsByClassName("maincontent")[0];
let newChild = template.content.firstChild;
mainContentDiv.appendChild(newChild);
checkRow(newChild);
newChild.querySelectorAll(".asf_stm_trashbusket_button").forEach((trashButton) => {
trashButton.addEventListener("click", function() {
if (this.dataset.asfstmbot) {
newChild.parentNode.removeChild(newChild);
} else if (this.dataset.appid) {
let idx = itemsToSend.findIndex((game) => game.appId == this.dataset.appid && game.border == this.dataset.border);
if (idx >= 0) {
let gameMatchElement = newChild.querySelector(`.asf_stm_appid_${this.dataset.appid}_${this.dataset.border}`);
gameMatchElement.parentNode.removeChild(gameMatchElement);
itemsToSend.splice(idx, 1);
itemsToReceive.splice(idx, 1);
checkRow(newChild);
}
}
});
});
}
function calcState(badge) {
//state 0 - less than max sets; state 1 - we have max sets, even out the rest, state 2 - all even
if (badge.cards[badge.maxCards - 1].count == badge.maxSets) {
if (badge.cards[0].count == badge.lastSet) {
return 2; //nothing to do
} else {
return 1; //max sets are here, but we can distribute cards further
}
} else {
return 0; //less than max sets
}
}
function compareCards(index, callback) {
let itemsToSend = [];
let itemsToReceive = [];
//debugPrint("bot's cards");
//debugPrint(JSON.stringify(botBadges));
//debugPrint("our cards");
//debugPrint(JSON.stringify(myBadges));
for (let i = 0; i < botBadges.length; i++) {
let myBadge = deepClone(myBadges[i]);
let theirBadge = deepClone(botBadges[i]);
let myState = calcState(myBadge);
debugPrint("state=" + myState);
debugPrint("myapp=" + myBadge.appId + " botapp=" + theirBadge.appId);
while (myState < 2) {
let foundMatch = false;
for (let j = 0; j < theirBadge.maxCards; j++) {
//index of card they give
if (theirBadge.cards[j].count > 0) {
//try to match
let myInd = myBadge.cards.findIndex((a) => a.item == theirBadge.cards[j].item); //index of slot where we receive card
if ((myState == 0 && myBadge.cards[myInd].count < myBadge.maxSets) || (myState == 1 && myBadge.cards[myInd].count < myBadge.lastSet)) {
//we need this ^Kfor the Emperor
debugPrint("we need this: " + theirBadge.cards[j].item + " (" + theirBadge.cards[j].count + ")");
//find a card to match.
for (let k = 0; k < myInd; k++) {
//index of card we give
debugPrint("i=" + i + " j=" + j + " k=" + k + " myState=" + myState);
debugPrint("we have this: " + myBadge.cards[k].item + " (" + myBadge.cards[k].count + ")");
if ((myState == 0 && myBadge.cards[k].count > myBadge.maxSets) || (myState == 1 && myBadge.cards[k].count > myBadge.lastSet)) {
//that's fine for us
debugPrint("it's a good trade for us");
let theirInd = theirBadge.cards.findIndex((a) => a.item == myBadge.cards[k].item); //index of slot where they will receive card
if (!bots.Result[index].MatchEverything) {
//make sure it's neutral+ for them
if (theirBadge.cards[theirInd].count >= theirBadge.cards[j].count) {
debugPrint("Not fair for them");
debugPrint("they have this: " + theirBadge.cards[theirInd].item + " (" + theirBadge.cards[theirInd].count + ")");
continue; //it's not neutral+, check other options
}
}
debugPrint("it's a match!");
let itemToSend = {
item: myBadge.cards[k].item,
count: 1,
class: classIdsDB[myBadge.appId][myBadge.cards[k].item],
iconUrl: myBadge.cards[k].iconUrl,
};
let itemToReceive = {
item: theirBadge.cards[j].item,
count: 1,
class: classIdsDB[theirBadge.appId][theirBadge.cards[j].item],
iconUrl: theirBadge.cards[j].iconUrl,
};
//fill items to send
let sendMatch = itemsToSend.find((item) => item.appId == myBadge.appId);
if (sendMatch == undefined) {
let newMatch = {
appId: myBadge.appId,
title: myBadge.title,
cards: [itemToSend],
border: myBadge.border,
};
itemsToSend.push(newMatch);
} else {
let existingCard = sendMatch.cards.find((a) => a.item == itemToSend.item);
if (existingCard == undefined) {
sendMatch.cards.push(itemToSend);
} else {
existingCard.count += 1;
}
}
//add this item to their inventory
theirBadge.cards[theirInd].count += 1;
//remove this item from our inventory
myBadge.cards[k].count -= 1;
//fill items to receive
let receiveMatch = itemsToReceive.find((item) => item.appId == myBadge.appId);
if (receiveMatch == undefined) {
let newMatch = {
appId: myBadge.appId,
title: myBadge.title,
cards: [itemToReceive],
border: myBadge.border,
};
itemsToReceive.push(newMatch);
} else {
let existingCard = receiveMatch.cards.find((a) => a.item == itemToReceive.item);
if (existingCard == undefined) {
receiveMatch.cards.push(itemToReceive);
} else {
existingCard.count += 1;
}
}
//add this item to our inventory
myBadge.cards[myInd].count += 1;
//remove this item from their inventory
theirBadge.cards[j].count -= 1;
foundMatch = true;
break; //found a match!
}
}
if (foundMatch) {
//if we found something - we need to sort cards again and start over.
myBadge.cards.sort((a, b) => b.count - a.count);
myState = calcState(myBadge);
debugPrint("new myState=" + myState);
}
}
}
}
if (!foundMatch) {
break; //found no matches - move to next badge
}
theirBadge.cards.sort((a, b) => b.count - a.count);
}
}
//debugPrint("items to send");
//debugPrint(JSON.stringify(itemsToSend));
//debugPrint("items to receive");
//debugPrint(JSON.stringify(itemsToReceive));
bots.Result[index].itemsToSend = itemsToSend;
bots.Result[index].itemsToReceive = itemsToReceive;
if (itemsToSend.length > 0) {
addMatchRow(index);
} else {
debugPrint("no matches");
}
callback();
}
function GetCards(badgeIndex, userindex) {
debugPrint("GetCards " + badgeIndex + " : " + userindex);
if (userindex >= bots.Result.length) {
debugPrint("finished");
debugPrint(new Date(Date.now()));
hideThrobber();
hideMessage();
updateProgress(1, 1);
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
if (badgeIndex == 0) {
//botBadges.length = 0;
botBadges = deepClone(myBadges);
for (let i = 0; i < botBadges.length; i++) {
botBadges[i].cards.forEach((card) => {
card.count = 0;
});
}
}
if (badgeIndex < botBadges.length) {
let xhr = new XMLHttpRequest(); // ToDo: GM_xmlhttpRequest
if (userindex == -1) {
updateMessage("Getting our data for badge " + (badgeIndex + 1) + " of " + botBadges.length);
updateProgress(badgeIndex, botBadges.length);
let url = "https://steamcommunity.com/" + myProfileLink + "/ajaxgetbadgeinfo/" + botBadges[badgeIndex].appId + "?l=english" + (botBadges[badgeIndex].border ? "&border=1" : "");
xhr.open("GET", url, true);
xhr.responseType = "json";
xhr.onload = function () {
if (stop) {
updateMessage("Interrupted by user");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
let status = xhr.status;
if (status === 200 && xhr.response.eresult === 1) {
debugPrint("processing badge " + botBadges[badgeIndex].appId);
let badgeCards = xhr.response.badgedata;
if (!!badgeCards && !!badgeCards.rgCards && badgeCards.rgCards.length >= 5) {
errors = 0;
botBadges[badgeIndex].maxCards = badgeCards.rgCards.length;
badgeCards.rgCards.forEach(function (card, pos) {
let newcard = {
position: pos,
item: card.markethash, // card.name
count: card.owned,
iconUrl: card.imgurl,
};
//debugPrint(JSON.stringify(newcard));
botBadges[badgeIndex].cards.push(newcard);
});
}
badgeIndex++;
setTimeout(GetCards, weblimiter, badgeIndex, userindex);
return;
} else {
errors++;
}
if ((status < 400 || status >= 500) && errors <= maxErrors) {
setTimeout(GetCards, weblimiter + errorLimiter * errors, badgeIndex, userindex);
} else {
if (status != 200) {
updateMessage("Error getting badge data, ERROR " + status);
} else {
//ToDo: should never happen
updateMessage("Error getting badge data, malformed JSON");
}
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
};
} else {
updateMessage("Fetching bot " + (userindex + 1).toString() + " of " + bots.Result.length.toString() + " (badge " + (badgeIndex + 1) + " of " + botBadges.length + ")");
updateProgress(userindex, bots.Result.length);
if (botBadges[badgeIndex].border == 1 && !bots.Result[userindex].MatchableTypes.includes(EType.TradingCardFoil) ||
botBadges[badgeIndex].border == 0 && !bots.Result[userindex].MatchableTypes.includes(EType.TradingCard)
) {
setTimeout(GetCards, weblimiter, badgeIndex + 1, userindex);
return;
}
let url = "https://steamcommunity.com/profiles/" + bots.Result[userindex].SteamIDText + "/gamecards/" + botBadges[badgeIndex].appId + "?l=english" + (botBadges[badgeIndex].border ? "&border=1" : "");
xhr.open("GET", url, true);
xhr.responseType = "document";
xhr.onload = function () {
if (stop) {
compareCards(userindex, function () {/* noop */});
updateMessage("Interrupted by user");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
let status = xhr.status;
if (status === 200) {
debugPrint("processing badge " + botBadges[badgeIndex].appId);
let badgeCards = xhr.response.documentElement.querySelectorAll(".badge_card_set_card");
if (badgeCards.length >= 5) {
errors = 0;
badgeCards.forEach(function (cardElem, pos) {
let quantityElement = cardElem.querySelector(".badge_card_set_text_qty");
let quantity = quantityElement == null ? 0 : Number(quantityElement.innerText.trim().slice(1, -1));
botBadges[badgeIndex].cards.find((card) => card.position == pos).count = quantity;
});
setTimeout(GetCards, weblimiter, badgeIndex + 1, userindex);
return;
} else {
//if can't find any cards on badge page - retry, that's must be a bug.
//debugPrint(xhr.response.documentElement.outerHTML);
errors++;
}
} else {
errors++;
}
if ((status < 400 || status >= 500) && errors <= maxErrors) {
setTimeout(GetCards, weblimiter + errorLimiter * errors, badgeIndex, userindex);
} else {
if (status != 200) {
updateMessage("Error getting badge data, ERROR " + status);
} else {
//ToDo: should never happen
updateMessage("Error getting badge data, malformed HTML");
}
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
};
}
xhr.onerror = function () {
if (stop) {
compareCards(userindex, function () {/* noop */});
updateMessage("Interrupted by user");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
errors++;
if (errors <= maxErrors) {
setTimeout(GetCards, weblimiter + errorLimiter * errors, badgeIndex, userindex);
return;
} else {
debugPrint("error");
updateMessage("Error getting badge data");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
};
xhr.send();
return; //do this synchronously to avoid rate limit
}
debugPrint("populated");
debugTime("Filter and sort");
// ToDo: Array.filter
for (let i = botBadges.length - 1; i >= 0; i--) {
debugPrint("badge " + i + " " + JSON.stringify(botBadges[i]));
botBadges[i].cards.sort((a, b) => b.count - a.count);
if (userindex < 0) {
if (botBadges[i].cards[0].count - botBadges[i].cards[botBadges[i].cards.length - 1].count < 2) {
//nothing to match, remove from list.
botBadges.splice(i, 1);
continue;
}
}
let totalCards = 0;
for (let j = 0; j < botBadges[i].maxCards; j++) {
totalCards += botBadges[i].cards[j].count;
}
botBadges[i].maxSets = Math.floor(totalCards / botBadges[i].maxCards);
botBadges[i].lastSet = Math.ceil(totalCards / botBadges[i].maxCards);
debugPrint("totalCards=" + totalCards + " maxSets=" + botBadges[i].maxSets + " lastSet=" + botBadges[i].lastSet);
}
debugTimeEnd("Filter and sort");
if (userindex < 0) {
if (botBadges.length == 0) {
hideThrobber();
updateMessage("No cards to match");
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
} else {
myBadges = deepClone(botBadges);
getClassIDs(0);
return;
}
} else {
debugPrint(bots.Result[userindex].SteamIDText);
compareCards(userindex, function () {
setTimeout(GetCards, weblimiter, 0, userindex + 1);
});
}
}
function getBadges(page) {
let url = "https://steamcommunity.com/" + myProfileLink + "/badges?p=" + page + "&l=english";
let xhr = new XMLHttpRequest(); // ToDo: GM_xmlhttpRequest
xhr.open("GET", url, true);
xhr.responseType = "document";
xhr.onload = function () {
if (stop) {
updateMessage("Interrupted by user");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
let status = xhr.status;
if (status === 200) {
errors = 0;
debugPrint("processing page " + page);
updateMessage("Processing badges page " + page);
if (page === 1) {
let pageLinks = xhr.response.documentElement.getElementsByClassName("pagelink");
if (pageLinks.length > 0) {
maxPages = Number(pageLinks[pageLinks.length - 1].textContent.trim());
}
if (maxPagesLimit && !appIdsToScanOverride.length) {
maxPages = Math.min(maxPages, maxPagesLimit);
}
}
updateProgress(page - 1, maxPages); // substract 1 from page number as it starts from 1
let badges = xhr.response.documentElement.getElementsByClassName("badge_row_inner");
for (let i = 0; i < badges.length; i++) {
//if (badges[i].getElementsByClassName("owned").length > 0) {
// not max level with cards OR max level AND has cards
if (!!badges[i].querySelector(".badge_progress_tasks .owned") || !badges[i].querySelector(".badge_progress_tasks .unowned") && !!badges[i].querySelector(".badge_progress_info")) { // .progress_info_bold
let overlayElement = badges[i].parentElement.querySelector(".badge_row_overlay");
let appId = parseInt(overlayElement.href.match(/\/gamecards\/(\d+)/)[1], 10);
if (!!appIdsToScanOverride.length && !appIdsToScanOverride.includes(appId)) {
continue; // skip appid - not what we looking for
}
let badgeBorder = overlayElement.href.endsWith("border=1") ? 1 : 0;
let title = badges[i].querySelector(".badge_title").childNodes[0].textContent.trim().replaceAll(/\s+/g, ' ');
let badgeStub = {
appId: appId,
title: title,
border: badgeBorder,
maxCards: 0,
maxSets: 0,
lastSet: 0,
cards: [],
};
myBadges.push(badgeStub);
}
}
page++;
} else {
errors++;
}
if ((status < 400 || status >= 500) && errors <= maxErrors) {
if (page <= maxPages) {
setTimeout(getBadges, weblimiter + errorLimiter * errors, page);
} else {
debugPrint("all badge pages processed");
debugPrint(weblimiter + errorLimiter * errors);
if (myBadges.length === 0) {
hideThrobber();
updateMessage("No cards to match");
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
} else {
setTimeout(GetCards, weblimiter + errorLimiter * errors, 0, -1);
}
}
} else {
if (status != 200) {
updateMessage("Error getting badge page, ERROR " + status);
} else {
updateMessage("Error getting badge page, malformed HTML");
}
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
};
xhr.onerror = function () {
if (stop) {
updateMessage("Interrupted by user");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
errors++;
if (errors <= maxErrors) {
setTimeout(getBadges, weblimiter + errorLimiter * errors, page);
} else {
debugPrint("error getting badge page");
updateMessage("Error getting badge page");
hideThrobber();
enableButton();
let stopButton = document.getElementById("asf_stm_stop");
stopButton.parentNode.removeChild(stopButton);
return;
}
};
xhr.send();
}
function filterEventHandler(event) {
let [, appId, border] = event.target.id.split("_");
let matches = document.getElementsByClassName(`asf_stm_appid_${appId}_${border}`);
for (let i = 0; i < matches.length; i++) {
matches[i].style.display = event.target.checked ? "inline-block" : "none";
checkRow(matches[i].parentElement.parentElement);
}
}
function filterSwitchesHandler(event) {
let action = event.target.id.split("_")[3];
let filterWidget = document.getElementById("asf_stm_filters_body");
let checkboxes = filterWidget.getElementsByTagName("input");
for (let i = 0; i < checkboxes.length; i++) {
if (action === "all") {
if (!checkboxes[i].checked) {
checkboxes[i].checked = true;
filterEventHandler({ target: checkboxes[i] });
}
} else if (action === "none") {
if (checkboxes[i].checked) {
checkboxes[i].checked = false;
filterEventHandler({ target: checkboxes[i] });
}
} else if (action === "invert") {
checkboxes[i].checked = !checkboxes[i].checked;
filterEventHandler({ target: checkboxes[i] });
}
}
}
function filtersButtonEvent() {
let filterWidget = document.getElementById("asf_stm_filters");
if (filterWidget.style.marginRight == "-50%") {
filterWidget.style.marginRight = "unset";
} else {
filterWidget.style.marginRight = "-50%";
}
}
function stopButtonEvent() {
let stopButton = document.getElementById("asf_stm_stop");
stopButton.removeEventListener("click", stopButtonEvent, false);
stopButton.title = "Stopping...";
stopButton.classList.add("btn_disabled");
updateMessage("Stopping...");
stop = true;
}
function buttonPressedEvent() {
if (bots === null || bots.Result === undefined || bots.Result.length == 0 || bots.Success != true || bots.cacheTime + botCacheTime < Date.now()) {
debugPrint("Bot cache invalidated");
fetchBots();
return;
}
disableButton();
debugPrint(new Date(Date.now()));
let mainContentDiv = document.getElementsByClassName("maincontent")[0];
mainContentDiv.textContent = "";
mainContentDiv.style.width = "90%";
mainContentDiv.innerHTML = `
<div class="profile_badges_header">
<div id="throbber">
<div class="LoadingWrapper">
<div class="LoadingThrobber">
<div class="Bar Bar1"></div>
<div class="Bar Bar2"></div>
<div class="Bar Bar3"></div>
</div>
</div>
</div>
<div>
<div id="asf_stm_messagebox" class="profile_badges_header">
<div id="asf_stm_message" class="profile_badges_header_title" style="text-align: center;">Initialization</div>
</div>
</div>
<div style="width: 100%;">
<div id="asf_stm_stop" class="btn_darkred_white_innerfade btn_medium_thin" style="float: right;margin-top: -12px;margin-left: 10px;" title="Stop scan">
<span>🛑</span>
</div>
<div style="width: auto;overflow: hidden;" class="profile_xp_block_remaining_bar">
<div id="asf_stm_progress" class="profile_xp_block_remaining_bar_progress" style="width: 100%;transition: width 0.5s ease-in-out 0s">
</div>
</div>
</div>
</div>
<div id="asf_stm_filters" style="position: fixed; z-index: 1000; right: 5px; bottom: 45px; transition-duration: 500ms;
transition-timing-function: ease; margin-right: -50%; padding: 5px; max-width: 40%; display: inline-block; border-radius: 2px;
background:${filterBackgroundColor}; color: #67c1f5;">
<div style="white-space: nowrap;">Select:
<a id="asf_stm_filter_all" class="commentthread_pagelinks">
all
</a>
<a id="asf_stm_filter_none" class="commentthread_pagelinks">
none
</a>
<a id="asf_stm_filter_invert" class="commentthread_pagelinks">
invert
</a>
</div>
<hr />
<div id="asf_stm_filters_body">
<span id="asf_stm_placeholder" style="margin-right: 15px;">No matches to filter</span>
</div>
</div>
<div style="position: fixed;z-index: 1000;right: 5px;bottom: 5px;" id="asf_stm_filters_button_div">
<a id="asf_stm_filters_button" class="btnv6_blue_hoverfade btn_medium">
<span>Filters</span>
</a>
</div>
`;
document.getElementById("asf_stm_stop").addEventListener("click", stopButtonEvent, false);
document.getElementById("asf_stm_filters_body").addEventListener("change", filterEventHandler);
document.getElementById("asf_stm_filter_all").addEventListener("click", filterSwitchesHandler);
document.getElementById("asf_stm_filter_none").addEventListener("click", filterSwitchesHandler);
document.getElementById("asf_stm_filter_invert").addEventListener("click", filterSwitchesHandler);
document.getElementById("asf_stm_filters_button").addEventListener("click", filtersButtonEvent, false);
stop = false;
myBadges.length = 0;
getBadges(1);
}
function fetchBots() {
let requestUrl = "https://asf.justarchi.net/Api/Listing/Bots";
let requestFunc;
if (typeof GM_xmlhttpRequest !== "function") {
requestFunc = GM.xmlHttpRequest.bind(GM);
} else {
requestFunc = GM_xmlhttpRequest;
}
requestFunc({ // ToDo: .responseType = "json";
method: "GET",
url: requestUrl,
onload: function (response) {
if (response.status != 200) {
disableButton();
document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots");
debugPrint("can't fetch list of bots, ERROR=" + response.status);
//debugPrint(JSON.stringify(response));
return;
}
try {
bots = JSON.parse(response.response);
bots.cacheTime = Date.now();
if (bots.Success) {
console.log("total bots:", bots.Result.length);
bots.Result = bots.Result.filter(function (bot) {
return bot.MatchableTypes.includes(EType.TradingCardFoil) ||
bot.MatchableTypes.includes(EType.TradingCard);
});
console.log("filtered bots accept cards:", bots.Result.length);
if (bRandomizeBots) {
//Fisher–Yates shuffle
let currentIndex = bots.Result.length, randomIndex;
// While there remain elements to shuffle…
while (currentIndex) {
// Pick a remaining element…
randomIndex = Math.floor(Math.random() * currentIndex--);
// And swap it with the current element.
[bots.Result[currentIndex], bots.Result[randomIndex]] = [bots.Result[randomIndex], bots.Result[currentIndex]];
}
}
if (maxBotsToScan > 0) {
bots.Result = bots.Result.slice(0, maxBotsToScan);
}
if (!bRandomizeBots) {
bots.Result.sort(function (a, b) {
//sort received array as I like it. TODO: sort according to settings
let result = b.MatchEverything - a.MatchEverything; //bots with MatchEverything go first
if (result === 0) {
result = b.TotalGamesCount - a.TotalGamesCount; //then by TotalGamesCount descending
}
if (result === 0) {
result = b.TotalItemsCount - a.TotalItemsCount; //then by TotalItemsCounts descending
}
if (result === 0) {
result = a.TotalInventoryCount - b.TotalInventoryCount; //then by TotalInventoryCount ascending
}
return result;
});
}
debugPrint("found total " + bots.Result.length + " bots");
localStorage.setItem("Ryzhehvost.ASF.STM.BotCache", JSON.stringify(bots));
buttonPressedEvent();
} else {
//ASF backend does not indicate success
disableButton();
document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots, try later");
debugPrint("can't fetch list of bots");
debugPrint(bots.Message);
//debugPrint(JSON.stringify(response));
return;
}
return;
} catch (e) {
disableButton();
document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots, try later");
debugPrint("can't fetch list of bots");
debugPrint(e);
//debugPrint(JSON.stringify(response));
return;
}
},
onerror: function (response) {
disableButton();
document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots");
debugPrint("can't fetch list of bots");
//debugPrint(JSON.stringify(response));
},
onabort: function (response) {
disableButton();
document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots");
debugPrint("can't fetch list of bots - aborted");
//debugPrint(JSON.stringify(response));
},
ontimeout: function (response) {
disableButton();
document.getElementById("asf_stm_button_div").setAttribute("title", "Can't fetch list of bots");
debugPrint("can't fetch list of bots - timeout");
//debugPrint(JSON.stringify(response));
},
});
}
if (document.getElementsByClassName("badge_details_set_favorite").length != 0) {
let profileRegex = /http[s]?:\/\/steamcommunity.com\/(.*)\/badges.*/g;
let result = profileRegex.exec(document.location);
if (result) {
myProfileLink = result[1];
} else {
//should never happen, but whatever.
myProfileLink = "my";
}
//debugPrint(profileRegex);
let botCache = JSON.parse(localStorage.getItem("Ryzhehvost.ASF.STM.BotCache"));
if (botCache === null || botCache.cacheTime === undefined || botCache.cacheTime === null || botCache.cacheTime + botCacheTime < Date.now()) {
botCache = null;
debugPrint("Bot cache invalidated");
} else {
bots = botCache;
}
let buttonDiv = document.createElement("div");
buttonDiv.setAttribute("class", "profile_small_header_additional");
buttonDiv.setAttribute("style", "margin-top: 40px;");
buttonDiv.setAttribute("id", "asf_stm_button_div");
buttonDiv.setAttribute("title", "Scan ASF STM");
let button = document.createElement("a");
button.setAttribute("class", "btnv6_blue_hoverfade btn_medium");
button.setAttribute("id", "asf_stm_button");
button.appendChild(document.createElement("span"));
button.firstChild.appendChild(document.createTextNode("Scan ASF STM"));
buttonDiv.appendChild(button);
let anchor = document.getElementsByClassName("profile_small_header_texture")[0];
anchor.appendChild(buttonDiv);
enableButton();
// add our styles to the document's style sheet
if (typeof GM_addStyle != "undefined") {
GM_addStyle(css);
} else {
const node = document.createElement("style");
node.appendChild(document.createTextNode(css));
if (document.head) {
document.head.appendChild(node);
} else {
// no head yet, stick it whereever
document.documentElement.appendChild(node);
}
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment