Queue to join full servers when open.
À data de
// ==UserScript==
// @name Bumpyball Server Queue
// @namespace https://greasyfork.org/en/users/1574974-dylko
// @version 1.1
// @description Queue to join full servers when open.
// @author Dylko
// @match https://www.bumpyball.io/*
// @match https://www.pucks.io/*
// @run-at document-start
// @grant none
// @license All Rights Reserved
// ==/UserScript==
(() => {
"use strict";
/******************************************************************
* STORAGE / CONFIG
******************************************************************/
const STORAGE_KEY = "DYLKO_DYNAMIC_QUEUE_V5";
const saved = (() => {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
} catch {
return {};
}
})();
// Old versions saved target; ignore and remove it so reloads always start clean.
if (Object.prototype.hasOwnProperty.call(saved, "target")) {
try {
delete saved.target;
localStorage.setItem(STORAGE_KEY, JSON.stringify(saved));
} catch {}
}
const CFG = {
listingUrl: "https://nifty-condition-169823.appspot.com/Listing?Game=BumpyBall&ListingVersion=3",
// Full list refresh. Keep this slower because it pings every lobby.
scanIntervalMs: saved.scanIntervalMs || 5000,
// Active queue check. This only pings the selected target lobby.
// 200ms = 5 checks/sec.
queuePollMs: saved.queuePollMs || 200,
// Canvas-relative default home-screen Play button.
// Calibrated from your canvas:
// client 275,430 on canvas 943.2x825.6 => 0.29156064, 0.52083331
defaultPlayClickNorm: {
x: 0.175,
y: 0.5208333102307589,
capturedAt: "default-home-play-normalized",
label: "Default Play Button",
},
// Optional custom trigger. If set, this overrides defaultPlayClickNorm.
decoyClick: saved.decoyClick || null,
requireOpenSlot: true,
// Auto click retry behavior.
decoyRetryMax: saved.decoyRetryMax || 12,
decoyRetryDelayMs: saved.decoyRetryDelayMs || 550,
// Keep the panel visible while firing the trigger, but hide it once a join/redirect is confirmed.
hidePanelOnTriggerFire: false,
hidePanelOnRedirect: true,
// Keep false unless debugging.
dryRun: false,
notify: saved.notify ?? true,
beep: saved.beep ?? true,
};
const state = {
uiReady: false,
lobbies: [],
// Do NOT restore saved target on page load. Each reload must start with no queued server.
target: null,
queueing: false,
armed: false,
captureMode: false,
scanTimer: null,
queueTimer: null,
decoyTimer: null,
lastTargetPing: null,
lastStatus: "loading; choose a server",
serversVisible: true,
redirects: [],
websockets: [],
};
function save() {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
scanIntervalMs: CFG.scanIntervalMs,
queuePollMs: CFG.queuePollMs,
decoyRetryMax: CFG.decoyRetryMax,
decoyRetryDelayMs: CFG.decoyRetryDelayMs,
defaultPlayClickNorm: CFG.defaultPlayClickNorm,
decoyClick: CFG.decoyClick,
hidePanelOnTriggerFire: CFG.hidePanelOnTriggerFire,
hidePanelOnRedirect: CFG.hidePanelOnRedirect,
notify: CFG.notify,
beep: CFG.beep,
})
);
}
/******************************************************************
* HELPERS
******************************************************************/
const targetPingUrl = () => {
if (!state.target) return null;
return `https://${state.target.host}:${state.target.port}/Ping?nocache=${Date.now()}`;
};
const targetWsUrl = () => {
if (!state.target) return null;
return `wss://${state.target.host}:${state.target.port}/server`;
};
function getCanvas() {
return document.getElementById("unity-canvas") || document.querySelector("canvas");
}
function resolveCanvasPoint(normPoint) {
const canvas = getCanvas();
if (!canvas || !normPoint) return null;
const rect = canvas.getBoundingClientRect();
const x = rect.left + rect.width * normPoint.x;
const y = rect.top + rect.height * normPoint.y;
return {
x,
y,
screenX: window.screenX + x,
screenY: window.screenY + y,
label: normPoint.label || "Canvas-relative point",
normX: normPoint.x,
normY: normPoint.y,
capturedAt: normPoint.capturedAt || "canvas-relative",
};
}
function getTriggerClick() {
// Custom captured trigger still wins.
if (CFG.decoyClick) return CFG.decoyClick;
// Default Play button is canvas-relative.
return resolveCanvasPoint(CFG.defaultPlayClickNorm);
}
function getTriggerLabel() {
return CFG.decoyClick ? "Custom trigger" : "Default Play button auto-detected";
}
function log(...args) {
console.log("[DylkoDQ]", ...args);
}
function warn(...args) {
console.warn("[DylkoDQ]", ...args);
}
async function fetchJson(url) {
const res = await fetch(url, { cache: "no-store" });
return await res.json();
}
function pointText(p) {
if (!p) return "not set";
const base = `${Math.round(p.x)}, ${Math.round(p.y)}`;
if (Number.isFinite(p.normX) && Number.isFinite(p.normY)) {
return `${base} / norm ${p.normX.toFixed(4)}, ${p.normY.toFixed(4)}`;
}
return base;
}
function hidePanel(reason = "hidden") {
const panel = document.getElementById("dylko-dq-panel");
if (panel) {
panel.style.display = "none";
}
state.lastStatus = reason;
warn("panel hidden:", reason);
}
function showPanel() {
const panel = document.getElementById("dylko-dq-panel");
if (panel) {
panel.style.display = "block";
}
render();
warn("panel shown");
}
function togglePanel() {
const panel = document.getElementById("dylko-dq-panel");
if (!panel) return;
panel.style.display = panel.style.display === "none" ? "block" : "none";
warn("toggled panel:", panel.style.display || "block");
}
function beep() {
if (!CFG.beep) return;
try {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
const ctx = new AudioCtx();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.frequency.value = 880;
gain.gain.value = 0.07;
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
setTimeout(() => {
osc.stop();
ctx.close();
}, 160);
} catch {}
}
function notify(title, text) {
warn(title, text);
beep();
if (!CFG.notify) return;
try {
if ("Notification" in window && Notification.permission === "granted") {
new Notification(title, { body: text });
} else if ("Notification" in window && Notification.permission !== "denied") {
Notification.requestPermission();
}
} catch {}
}
function showTriggerMarker() {
const point = getTriggerClick();
if (!point) {
warn("No trigger point to mark");
state.lastStatus = "no trigger point to mark";
render();
return;
}
let dot = document.getElementById("dylko-trigger-marker");
if (!dot) {
dot = document.createElement("div");
dot.id = "dylko-trigger-marker";
Object.assign(dot.style, {
position: "fixed",
width: "14px",
height: "14px",
borderRadius: "50%",
background: "red",
border: "2px solid white",
zIndex: "1000000",
pointerEvents: "none",
transform: "translate(-50%, -50%)",
boxShadow: "0 0 10px rgba(255,0,0,.9)",
});
document.body.appendChild(dot);
}
dot.style.left = `${point.x}px`;
dot.style.top = `${point.y}px`;
setTimeout(() => dot?.remove(), 2500);
state.lastStatus = `marker shown for ${getTriggerLabel()}`;
render();
warn("trigger marker shown", {
label: getTriggerLabel(),
point,
});
}
/******************************************************************
* LOBBY SCANNER
******************************************************************/
async function scanLobbies() {
try {
const listing = await fetchJson(CFG.listingUrl);
const rows = [];
for (const item of listing) {
const host = item.Address ?? item.address ?? item.Host ?? item.host;
const port = Number(item.Port ?? item.port);
if (!host || !port) continue;
try {
const ping = await fetchJson(`https://${host}:${port}/Ping?nocache=${Date.now()}`);
const current = Number(ping.CurrentPlayers);
const max = Number(ping.MaxPlayers);
rows.push({
host,
port,
name: item.Host || host,
current,
max,
full: current >= max,
open: current < max,
public: item.Public,
password: item.Password,
secure: item.Secure,
ws: `wss://${host}:${port}/server`,
listingItem: item,
ping,
});
} catch (e) {
rows.push({
host,
port,
name: item.Host || host,
error: e.message,
listingItem: item,
});
}
}
rows.sort((a, b) => {
const af = a.full ? 0 : 1;
const bf = b.full ? 0 : 1;
if (af !== bf) return af - bf;
const ap = Number.isFinite(a.current) ? -a.current : 999;
const bp = Number.isFinite(b.current) ? -b.current : 999;
if (ap !== bp) return ap - bp;
const n = String(a.name).localeCompare(String(b.name));
if (n) return n;
return Number(a.port) - Number(b.port);
});
state.lobbies = rows;
state.lastStatus = `scanned ${rows.length} lobbies`;
render();
return rows;
} catch (e) {
state.lastStatus = `scan error: ${e.message}`;
warn("scan error", e);
render();
return [];
}
}
/******************************************************************
* QUEUE TARGET LOGIC
******************************************************************/
async function checkTarget() {
if (!state.target) return false;
try {
const ping = await fetchJson(targetPingUrl());
const current = Number(ping.CurrentPlayers);
const max = Number(ping.MaxPlayers);
const open = Number.isFinite(current) && Number.isFinite(max) && current < max;
state.lastTargetPing = {
time: new Date().toISOString(),
current,
max,
open,
raw: ping,
};
state.lastStatus = open
? `target open: ${current}/${max}`
: `queued: ${current}/${max}`;
render();
return open;
} catch (e) {
state.lastTargetPing = {
time: new Date().toISOString(),
error: e.message,
};
state.lastStatus = `target ping error: ${e.message}`;
warn("target ping error", e);
render();
return false;
}
}
async function queueTick() {
if (!state.queueing || !state.target) return;
const open = await checkTarget();
if (open && !state.armed) {
armRedirect();
}
}
function startQueue(target) {
state.target = {
host: target.host,
port: target.port,
name: target.name,
max: target.max,
ws: target.ws,
};
state.queueing = true;
state.armed = false;
state.redirects = [];
save();
if (state.queueTimer) clearInterval(state.queueTimer);
state.lastStatus = `queueing ${state.target.name}:${state.target.port}`;
render();
queueTick();
state.queueTimer = setInterval(queueTick, CFG.queuePollMs);
log("queue started", {
target: state.target,
queuePollMs: CFG.queuePollMs,
trigger: getTriggerLabel(),
triggerPoint: getTriggerClick(),
});
}
function stopQueue() {
state.queueing = false;
state.armed = false;
if (state.queueTimer) clearInterval(state.queueTimer);
if (state.decoyTimer) clearTimeout(state.decoyTimer);
state.queueTimer = null;
state.decoyTimer = null;
state.lastStatus = "queue stopped";
render();
log("queue stopped");
}
function completeQueue(reason = "joined target; queue stopped") {
state.queueing = false;
state.armed = false;
if (state.queueTimer) clearInterval(state.queueTimer);
if (state.decoyTimer) clearTimeout(state.decoyTimer);
state.queueTimer = null;
state.decoyTimer = null;
state.lastStatus = reason;
render();
log("queue completed", reason);
}
/******************************************************************
* TRIGGER CLICK + REDIRECT ARMING
******************************************************************/
function fireClick(point, label = "trigger") {
const canvas = getCanvas();
if (!canvas) {
state.lastStatus = `${label} click failed: no canvas`;
warn("no canvas found");
render();
return false;
}
if (!point) {
state.lastStatus = `${label} click failed: no point`;
warn("no trigger point saved");
render();
return false;
}
const rect = canvas.getBoundingClientRect();
const x = point.x;
const y = point.y;
const common = {
bubbles: true,
cancelable: true,
composed: true,
view: window,
clientX: x,
clientY: y,
screenX: point.screenX || x,
screenY: point.screenY || y,
pageX: x + window.scrollX,
pageY: y + window.scrollY,
offsetX: x - rect.left,
offsetY: y - rect.top,
movementX: 0,
movementY: 0,
button: 0,
buttons: 1,
};
try {
window.focus();
canvas.focus?.();
canvas.dispatchEvent(new PointerEvent("pointerover", {
...common,
pointerId: 1,
pointerType: "mouse",
isPrimary: true,
}));
canvas.dispatchEvent(new PointerEvent("pointermove", {
...common,
pointerId: 1,
pointerType: "mouse",
isPrimary: true,
}));
canvas.dispatchEvent(new MouseEvent("mousemove", common));
canvas.dispatchEvent(new PointerEvent("pointerdown", {
...common,
pointerId: 1,
pointerType: "mouse",
isPrimary: true,
}));
canvas.dispatchEvent(new MouseEvent("mousedown", common));
setTimeout(() => {
const upCommon = { ...common, buttons: 0 };
canvas.dispatchEvent(new PointerEvent("pointerup", {
...upCommon,
pointerId: 1,
pointerType: "mouse",
isPrimary: true,
}));
canvas.dispatchEvent(new MouseEvent("mouseup", upCommon));
canvas.dispatchEvent(new MouseEvent("click", upCommon));
log(`${label} click dispatched`, {
x,
y,
offsetX: common.offsetX,
offsetY: common.offsetY,
});
}, 45);
log(`fired ${label} click`, point);
return true;
} catch (e) {
state.lastStatus = `${label} click error: ${e.message}`;
warn(`${label} click error`, e);
render();
return false;
}
}
function armRedirect() {
if (!state.target) {
warn("armRedirect failed: no target");
return;
}
state.armed = true;
state.lastStatus = `ARMED → ${state.target.host}:${state.target.port}`;
const triggerPoint = getTriggerClick();
warn("ARMING REDIRECT", {
target: state.target,
targetWs: targetWsUrl(),
lastTargetPing: state.lastTargetPing,
trigger: getTriggerLabel(),
triggerPoint,
});
render();
if (!triggerPoint) {
state.lastStatus = "armed, but no trigger point available";
warn("NO TRIGGER POINT AVAILABLE");
notify("Need trigger click", "Queue is armed. Click any joinable button manually.");
render();
return;
}
notify(
"Bumpyball queue armed",
`Slot open: ${state.target.name || state.target.host}:${state.target.port}`
);
let tries = 0;
const tryTrigger = () => {
if (!state.armed) {
warn("trigger loop stopped; redirect already fired or disarmed");
return;
}
tries++;
state.lastStatus = `armed; firing ${getTriggerLabel()} #${tries}`;
render();
const latestTriggerPoint = getTriggerClick();
warn(`FIRING ${getTriggerLabel()} #${tries}`, latestTriggerPoint);
let fired = false;
try {
fired = fireClick(latestTriggerPoint, getTriggerLabel());
} catch (e) {
warn("fireClick threw", e);
}
// Do not hide the panel here. Keep showing the UI until the WebSocket hook
// confirms the join attempt, stops the queue, and then hides the panel.
if (tries < CFG.decoyRetryMax && state.armed) {
state.decoyTimer = setTimeout(tryTrigger, CFG.decoyRetryDelayMs);
} else if (state.armed) {
state.lastStatus = "armed; auto-trigger failed, click Play/join manually";
render();
notify(
"Auto-trigger did not start join",
"Click Play or any joinable button manually; redirect is still armed."
);
}
};
state.decoyTimer = setTimeout(tryTrigger, 100);
}
/******************************************************************
* WEBSOCKET REDIRECT HOOK
******************************************************************/
function shouldRedirect(url) {
const s = String(url);
if (!state.armed) return false;
if (!state.target) return false;
if (!/\/server$/i.test(s)) return false;
if (!/^wss:\/\/.*usemapsettings\.com:\d+\/server$/i.test(s)) return false;
if (s === targetWsUrl()) return false;
if (CFG.requireOpenSlot && !state.lastTargetPing?.open) return false;
return true;
}
const NativeWebSocket = window.WebSocket;
window.WebSocket = function(url, protocols) {
const originalUrl = String(url);
let finalUrl = originalUrl;
const info = {
time: new Date().toISOString(),
originalUrl,
finalUrl,
armed: state.armed,
target: state.target,
redirected: false,
};
if (shouldRedirect(originalUrl)) {
finalUrl = targetWsUrl();
info.finalUrl = finalUrl;
info.redirected = true;
state.redirects.push(info);
completeQueue("redirect fired; joining target");
notify("Bumpyball redirect fired", `${originalUrl} → ${finalUrl}`);
warn("REDIRECT FIRED", {
from: originalUrl,
to: finalUrl,
target: state.target,
});
if (CFG.hidePanelOnRedirect && !CFG.dryRun) {
hidePanel("redirect fired; joining target");
}
if (CFG.dryRun) {
finalUrl = originalUrl;
}
}
state.websockets.push(info);
const ws = protocols === undefined
? new NativeWebSocket(finalUrl)
: new NativeWebSocket(finalUrl, protocols);
// If the game opens the target socket directly, or after redirect, consider the
// queue finished immediately. This keeps the button from staying in STOP mode.
if (state.target && String(finalUrl) === targetWsUrl() && (state.queueing || state.armed)) {
completeQueue("joining target; queue stopped");
if (CFG.hidePanelOnRedirect && !CFG.dryRun) {
hidePanel("joining target; queue stopped");
}
} else {
render();
}
return ws;
};
window.WebSocket.prototype = NativeWebSocket.prototype;
window.WebSocket.CONNECTING = NativeWebSocket.CONNECTING;
window.WebSocket.OPEN = NativeWebSocket.OPEN;
window.WebSocket.CLOSING = NativeWebSocket.CLOSING;
window.WebSocket.CLOSED = NativeWebSocket.CLOSED;
/******************************************************************
* CUSTOM TRIGGER CAPTURE
******************************************************************/
function captureEvent(ev) {
if (!state.captureMode) return;
const panel = document.getElementById("dylko-dq-panel");
if (panel?.contains(ev.target)) return;
CFG.decoyClick = {
x: ev.clientX,
y: ev.clientY,
screenX: ev.screenX,
screenY: ev.screenY,
capturedAt: new Date().toISOString(),
label: "Custom Trigger",
};
state.captureMode = false;
state.lastStatus = `captured custom trigger at ${pointText(CFG.decoyClick)}`;
save();
render();
warn("captured custom trigger", CFG.decoyClick);
ev.preventDefault();
ev.stopPropagation();
ev.stopImmediatePropagation();
}
for (const type of ["pointerdown", "mousedown", "mouseup", "click"]) {
document.addEventListener(type, captureEvent, true);
}
/******************************************************************
* HOTKEYS
******************************************************************/
document.addEventListener("keydown", (ev) => {
// Backslash toggles the queue UI. This replaces the old Shift+Q toggle.
if (ev.key === "\\" || ev.code === "Backslash") {
togglePanel();
ev.preventDefault();
}
});
/******************************************************************
* UI
******************************************************************/
function setAdvancedOpen(open) {
const panel = document.getElementById("dq-advanced-panel");
const toggle = document.getElementById("dq-advanced-toggle");
if (!panel || !toggle) return;
panel.style.display = open ? "block" : "none";
toggle.textContent = open ? "Advanced ▾" : "Advanced ▸";
toggle.setAttribute("aria-expanded", open ? "true" : "false");
}
function setServersVisible(open) {
state.serversVisible = !!open;
render();
}
function toggleServersVisible() {
setServersVisible(!state.serversVisible);
}
function startSelectedTargetFromMainButton() {
if (!state.target) {
state.lastStatus = "choose a server from the visible list first";
setAdvancedOpen(false);
render();
return;
}
startQueue({
host: state.target.host,
port: state.target.port,
name: state.target.name,
max: state.target.max,
ws: state.target.ws || `wss://${state.target.host}:${state.target.port}/server`,
});
}
function makeUI() {
if (!document.body || document.getElementById("dylko-dq-panel")) return;
const style = document.createElement("style");
style.id = "dylko-dq-style";
style.textContent = `
#dylko-dq-panel,
#dylko-dq-panel * {
box-sizing: border-box;
}
#dylko-dq-panel {
position: fixed;
top: 10px;
left: 50%;
transform: translateX(-50%);
width: 360px;
max-width: calc(100vw - 16px);
max-height: calc(100vh - 20px);
overflow: auto;
z-index: 999999;
color: white;
background: rgba(9, 10, 15, .92);
border: 1px solid rgba(255,255,255,.16);
border-radius: 14px;
font-family: Arial, sans-serif;
box-shadow: 0 8px 22px rgba(0,0,0,.36);
backdrop-filter: blur(8px);
}
#dylko-dq-panel button {
border: 0;
border-radius: 8px;
padding: 6px 8px;
cursor: pointer;
font-weight: 800;
color: white;
background: rgba(255,255,255,.14);
}
#dylko-dq-panel button:hover {
background: rgba(255,255,255,.22);
}
#dq-main {
padding: 8px;
text-align: center;
}
#dq-title {
font-size: 15px;
font-weight: 900;
letter-spacing: .2px;
margin-bottom: 1px;
}
#dq-credit {
font-size: 10px;
font-weight: 700;
opacity: .68;
margin-bottom: 5px;
}
#dq-status {
font-size: 12px;
line-height: 1.3;
margin-bottom: 5px;
opacity: .92;
}
#dq-target {
font-size: 12px;
line-height: 1.25;
opacity: .78;
word-break: break-word;
margin-bottom: 7px;
}
#dq-main-startstop {
width: 100%;
min-height: 36px;
font-size: 14px;
margin-bottom: 5px;
}
#dq-main-startstop.dq-start {
background: rgba(40, 165, 85, .88) !important;
}
#dq-main-startstop.dq-stop {
background: rgba(190, 55, 55, .86) !important;
}
#dq-toggle-servers {
width: 100%;
min-height: 30px;
font-size: 11px;
margin-bottom: 5px;
background: rgba(255,255,255,.12) !important;
}
#dq-advanced-toggle {
width: 100%;
margin-top: 2px;
background: transparent !important;
color: rgba(255,255,255,.66) !important;
padding: 4px 0 1px !important;
font-size: 11px;
}
#dq-advanced-panel {
display: none;
border-top: 1px solid rgba(255,255,255,.11);
padding: 8px;
max-height: 28vh;
overflow: auto;
text-align: left;
background: rgba(0,0,0,.18);
}
.dq-advanced-tag {
display: inline-block;
font-size: 9px;
font-weight: 900;
letter-spacing: .5px;
text-transform: uppercase;
color: rgba(255,255,255,.7);
border: 1px solid rgba(255,255,255,.18);
border-radius: 999px;
padding: 1px 6px;
margin-bottom: 5px;
}
.dq-muted {
font-size: 10px;
opacity: .7;
line-height: 1.25;
margin-bottom: 4px;
}
.dq-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5px;
margin-bottom: 5px;
}
#dq-advanced-panel #dq-scan {
width: 100%;
min-height: 30px;
font-size: 11px;
margin-bottom: 5px;
}
#dq-advanced-panel button {
font-size: 11px;
min-height: 30px;
}
#dq-server-note {
display: none;
}
#dq-list {
max-height: 250px;
overflow: auto;
border-top: 1px solid rgba(255,255,255,.14);
padding-top: 5px;
margin-top: 5px;
text-align: left;
}
.dq-lobby-row {
display: grid;
grid-template-columns: 1fr 38px 54px;
gap: 5px;
align-items: center;
margin: 3px 0;
padding: 5px;
border-radius: 8px;
font-size: 11px;
}
.dq-lobby-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dq-lobby-row button {
padding: 5px 6px !important;
font-size: 11px;
min-height: 28px;
}
#dq-toggle-hint {
position: absolute;
top: 8px;
left: 8px;
z-index: 2;
padding: 4px 7px;
border-radius: 7px;
border: 1px solid rgba(255,255,255,.16);
background: rgba(255,255,255,.07);
color: rgba(255,255,255,.62);
font-size: 10px;
font-weight: 800;
font-family: Arial, sans-serif;
line-height: 1;
cursor: pointer;
user-select: none;
}
#dq-toggle-hint:hover {
background: rgba(255,255,255,.13);
color: white;
}
`;
if (!document.getElementById("dylko-dq-style")) {
document.head?.appendChild(style);
}
const box = document.createElement("div");
box.id = "dylko-dq-panel";
box.innerHTML = `
<div id="dq-toggle-hint" title="Press backslash to show/hide Server Queue">\ to toggle</div>
<div id="dq-main">
<div id="dq-title">Server Queue</div>
<div id="dq-credit">created by Dylko</div>
<div id="dq-status">Status: loading</div>
<div id="dq-target">Target: none</div>
<button id="dq-main-startstop" class="dq-start">START</button>
<button id="dq-toggle-servers">Hide Servers</button>
<div id="dq-list"></div>
<button id="dq-advanced-toggle" aria-expanded="false">Advanced ▸</button>
</div>
<div id="dq-advanced-panel">
<div class="dq-advanced-tag">Advanced</div>
<button id="dq-scan">Refresh Servers</button>
<div class="dq-muted">
Trigger tools, diagnostics, and custom setup live here.
</div>
<div class="dq-row">
<button id="dq-capture">Capture Custom Trigger</button>
<button id="dq-clear-custom">Clear Custom</button>
</div>
<div class="dq-row">
<button id="dq-test-trigger">Test Trigger</button>
<button id="dq-show-trigger">Show Trigger Marker</button>
</div>
<div id="dq-trigger" class="dq-muted"></div>
<div id="dq-lastping" class="dq-muted"></div>
<div class="dq-muted">Queue polling: ${CFG.queuePollMs}ms.</div>
</div>
`;
document.body.appendChild(box);
document.getElementById("dq-toggle-hint").onclick = togglePanel;
document.getElementById("dq-main-startstop").onclick = () => {
if (state.queueing || state.armed) {
stopQueue();
} else {
startSelectedTargetFromMainButton();
}
};
document.getElementById("dq-toggle-servers").onclick = toggleServersVisible;
document.getElementById("dq-advanced-toggle").onclick = () => {
const panel = document.getElementById("dq-advanced-panel");
setAdvancedOpen(panel?.style.display === "none" || !panel?.style.display);
};
document.getElementById("dq-scan").onclick = scanLobbies;
document.getElementById("dq-capture").onclick = () => {
state.captureMode = true;
state.lastStatus = "click the custom trigger spot now";
render();
};
document.getElementById("dq-test-trigger").onclick = () => {
fireClick(getTriggerClick(), getTriggerLabel());
};
document.getElementById("dq-show-trigger").onclick = showTriggerMarker;
document.getElementById("dq-clear-custom").onclick = () => {
CFG.decoyClick = null;
save();
state.lastStatus = "cleared custom trigger; using default Play";
render();
};
state.uiReady = true;
render();
scanLobbies();
if (state.scanTimer) clearInterval(state.scanTimer);
state.scanTimer = setInterval(scanLobbies, CFG.scanIntervalMs);
}
function render() {
if (!state.uiReady) return;
const statusEl = document.getElementById("dq-status");
const targetEl = document.getElementById("dq-target");
const triggerEl = document.getElementById("dq-trigger");
const pingEl = document.getElementById("dq-lastping");
const listEl = document.getElementById("dq-list");
const serverNoteEl = document.getElementById("dq-server-note");
const panel = document.getElementById("dylko-dq-panel");
const startStopBtn = document.getElementById("dq-main-startstop");
const toggleServersBtn = document.getElementById("dq-toggle-servers");
if (!statusEl || !targetEl || !triggerEl || !listEl) return;
statusEl.textContent = `Status: ${state.lastStatus}`;
targetEl.textContent = state.target
? `Target: ${state.target.name || state.target.host}:${state.target.port}`
: "Target: none — pick a server below";
triggerEl.textContent = `Trigger: ${getTriggerLabel()} at ${pointText(getTriggerClick())}`;
if (startStopBtn) {
const active = state.queueing || state.armed;
startStopBtn.textContent = active ? "STOP" : "START";
startStopBtn.classList.toggle("dq-stop", active);
startStopBtn.classList.toggle("dq-start", !active);
}
if (toggleServersBtn) {
toggleServersBtn.textContent = state.serversVisible ? "Hide Servers" : "Show Servers";
}
if (serverNoteEl) {
serverNoteEl.style.display = state.serversVisible ? "block" : "none";
}
if (listEl) {
listEl.style.display = state.serversVisible ? "block" : "none";
}
if (pingEl) {
if (state.lastTargetPing?.error) {
pingEl.textContent = `Target ping: error ${state.lastTargetPing.error}`;
} else if (state.lastTargetPing) {
pingEl.textContent = `Target ping: ${state.lastTargetPing.current}/${state.lastTargetPing.max} ${
state.lastTargetPing.open ? "OPEN" : "FULL"
}`;
} else {
pingEl.textContent = "Target ping: none";
}
}
const rows = state.lobbies || [];
if (!state.serversVisible) {
listEl.innerHTML = "";
} else if (!rows.length) {
listEl.innerHTML = `<div class="dq-muted" style="text-align:center;margin:8px 0;">Scanning servers...</div>`;
} else {
listEl.innerHTML = rows
.map((lobby, i) => {
const players = lobby.error ? "ERR" : `${lobby.current}/${lobby.max}`;
const bg = lobby.error
? "rgba(80,80,80,.45)"
: lobby.full
? "rgba(120,40,40,.48)"
: "rgba(40,100,45,.35)";
const label = `${lobby.name || lobby.host}:${lobby.port}`;
const privacy = lobby.password ? " 🔒" : "";
const isTarget = state.target?.host === lobby.host && Number(state.target?.port) === Number(lobby.port);
const btnText = isTarget ? "Queued" : "Queue";
return `
<div class="dq-lobby-row" style="background:${bg}; ${isTarget ? "outline:1px solid rgba(255,255,255,.55);" : ""}">
<div class="dq-lobby-name" title="${lobby.host}:${lobby.port}">
${label}${privacy}
</div>
<div>${players}</div>
<button data-dq-queue="${i}">${btnText}</button>
</div>
`;
})
.join("");
}
for (const btn of listEl.querySelectorAll("[data-dq-queue]")) {
btn.onclick = () => {
const idx = Number(btn.getAttribute("data-dq-queue"));
const lobby = state.lobbies[idx];
if (lobby) startQueue(lobby);
};
}
if (panel) {
if (state.armed) {
panel.style.background = "rgba(0,80,25,.94)";
panel.style.border = "1px solid rgba(100,255,150,.8)";
} else if (state.queueing) {
panel.style.background = "rgba(75,50,0,.94)";
panel.style.border = "1px solid rgba(255,215,90,.75)";
} else if (state.captureMode) {
panel.style.background = "rgba(70,0,80,.94)";
panel.style.border = "1px solid rgba(255,120,255,.75)";
} else {
panel.style.background = "rgba(9, 10, 15, .92)";
panel.style.border = "1px solid rgba(255,255,255,.16)";
}
}
}
/******************************************************************
* PUBLIC DEBUG OBJECT
******************************************************************/
window.__DYLKO_DYNAMIC_QUEUE__ = {
CFG,
state,
scanLobbies,
startQueue,
stopQueue,
completeQueue,
checkTarget,
armRedirect,
fireClick,
getTriggerClick,
getTriggerLabel,
getCanvas,
resolveCanvasPoint,
showTriggerMarker,
setServersVisible,
toggleServersVisible,
hidePanel,
showPanel,
togglePanel,
targetPingUrl,
targetWsUrl,
save,
};
/******************************************************************
* BOOT
******************************************************************/
const uiTimer = setInterval(() => {
if (document.body) {
clearInterval(uiTimer);
makeUI();
}
}, 100);
log("installed");
})();