// ==UserScript==
// @name ASF STM
// @language English
// @namespace https://gf.qytechs.cn/users/2205
// @description ASF bot list trade matcher
// @include http*://steamcommunity.com/id/*/badges
// @include http*://steamcommunity.com/id/*/badges/
// @include http*://steamcommunity.com/profiles/*/badges
// @include http*://steamcommunity.com/profiles/*/badges/
// @version 0.3
// @connect asf.justarchi.net
// @grant GM.xmlhttpRequest
// @grant GM_xmlhttpRequest
// ==/UserScript==
/* global g_steamID */
(function()
{
const limiter = 500;
const debug = false;
const maxErrors = 5;
let errors=0;
let bots;
let assets=[];
let descriptions=[];
let mybadges=[];
let userbadges=[];
let maxpages;
function debugProfile(name){
if (debug) {
console.time(name);
}
}
function debugProfileEnd(name){
if (debug) {
console.timeEnd(name);
}
}
function debugPrint(msg){
if (debug) {
console.log(msg);
}
}
function getPartner(str) {
return (BigInt(str)%4294967296n).toString();
}
function EnableButton(){
let buttondiv=document.getElementById("asf_stm_button_div");
buttondiv.setAttribute("class","profile_small_header_additional");
let button=document.getElementById("asf_stm_button");
button.addEventListener("click",ButtonPressed, false);
}
function DisableButton(){
let buttondiv=document.getElementById("asf_stm_button_div");
buttondiv.setAttribute("class","profile_small_header_additional btn_disabled");
let button=document.getElementById("asf_stm_button");
button.removeEventListener("click",ButtonPressed, 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){
let bar=document.getElementById("asf_stm_progress");
let progress = (100*((index+1)/bots.length));
bar.setAttribute("style","width: "+progress+"%;");
}
function populateCards(item) {
let classlist = "";
let htmlCards = ""
for (let j=0;j<item.cards.length; j++) {
let item_icon = item.cards[j].icon_url;
let item_name = 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+=item.cards[j].class;
let cardTemplate =`
<div class="showcase_slot">
<img class="image-container" src="https://steamcommunity-a.akamaihd.net/economy/image/${item_icon}/98x115">
<div class="commentthread_subscribe_hint" style="width: 98px;">${item_name}</div>
</div>
`;
htmlCards+=cardTemplate;
}
}
return {"htmlCards":htmlCards,"classlist": classlist};
}
function AddMatchRow(itemsToSend,itemsToReceive,botname,index){
let tradeurl="https://steamcommunity.com/tradeoffer/new/?partner="+getPartner(bots[index].steam_id)+"&token="+bots[index].trade_token+"&source=stm";
let globalyou="";
let globalthem="";
let matches = "";
if (bots[index].match_everything==1) {
botname = botname+` <sup><span class="avatar_block_status_in-game" style="font-size: 8px; cursor:help" title="This bots trades for any cards within same set"> ANY </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 sendResult = populateCards(itemsToSend[i]);
let receiveResult = populateCards(itemToReceive);
let tradeurlappid=tradeurl+"&them="+receiveResult.classlist+"&you="+sendResult.classlist;
let matchTemplate = `
<div class="badge_sort_option">
<div class="badge_row is_link goo_untradable_note showcase_slot">
<div class="notLoggedInText">
<img alt="${gameName}" src="https://steamcdn-a.akamaihd.net/steam/apps/${appid}/capsule_184x69.jpg">
<div>
<div title="View badge progress for this game">
<a target="_blank" href="https://steamcommunity.com/my/gamecards/${appid}/">${gameName}</a>
</div>
</div>
<div class="btn_darkblue_white_innerfade btn_medium">
<span>
<a href="${tradeurlappid}" target="_blank" rel="noopener">Offer a trade</a>
</span>
</div>
</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>➡</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;">
${botname}
</div>
${receiveResult.htmlCards}
</div>
</div>
</div>
`;
matches+=matchTemplate;
if (globalyou!=""){
globalyou+=";";
};
globalyou+=sendResult.classlist;
if (globalthem!=""){
globalthem+=";";
};
globalthem+=receiveResult.classlist
}
let tradeurlfull = tradeurl+"&them="+globalthem+"&you="+globalyou;
let rowTemplate = `
<div class="badge_row">
<div class="badge_row_inner">
<div class="badge_title_row guide_showcase_contributors">
<div class="badge_title_stats">
<div class="btn_darkblue_white_innerfade btn_medium">
<span>
<a href="${tradeurlfull}" target="_blank" rel="noopener" >Offer a trade for all</a>
</span>
</div>
</div>
<div class="badge_title">
${botname}
</div>
</div>
<div class="badge_title_rule"></div>
${matches}
</div>
</div>
`;
let template = document.createElement('template');
template.innerHTML = rowTemplate.trim();
let maincontent = document.getElementsByClassName("maincontent")[0];
maincontent.appendChild(template.content.firstChild);
}
function DeepClone(object) {
return JSON.parse(JSON.stringify(object));
}
function fetchinventory(steamid,startasset,callback) {
let url="https://steamcommunity.com/inventory/"+steamid+"/753/6?l=english&count=5000&l=english";
if (startasset>0) {
url=url+"&start_assetid="+startasset.toString();
} else {
assets.clear();
descriptions.clear();
}
debugPrint(url);
let xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'json';
xhr.onload = function() {
debugPrint("...");
let status = xhr.status;
let lastasset=startasset;
if (status === 200) {
errors = 0;
if (typeof(xhr.response) !== 'undefined') {
if (typeof(xhr.response.descriptions) !== 'undefined') {
assets=assets.concat(xhr.response.assets);
descriptions=descriptions.concat(xhr.response.descriptions);
if (typeof(xhr.response.last_assetid) == 'undefined') { //end of inventory
debugPrint("total_inventory_count = "+xhr.response.total_inventory_count);
callback();
return;
} else {
lastasset=xhr.response.last_assetid;
}
}
}
} else {
errors++;
debugPrint("HTTP Error="+status);
}
if (status==403) {
assets.clear(); //switch to next bot
console.log(403);
callback();
} else if ((status<400 || status>=500 || status==408)&&(errors <= maxErrors)) {
setTimeout((function(steamid,startasset,callback) { return function(){ fetchinventory(steamid,startasset,callback); };})(steamid,lastasset,callback), limiter);
} else {
UpdateMessage("Error getting inventory, ERROR "+status);
HideThrobber();
EnableButton();
}
}
xhr.onerror = function() {
debugPrint("error getting inventory");
UpdateMessage("Error getting inventory");
HideThrobber();
EnableButton();
}
xhr.send();
}
function CalcState(badge) { //state 0 - less than max sets; state 1 - we have max sets, even out the rest, state 2 - all even
let state = 0;
if (badge.cards[0].count == badge.lastset) {
return 2; //nothing to do
} else if (badge.cards[badge.maxcards-1].count == badge.maxsets) {
return 1; //max sets are here, but we can distribute cards further
}
return 0; //less than max sets
}
function CompareCards(index,callback) {
let itemsToSend=[];
let itemsToReceive=[];
userbadges.clear();
userbadges = DeepClone(mybadges);
for (let i = 0; i < userbadges.length; i++) {
userbadges[i].cards.clear();
}
PopulateExistingCards(userbadges,false);
debugPrint("bot's cards");
debugPrint(DeepClone(userbadges));
debugPrint("our cards");
debugPrint(DeepClone(mybadges));
for(let i = 0; i < userbadges.length; i++) {
let mybadge = DeepClone(mybadges[i]);
let theirbadge = DeepClone(userbadges[i]);
let mystate=CalcState(mybadge);
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
//if (bots[index].match_everything) {}
let myInd = mybadge.cards.findIndex(a => a.item == theirbadge.cards[j].item); //index of slot where we receive card
if (myInd==-1) {
debugPrint("we don't have it");
let empty = mybadge.cards.find(card => card.item==null);
if (empty != undefined) {
debugPrint("found a place!");
empty.item=theirbadge.cards[j].item;
empty.icon_url=theirbadge.cards[j].icon_url;
myInd=mybadge.cards.indexOf(empty);
} else {
debugPrint("Error! We found more cards than expected");
debugPrint(DeepClone(mybadge.cards));
debugPrint(DeepClone(theirbadge.cards));
}
}
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 (theirInd == -1) { //they don't even know this card
theirInd = theirbadge.cards.findIndex(a => a.item == null); //index of empty space
//it's safe to assign item name to this card, they don't have it
theirbadge.cards[theirInd].item = mybadge.cards[k].item;
}
if (bots[index].match_everything==0) { //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":mybadge.cards[k].class, "icon_url":mybadge.cards[k].icon_url};
let itemToReceive = {"item":theirbadge.cards[j].item,"count":1, "class":theirbadge.cards[j].class, "icon_url":theirbadge.cards[j].icon_url};
//fill items to send
let sendmatch = itemsToSend.find(item => item.appid == mybadges[i].appid);
if (sendmatch == undefined) {
let newmatch = {"appid":mybadges[i].appid, "title":mybadge.title ,"cards":[ itemToSend ]};
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 == mybadges[i].appid);
if (receivematch == undefined) {
let newmatch = {"appid":mybadges[i].appid,"title":mybadge.title,"cards":[ itemToReceive ]};
itemsToReceive.push(newmatch);
} else {
let existingCard = sendmatch.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) {
break; //found no matches - move to next badge
}
mybadge.cards.sort((a,b)=>b.count-a.count);
theirbadge.cards.sort((a,b)=>b.count-a.count);
mystate=CalcState(mybadge);
}
}
debugPrint("items to send");
debugPrint(DeepClone(itemsToSend));
debugPrint("items to receive");
debugPrint(DeepClone(itemsToReceive));
if (itemsToSend.length>0){
let url="https://steamcommunity.com/profiles/"+bots[index].steam_id+"?xml=1";
let xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'text';
//xhr.setRequestHeader("Range","bytes=0-200"); //fuck it, get the whole page
xhr.onload = function() {
let status = xhr.status;
let username=bots[index].steam_id;
debugPrint("getting username");
if (status === 200) {
errors = 0;
let re = /<steamID><!\[CDATA\[(.+)]]><\/steamID>/g;
username = re.exec(xhr.response)[1];
debugPrint(username);
}
AddMatchRow(itemsToSend,itemsToReceive,username,index);
callback();
};
xhr.onerror = function() {
debugPrint("error");
UpdateMessage("Error getting username data");
HideThrobber();
EnableButton();
};
xhr.send();
} else {
debugPrint("no matches");
callback();
}
}
function checkuser(index){
debugPrint(index);
UpdateMessage("Fetching bot "+(index+1).toString()+" of "+bots.length.toString());
UpdateProgress(index);
fetchinventory(bots[index].steam_id,0,function() {
debugPrint(bots[index].steam_id);
debugPrint(assets.length);
CompareCards(index, function() {
if (index<bots.length-1) {
setTimeout((function(index) { return function(){ checkuser(index); };})(index+1), limiter);
} else {
debugPrint("finished");
if (debug) {
console.log(new Date(Date.now()));
}
HideThrobber();
HideMessage();
UpdateProgress(bots.length);
EnableButton();
}
});
});
}
function PopulateExistingCards(badges, filter){
debugProfile("PopulateExistingCards1");
debugPrint(DeepClone(assets));
debugPrint(DeepClone(descriptions));
descriptions=descriptions.filter(desc=>badges.find(item=>item.appid==desc.market_hash_name.split("-")[0])!=undefined);
assets=assets.filter(asset=>descriptions.find(item=>item.classid==asset.classid)!=undefined);
for (let i = 0; i < assets.length; i++){
debugPrint(".");
let descr = descriptions.find(desc=>desc.classid==assets[i].classid);
if (descr != undefined) {
let appid=descr.market_hash_name.split("-")[0];
let title=appid;
let game_tag=descr.tags.find(tag=>tag.category=="Game");
if (game_tag != undefined) {
title=game_tag.localized_tag_name;
}
let item_class_tag=descr.tags.find(tag=>tag.category=="item_class");
if (item_class_tag != undefined) {
if (item_class_tag.internal_name == "item_class_2") {
let cardborder_tag=descr.tags.find(tag=>tag.category=="cardborder");
if (cardborder_tag != undefined) {
if (cardborder_tag.internal_name == "cardborder_0") {
let badge = badges.find(badge=>badge.appid==appid);
if (badge != undefined) {
let card=badge.cards.find(card=>card.item==descr.market_hash_name);
if (card == undefined) {
let newcard = {"item":descr.market_hash_name, "count":1,"class": assets[i].classid ,"icon_url":descr.icon_url};
badge.cards.push(newcard);
badge.title = title;
} else {
card.count+=1;
}
}
}
}
}
}
}
}
debugProfileEnd("PopulateExistingCards1");
debugProfile("PopulateExistingCards2");
for (let i = badges.length-1; i >=0; i--) {
for (let j = badges[i].cards.length; j < badges[i].maxcards; j++){
badges[i].cards.push({"item":null, "count":0,"class":0, "icon_url":null}); //fill missing cards with dummy element
}
badges[i].cards.sort((a,b)=>b.count-a.count);
if (filter) {
if (badges[i].cards[0].count-badges[i].cards[badges[i].cards.length-1].count<2) {
//nothing to match, remove from list.
badges.splice(i,1);
continue;
}
}
let totalcards = 0;
for (let j = 0; j<badges[i].maxcards; j++) {
totalcards+=badges[i].cards[j].count;
}
badges[i].maxsets = Math.floor(totalcards/badges[i].maxcards);
badges[i].lastset = Math.ceil(totalcards/badges[i].maxcards);
}
debugProfileEnd("PopulateExistingCards2");
}
function PopulateMaxcards(index){
while (index < mybadges.length) {
if (mybadges[index].maxcards === 0) {
let url="https://steamcommunity.com/my/gamecards/"+mybadges[index].appid+"?l=english";
let xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'document';
xhr.onload = function() {
let status = xhr.status;
let lastasset=0;
if (status === 200) {
errors = 0;
debugPrint("processing badge "+mybadges[index].appid);
UpdateMessage("Getting badge data for "+mybadges[index].appid);
let maxcards = xhr.response.documentElement.getElementsByClassName("gamecard").length;
mybadges[index].maxcards=maxcards;
index++;
} else {
errors++;
}
if ((status<400 || status>=500)&&(errors <= maxErrors)) {
setTimeout((function(index) { return function(){ PopulateMaxcards(index); };})(index), limiter);
} else {
UpdateMessage("Error getting badge data, ERROR "+status);
HideThrobber();
EnableButton();
}
};
xhr.onerror = function() {
debugPrint("error");
UpdateMessage("Error getting badge data");
HideThrobber();
EnableButton();
};
xhr.send();
return; //do this synchronously to avoid rate limit
} else {
index++;
}
}
debugPrint("populated");
UpdateMessage("Fetching own inventory");
//g_steamID is a global steam variable
fetchinventory(g_steamID,0,function(){
debugPrint("fetched");
debugPrint(DeepClone(assets));
debugPrint(DeepClone(descriptions));
debugPrint("our cards");
debugPrint(DeepClone(mybadges));
PopulateExistingCards(mybadges,true);
checkuser(0);
});
}
function getbadges(page){
let url="https://steamcommunity.com/my/badges?p="+page+"&l=english";
let xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'document';
xhr.onload = function() {
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());
}
}
let badges=xhr.response.documentElement.getElementsByClassName("badge_row_inner");
for (let i=0;i<badges.length;i++){
if (badges[i].getElementsByClassName("owned").length>0){ //we only need badges where we have at least one card, and no special badges
let appidNodes = badges[i].getElementsByClassName("card_drop_info_dialog");
if (appidNodes.length>0) {
let appidText=appidNodes[0].getAttribute("id");
let appidSplitted = appidText.split("_");
if (appidSplitted.length >= 5) {
let appid = Number(appidSplitted[4]);
let maxcards=0;
if (badges[i].getElementsByClassName("badge_craft_button").length === 0 ){
let maxcardsText=badges[i].getElementsByClassName("badge_progress_info")[0].innerText.trim()
let maxcardsSplitted=maxcardsText.split(" ");
maxcards = Number(maxcardsSplitted[2]);
}
let badge_stub = {"appid":appid,"title":null, "maxcards":maxcards, "maxsets":0, "lastset":0, "cards":[]};
mybadges.push(badge_stub);
}
}
}
}
page++;
} else {
errors++;
}
if ((status<400 || status>=500)&&(errors <= maxErrors)) {
if (page<=maxpages) {
setTimeout((function(page) { return function(){ getbadges(page); };})(page), limiter);
} else {
debugPrint("all badge pages processed");
if (mybadges.length===0){
HideThrobber();
UpdateMessage("No cards to match");
return;
} else {
PopulateMaxcards(0);
}
}
} else {
UpdateMessage("Error getting badge data, ERROR "+status);
HideThrobber();
EnableButton();
}
};
xhr.onerror = function() {
debugPrint("error getting badge page");
UpdateMessage("Error getting badge page");
HideThrobber();
EnableButton();
};
xhr.send();
}
function ButtonPressed(){
DisableButton();
if (debug) {
console.log(new Date(Date.now()));
}
let maincontent = document.getElementsByClassName("maincontent")[0];
maincontent.textContent="";
maincontent.style.width="90%";
maincontent.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 class="profile_xp_block_remaining_bar">
<div id="asf_stm_progress" class="profile_xp_block_remaining_bar_progress" style="width: 0%"></div>
</div>
</div>
`;
maxpages=1;
getbadges(1);
}
if (document.getElementsByClassName("badge_details_set_favorite").length != 0) {
let requestURL = 'https://asf.justarchi.net/Api/Bots';
GM_xmlhttpRequest({
method: "GET",
url: requestURL,
onload: function(response) {
let re = /("steam_id":)(\d+)/g;
let fixedjson = response.response.replace(re, '$1\"$2\"'); //because fuck js
bots = JSON.parse(fixedjson);
//bots.filter(bot=>bot.matchable_cards===1||bot.matchable_foil_cards===1); //I don't think this is really needed
bots.sort(function(a,b) { //sort received array as I like it.
let result = b.match_everything - a.match_everything; //bots with match_everything go first
if (result===0) {
result = b.items_count - a.items_count; //then by items_counts descending
}
if (result===0) {
result = b.games_count - a.games_count; //then by games_count descending
}
return result;
});
debugPrint ("found total "+bots.length+" bots");
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");
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();
}
});
}
})();