Skip to content

Instantly share code, notes, and snippets.

@EklipZgit
Created January 19, 2025 22:19
Show Gist options
  • Save EklipZgit/c221e3d7a35f4f5133c3d9195eff8fbe to your computer and use it in GitHub Desktop.
Save EklipZgit/c221e3d7a35f4f5133c3d9195eff8fbe to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name zzd233's Queue Sniper
// @namespace http://tampermonkey.net/
// @version 1.1
// @description A tool for 1v1 to get stars. v1.1 changes: Updated to use new recent games APIs. Improved performance. Stop ddosing server from other browser tabs. Improve layout and formatting to fit more names and resize large lists better.
// @author zzd233 + EklipZ
// @match https://*.generals.io/*
// @icon https://generals.io/favicon/favicon-32x32.png
// @grant none
// ==/UserScript==
let abs = Math.abs, max = Math.max, min = Math.min;
function load(src) {
return new Promise((resolve, reject) => {
let c = document.createElement('script');
c.src = src;
c.addEventListener('load', resolve);
c.addEventListener('error', reject);
document.body.appendChild(c);
});
}
let logDebug = false;
// let socket;
// function waitConnect() {
// return new Promise((resolve, reject) => {
// socket.once('disconnect', reject);
// socket.once('connect', resolve);
// });
// }
function clickButton(text) {
Array.from(document.getElementsByTagName('button')).find(e => e.innerText.trim().toLowerCase() === text.toLowerCase().trim()).click();
}
function shouldRenderSniperModal() {
const mainMenuSideButtons = document.getElementById('main-menu-side-buttons');
if (mainMenuSideButtons && mainMenuSideButtons.checkVisibility())
return true;
const tipBanner = document.getElementById('tip-banner');
if (tipBanner && tipBanner.checkVisibility())
return true;
const queueAd = document.getElementById('custom-queue-bottom-ad');
if (queueAd && queueAd.checkVisibility()) {
let buttons = Array.from(document.getElementsByTagName('button')).map(a => a.innerHTML);
if (!buttons.find(a => a === "PLAY" || a === "1v1" || a === "Play Again" || a === "Cancel")) {
return false;
}
return true;
}
const customQueueAd = document.getElementById('custom-queue-ad-top');
if (customQueueAd && customQueueAd.checkVisibility()) {
let buttons = Array.from(document.getElementsByTagName('button')).map(a => a.innerHTML);
if (!buttons.find(a => a === "PLAY" || a === "1v1" || a === "Play Again" || a === "Cancel")) {
return false;
}
return true;
}
const igc = document.getElementById('in-game-chat');
if (igc && igc.checkVisibility()) {
var isGameOver = false;
for (var n of igc.childNodes.item(0).childNodes.item(0).childNodes) {
if (n.classList.contains('server-chat-message')) {
const messageItem = n.childNodes.item(3);
if (messageItem && messageItem.textContent.endsWith(' wins!')) {
isGameOver = true;
break;
}
}
// console.log(n);
}
return isGameOver;
}
// const userNameInput = document.getElementById('main-menu-username-input');
// if (userNameInput && userNameInput.checkVisibility())
// return true;
// let buttons = Array.from(document.getElementsByTagName('button')).map(a => a.innerHTML);
// if (!buttons.find(a => a === "PLAY" || a === "1v1" || a === "Play Again" || a === "Cancel")) {
// return false;
// }
// return true;
return false;
}
async function load_elements() {
let c = document.createElement("style");
c.innerHTML = `
body {
background-color: #222;
}
#Sniper-all {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
-khtml-user-select: none;
user-select: none;
z-index: 9999;
position: absolute;
top: 70px;
left: 10px;
width: 343px;
}
#Sniper-option, #Sniper-list {
font-size: 18px;
background-color: #333333;
border: #333333 solid 5px;
border-radius: 1px;
// font-family: Quicksand-Bold;
//text-shadow: 1px 1px 1px white;
}
#Sniper-title {
font-size: 22px;
height: 30px;
padding: 5px 10px 0px 10px;
}
#Sniper-option {
margin-top: 5px;
height: 164px;
padding: 10px 10px 10px 10px;
}
#Sniper-range {
border: #333333 solid 3px;
margin-bottom: 3px;
color: white;
}
#Sniper-list {
text-align: center;
margin-top: 5px;
font-size: 18px;
min-height: 260px;
max-height: 60vh;
padding: 5px 5px 5px 5px;
overflow-y: scroll;
}
#Sniper-list-table {
display: table;
}
.Sniper-button {
border: teal solid 1px;
border-radius: 1px;
background-color: teal;
height: 25px;
width: 50px;
text-align: center;
color: white;
}
#Sniper-button4 {
border: teal solid 1px;
border-radius: 1px;
background-color: teal;
height: 25px;
width: 76px;
text-align: center;
color: white;
}
#Sniper-enable_match, #Sniper-enable_leaderboard, #Sniper-enable_friends, #Sniper-new_friend, #Sniper-toggle_list {
display: flex;
flex-direction: row;
align-items: center;
color: white;
}
#Sniper-match-starbound {
font-size: 15px;
height: 30px;
width: 45px;
color: black;
}
#Sniper-new_friend {
padding-top: 3px;
color: white;
}
#Sniper-addfriend {
text-align: left;
font-size: 12px;
height: 25px;
width: 90px;
color; white;
}
.Sniper-table_row {
text-align: center;
display: table-row;
color: white;
}
.Sniper-table_cell {
text-align: center;
color: white;
display: table-cell;
border: #333333 solid 1px;
padding: 1px 1px 1px 1px;
}
.Sniper-header {
text-align: center;
background-color: teal;
color: white;
}
.Sniper-friendcell {
background-color: teal;
color: white;
}
.Sniper-name {
width: 75%;
// text-align: right;
}
.Sniper-links {
text-align: center;
color: white;
}
#Sniper-titlelink {
color: white;
text-align: center;
text-shadow: 2px 2px teal;
font-family: Quicksand-Bold;
}
`;
c.rel = "stylesheet";
document.body.appendChild(c);
let d = document.createElement("div");
d.innerHTML = `
<div id = "Sniper-all" hidden = "true">
<div id = "Sniper-title">
<a id = "Sniper-titlelink" href = "https://generals.io" target = "_blank">Generals.io 1v1 Match Helper</a>
</div>
<div id = "Sniper-option">
<div id = "Sniper-range">
<div>
&nbsp;Finding Range
</div>
<div id = "Sniper-enable_leaderboard">
<div>
&nbsp;Leaderboard: ★ ≥&nbsp;
</div>
<input id = "Sniper-match-starbound" placeholder = "inf"> &nbsp;
<div class = "Sniper-button" id = "Sniper-button2">OFF</div>
</div>
<div id = "Sniper-enable_friends">
<div>
&nbsp;Friends: &nbsp;
</div>
<div class = "Sniper-button" id = "Sniper-button3">OFF</div>
</div>
<div id = "Sniper-enable_match">
<div>
&nbsp;Auto Match:&nbsp;
</div>
<div class = "Sniper-button" id = "Sniper-button1">OFF</div>
</div>
<div id = "Sniper-new_friend">
<div>&nbsp;Modify friend:&nbsp;</div>
<input id = "Sniper-addfriend" placeholder = "Someone"> &nbsp;
<div id = "Sniper-button4">
Add/Del
</div>
</div>
<div id = "Sniper-toggle_list">
<div>
&nbsp;Show list: &nbsp;
</div>
<div class = "Sniper-button" id = "Sniper-toggle">ON</div>
</div>
</div>
</div>
<div id = "Sniper-list">
<div id = "Sniper-list-table">
</div>
</div>
</div>
`;
document.body.appendChild(d);
}
let main_div, button1, button2, button3, button4, option_element, list_element, toggle;
let match_starbound, addfriend;
let list_table;
let enable_auto_match = false;
let enable_leaderboard = false;
let enable_friends = false;
let friend_list = [];
const Eps = 1e-3;
let myname = undefined;
let data = {};//store other's star and last 1v1 game time; example: data["zzd233"] = {star: 70.00, time: 1617360510077}
let friend_dictionary = {};
function px2int(s) {
return parseInt(s.substr(0, s.length - 2));
}
function int2px(a) {
return `${a}px`;
}
function main() {
// after one second, try to run the thing, hopefully the site is loaded by then (?)
setTimeout(async () => {
// let lib_socket = 'https://cdn.jsdelivr.net/npm/socket.io-client@4/dist/socket.io.js';
let lib_jquery = 'https://code.jquery.com/jquery-3.6.0.min.js';
// await load(lib_socket);
await load(lib_jquery);
await load_elements();
// Detect when this is the active tab or not.
let isWindowFocused = true;
$(window).on("blur focus", function(e) {
var prevType = $(this).data("prevType");
if (prevType != e.type) { // reduce double fire issues
switch (e.type) {
case "blur":
isWindowFocused = false;
break;
case "focus":
isWindowFocused = true;
break;
}
}
$(this).data("prevType", e.type);
});
// // This opens its own socket...? Why not grab the socket thats already open, is that not possible with js?
// socket = io('https://ws.generals.io');
// await waitConnect();
// console.log('connected');
function get_myname() {
let tmp = document.getElementById('main-menu-username-input');
if (tmp)
myname = tmp.value;
}
get_myname();
main_div = document.getElementById('Sniper-all');
let local_main_div_position = JSON.parse(localStorage.getItem("SniperScript_main_div_position"));
if (!local_main_div_position) {
local_main_div_position = {x: 0, y: 0};
localStorage.setItem("SniperScript_main_div_position", JSON.stringify(local_main_div_position));
}
main_div.style.left = int2px(local_main_div_position.x);
main_div.style.top = int2px(local_main_div_position.y);
button1 = document.getElementById('Sniper-button1');
button2 = document.getElementById('Sniper-button2');
button3 = document.getElementById('Sniper-button3');
button4 = document.getElementById('Sniper-button4');
toggle = document.getElementById('Sniper-toggle');
option_element = document.getElementById('Sniper-option');
list_element = document.getElementById('Sniper-list');
toggle.style.backgroundColor = "teal";
if(window.localStorage["QUEUE_SNIPER_HIDE_LIST"] == "hide") {
toggle.textContent = "OFF";
list_element.style.display = "none";
toggle.style.backgroundColor = "#333333";
}
toggle.addEventListener('click', () => {
if(window.localStorage["QUEUE_SNIPER_HIDE_LIST"] == "hide") {
window.localStorage["QUEUE_SNIPER_HIDE_LIST"] = "show";
toggle.textContent = "ON";
list_element.style.display = "";
toggle.style.backgroundColor = "teal";
} else {
window.localStorage["QUEUE_SNIPER_HIDE_LIST"] = "hide";
toggle.textContent = "OFF";
list_element.style.display = "none";
toggle.style.backgroundColor = "#333333";
}
});
match_starbound = document.getElementById("Sniper-match-starbound");
addFriendInputBox = document.getElementById("Sniper-addfriend");
list_table = document.getElementById("Sniper-list-table");
var enable_match = false;
let settings = JSON.parse(localStorage.getItem("zzdscript_settings"));
if (!settings) {
settings = [enable_match, enable_leaderboard, enable_friends, "50"];
localStorage.setItem('zzdscript_settings',JSON.stringify(settings));
}
[enable_match, enable_leaderboard, enable_friends, match_starbound.value] = settings;
if (enable_match) {
button1.innerHTML = "ON";
button1.style.backgroundColor = "teal";
} else {
button1.innerHTML = "OFF";
button1.style.backgroundColor = "#333333";
}
if (enable_leaderboard) {
button2.innerHTML = "ON";
button2.style.backgroundColor = "teal";
} else {
button2.innerHTML = "OFF";
button2.style.backgroundColor = "#333333";
}
if (enable_friends) {
button3.innerHTML = "ON";
button3.style.backgroundColor = "teal";
} else {
button3.innerHTML = "OFF";
button3.style.backgroundColor = "#333333";
}
friend_list = JSON.parse(localStorage.getItem("zzdscript_Friends"));
if (!friend_list) {
friend_list = [];
localStorage.setItem("zzdscript_Friends", JSON.stringify(friend_list));
}
for (let name of friend_list)
friend_dictionary[name] = true;
button1.addEventListener('click', () => {
if (button1.innerHTML === "OFF") {
button1.innerHTML = "ON";
button1.style.backgroundColor = "teal";
enable_match = true;
} else {
button1.innerHTML = "OFF";
button1.style.backgroundColor = "#333333";
enable_match = false;
}
settings = [enable_match, enable_leaderboard, enable_friends, match_starbound.value];
localStorage.setItem('zzdscript_settings',JSON.stringify(settings));
});
button2.addEventListener('click', () => {
if (button2.innerHTML === "OFF") {
button2.innerHTML = "ON";
button2.style.backgroundColor = "teal";
enable_leaderboard = true;
} else {
button2.innerHTML = "OFF";
button2.style.backgroundColor = "#333333";
enable_leaderboard = false;
}
settings = [enable_match, enable_leaderboard, enable_friends, match_starbound.value];
localStorage.setItem('zzdscript_settings',JSON.stringify(settings));
});
button3.addEventListener('click', () => {
if (button3.innerHTML === "OFF") {
button3.innerHTML = "ON";
button3.style.backgroundColor = "teal";
enable_friends = true;
} else {
button3.innerHTML = "OFF";
button3.style.backgroundColor = "#333333";
enable_friends = false;
}
settings = [enable_match, enable_leaderboard, enable_friends, match_starbound.value];
localStorage.setItem('zzdscript_settings',JSON.stringify(settings));
});
function executeAddDelFriend() {
let name = addFriendInputBox.value.trim();
if (name.length === 0)
return;
let index = friend_list.findIndex(x => x === name);
if (index === -1) {
friend_list.push(name);
friend_dictionary[name] = true;
} else {
friend_list.splice(index,1);
friend_dictionary[name] = undefined;
}
addFriendInputBox.value = "";
localStorage.setItem("zzdscript_Friends", JSON.stringify(friend_list));
}
button4.addEventListener('click', executeAddDelFriend);
addFriendInputBox.addEventListener('keydown', (kEvent) => {
if (kEvent.keyCode === 13)
executeAddDelFriend();
});
let leaderboard_initialized = false, leaderboard_cnt = 0;
let NOREPLAY = "";
let time_delta = 0;
let real_time = Number(new Date($.ajax({async:false}).getResponseHeader("Date")));
let system_time = Number(new Date());
time_delta = real_time - system_time;
console.log(`time_delta = `, time_delta);
// Auto-hide on pages where this shouldnt show
setInterval(async () => {
if (!shouldRenderSniperModal()) {
main_div.hidden = true;
}
}, 100);
var lastUpdatedLadder = 0;
let players_from_leaderboard, bound;
let tasks = 0, time_0 = undefined, friend_list_loaded = false;
let count_tasks_n0 = 0, last_tasks = -1;
let requestNumber = 0;
players_from_leaderboard = new Set();
// Every half second, update the leaderboard
let main_interval = setInterval(async () => {
requestNumber++;
// Dont DDOS the server and get ourselves banned running for non-focused windows.
if (!isWindowFocused && requestNumber % 20 !== 0 && !enable_auto_match) {
if (logDebug)
console.log(`Skipping iteration ${requestNumber} because isWindowFocused ${isWindowFocused}`);
return;
}
if (logDebug)
console.log(`Main iteration: Tasks ${tasks}, last_tasks ${last_tasks}, count_tasks_n0 ${count_tasks_n0}`);
if (tasks !== 0 && last_tasks === tasks)
count_tasks_n0 ++;
else
count_tasks_n0 = 0;
if (count_tasks_n0 > 500) {
// clearInterval(main_interval);
tasks = count_tasks_n0 = 0;
}
last_tasks = tasks;
if (tasks < 0) {
clearInterval(main_interval);
throw "tasks < 0";
}
if (tasks > 0)
return;
if (time_0 !== undefined) {
let now = Number(new Date()) + time_delta;
if (now - time_0 > 2500)
console.log(new Date(), `last fetch time = ${now - time_0}ms ago`);
let pool = [];
if (enable_friends)
for (let name of friend_list) {
if (!data.hasOwnProperty(name))
continue;
pool.push({
name: name,
star: data[name].star,
time_past: isNaN(data[name].time) ? NaN : now - data[name].time,
isfriend: true,
type: data[name].type,
});
}
if (enable_leaderboard) {
for (let name of players_from_leaderboard) {
if (!name)
continue;
if ((friend_dictionary[name] === true && enable_friends) || !data[name] || !(data[name].star > bound) || name === myname)
continue;
if (isNaN(data[name].time) || now - data[name].time > 1000 * 60 * 30)
continue;
pool.push({
name: name,
star: data[name].star,
time_past: isNaN(data[name].time) ? NaN : now - data[name].time,
isfriend: false,
type: data[name].type,
});
}
}
pool.sort((a, b) => {
let [x, y] = [a.time_past, b.time_past];
if (isNaN(x) && isNaN(y)) {
return b.star - a.star;
}
if (isNaN(x) !== isNaN(y))
return isNaN(x) ? 1 : -1;
return x - y;
});
let htmlstring = `
<div class = "Sniper-table_row">
<div class = "Sniper-table_cell Sniper-header Sniper-name">
PLAYER
</div>
<div class = "Sniper-table_cell Sniper-header">
&nbsp;&nbsp;★&nbsp;&nbsp;
</div>
<div class = "Sniper-table_cell Sniper-header">
1V1
</div>
<div class = "Sniper-table_cell Sniper-header">
LAST
</div>
</div>
`;
function time_past_to_string(t) {
if (isNaN(t))
return "";
t = Math.floor(t / 1000);
if (t < 60)
return `${t}s`;
else if (t < 3600)
return `${Math.floor(t/60)}m`;
else if (t < 3600 * 24)
return `${(t/3600).toFixed(1)}h`;
else if (t < 3600 * 24 * 30)
return `${(t/(3600 * 24)).toFixed(1)}d`;
else
return '';
}
for (let user of pool) {
var typeName = user.type === 'classic' ? 'FFA' : (user.type === 'custom' ? 'Cust' : user.type);
htmlstring += `
<div class = "Sniper-table_row">
<div class = "Sniper-table_cell ${user.isfriend ? "Sniper-friendcell" : ""} Sniper-name">
<a class = "Sniper-links" href = "https://generals.io/profiles/${encodeURIComponent(user.name)}" target = "_blank">${user.name}</a>
</div>
<div class = "Sniper-table_cell ${user.isfriend ? "Sniper-friendcell" : ""}">
${user.star}
</div>
<div class = "Sniper-table_cell ${user.isfriend ? "Sniper-friendcell" : ""}">
${isNaN(user.time_past) ? '' : time_past_to_string(user.time_past)}
</div>
<div class = "Sniper-table_cell ${user.isfriend ? "Sniper-friendcell" : ""}">
${typeName}
</div>
</div>
`;
}
list_table.innerHTML = htmlstring;
// Automatch for 8 seconds, then cancel queue if no match
if (enable_match && pool.length > 0) {
let t = pool[0];
if (t.time_past < 8000) {
console.log("join 1v1!");
try { clickButton('play'); } catch (e) { }
try { clickButton('1v1'); } catch (e) { }
try { clickButton('play again'); } catch (e) { }
button1.innerHTML = "OFF";
button1.style.backgroundColor = "#333333";
enable_match = false;
// cancel in 8 seconds if the game hasn't started by then.
setTimeout(() => {
try {
clickButton('cancel');
button1.innerHTML = "ON";
button1.style.backgroundColor = "teal";
enable_match = true;
} catch (e) {}
}, 8000);
}
}
}
if (!shouldRenderSniperModal()) {
main_div.hidden = true;
time_0 = undefined;
return;
}
main_div.hidden = false;
time_0 = Number(new Date()) + time_delta;
get_myname();
function update_player(name, current_star) {
if (data[name] != undefined && abs(current_star - data[name].star) < 0.01)
return;
else {
if (logDebug)
console.log(`name = ${name} star = ${current_star}`, data[name]);
tasks+=100;
let task_id = Number(new Date())%100000;
// console.log(`+${task_id}, ${tasks}`);
let url = 'https://generals.io/api/replaysForUsername?u=' + encodeURIComponent(name) + '&offset=0&count=100';
fetch(url).then(tmp => {
return tmp.json();
}).then(tmp => {
if (!data.hasOwnProperty[name]) {
data[name] = {star: 0, time: NaN, type: NOREPLAY, timeOther: NaN};
}
if (!!tmp && !!tmp.length) {
var dataObj = data[name];
dataObj.star = current_star;
var isFirst = true;
for (var playerReplay of tmp) {
var replayTime = playerReplay.started + (playerReplay.turns - 1) * 500;
if (isFirst) {
dataObj.timeOther = replayTime;
dataObj.type = playerReplay.type;
}
if (playerReplay.type === '1v1')
{
dataObj.time = replayTime;
break;
}
isFirst = false;
}
} else {
console.log("cant find any replays for user " + name);
}
tasks-=100;
// console.log(`-${task_id}, ${tasks}`);
});
}
}
settings = [enable_match, enable_leaderboard, enable_friends, match_starbound.value];
localStorage.setItem('zzdscript_settings',JSON.stringify(settings));
bound = parseFloat(match_starbound.value);
if (isNaN(bound))
bound = Infinity;
bound -= Eps;
tasks+=10000;
// console.log(`+${task_id}, ${tasks}`);
const now = Number(new Date());
let gamesToRequest = 5;
// if its been longer than a second since we last updated
var timeSinceLast = now - lastUpdatedLadder;
if (logDebug)
console.log(`timeSinceLast ${timeSinceLast}`);
if (timeSinceLast > 1000) {
gamesToRequest = 20;
}
// if its been longer than 20 seconds since we last updated
if (timeSinceLast > 20 * 1000) {
gamesToRequest = 200;
}
lastUpdatedLadder = now;
let task_id = now%100000;
// console.log(`+${task_id}, ${tasks}`);
let url = `https://generals.io/api/replays?count=${gamesToRequest}&offset=0`;
if (logDebug)
console.log(`Making main recent games call to ${url}`);
var tmp = await fetch(url);
var deserialized = await tmp.json();
var handled = new Set();
for (let replay of deserialized) {
var timeFinished = replay.started + (replay.turns - 1) * 500;
for (let ranking of replay.ranking) {
var name = ranking.name;
if (handled.has(name))
continue;
if (!data.hasOwnProperty(name)) {
data[name] = {star: 0, time: NaN, type: NOREPLAY, timeOther: NaN};
}
const playerData = data[name];
if (replay.type === '1v1') {
playerData.star = ranking.stars;
playerData.time = timeFinished;
handled.add(name);
players_from_leaderboard.add(name);
}
playerData.timeOther = timeFinished;
playerData.type = replay.type;
}
}
// if (enable_friends || !friend_list_loaded) {
if (!friend_list_loaded) {
friend_list_loaded = true;
// for (var i = 0; i < users.length; i++)
for (let name of friend_list) {
// skip players we've seen play 1v1 already
if (handled.has(name) || (data.hasOwnProperty(name) && !isNaN(data[name].time)))
continue;
tasks++;
let task_id = Number(new Date())%100000;
// console.log(`+${task_id}, ${tasks}`);
let url = 'https://generals.io/api/starsAndRanks?u=' + encodeURIComponent(name);
// Leave this non-async, I guess, so it is parallel. May cause bans still though?
fetch(url).then(tmp => {
return tmp.json();
}).then(tmp => {
let star = parseFloat(tmp.stars.duel).toFixed(0);
if (isNaN(star))
star = 0;
update_player(name, star);
tasks--;
// console.log(`-${task_id}, ${tasks}`);
});
}
}
tasks-=10000;
}, 100);
}, 1000);
};
let checkready = setInterval(() => {
if (document.readyState === "complete") {
main();
clearInterval(checkready);
}
}, 250);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment