您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Shows open races while waiting for yours to start
// ==UserScript== // @name Torn Open Races Display // @namespace underko.torn.scripts.racing // @version 0.1 // @author underko[3362751] // @description Shows open races while waiting for yours to start // @match *.torn.com/loader.php?sid=racing* // @match *.torn.com/page.php?sid=racing* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @connect api.torn.com // @license MIT // ==/UserScript== (function () { 'use strict'; const FETCH_INTERVAL = 5_000; const RACE_COUNT = 100; const TRACKS = { 6: "Uptown", 7: "Withdrawal", 8: "Underdog", 9: "Parkland", 10: "Docks", 11: "Commerce", 12: "Two Islands", 15: "Industrial", 16: "Vector", 17: "Mudpit", 18: "Hammerhead", 19: "Sewage", 20: "Meltdown", 21: "Speedway", 23: "Stone Park", 24: "Convict" }; // Single hidden element reused for html escpaed string decoding const htmlEntityDecoder = document.createElement("textarea"); GM_registerMenuCommand("Set Torn API Key", () => { const key = prompt("Enter your public Torn API key", GM_getValue("tornApiKey", "")); if (key) { GM_setValue("tornApiKey", key); } }); const API_KEY = GM_getValue("tornApiKey", ""); if (!API_KEY) { console.warn("No API key set. Use Tampermonkey menu to set one."); return; } function formatTime(seconds) { if (seconds >= 3600 || seconds <= -3600) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; return `${hours}h ${minutes}m ${secs}s`; } else if (seconds >= 60 || seconds <= -60) { const minutes = Math.floor(seconds / 60); const secs = seconds % 60; return `${minutes}m ${secs}s`; } else { return `${seconds}s`; } } async function fetchAllRaces(limit) { const startFromUnix = Math.floor(Date.now() / 1000 - 3600); let races = []; let url = `https://api.torn.com/v2/racing/races?cat=custom&limit=100&sort=desc&from=${startFromUnix}&key=${API_KEY}`; let fetchedCount = 0; while (url && fetchedCount < limit) { const pageData = await new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url, onload: (res) => { try { const data = JSON.parse(res.responseText); resolve(data); } catch (err) { console.error("[TornRaces] Failed to parse API response", err); resolve({}); } }, onerror: (err) => { console.error("[TornRaces] API request error:", err); resolve({}); } }); }); if (pageData.races && pageData.races.length) { races.push(...pageData.races); fetchedCount += pageData.races.length; } const prev = pageData?._metadata?.links?.prev || null; if (prev) { // Append API key if not present in the meta url url = prev.includes("&key=") ? prev : `${prev}&key=${API_KEY}`; } else { url = null; } } return races.slice(0, limit); } function decodeHtml(text) { htmlEntityDecoder.innerHTML = text; return htmlEntityDecoder.value; } function renderTable(races) { const container = document.querySelector(".car-selected-wrap.m-top10"); if (!container) return; const clearEl = container.querySelector(".clear"); let tableWrap = container.querySelector("#openRacesTableWrap"); if (!tableWrap) { tableWrap = document.createElement("div"); tableWrap.className = "left"; tableWrap.id = "openRacesTableWrap"; const clearEl = Array.from(container.children) .find(el => el.classList.contains("clear")); if (clearEl) { container.insertBefore(tableWrap, clearEl); } else { container.appendChild(tableWrap); } } let html = ` <style> #openRacesTableWrap table { border-collapse: collapse; width: 233px; background-color: rgba(0,0,0,0.3); color: #ddd; font-size: 12px; } #openRacesTableWrap th { background-color: rgba(255,255,255,0.05); padding: 6px 8px; text-align: left; font-weight: bold; color: #fff; } #openRacesTableWrap td { padding: 4px 8px; border-bottom: 1px solid rgba(255,255,255,0.1); } #openRacesTableWrap tr:hover { background-color: rgba(255,255,255,0.05); } </style> <table> <tr> <th>Track</th> <th>Racers</th> <th>Laps</th> <th>Start</th> </tr>`; const now = Math.floor(Date.now() / 1000); races.sort((a, b) => { const aStart = a.schedule?.start || 0; const bStart = b.schedule?.start || 0; return aStart - bStart; }); races.forEach(r => { const timeUntilStart = r.schedule?.start ? r.schedule.start - now : 0; const trackName = TRACKS[r.track_id].slice(0, 5) || `Unknown (${r.track_id})`; if (timeUntilStart >= 3600) return; const decodedTitle = decodeHtml(r.title); html += `<tr title="${decodedTitle.replace(/"/g, """)}"> <td style="color: #ddd">${trackName}</td> <td style="color: #ddd">${r.participants.current}/${r.participants.maximum}</td> <td style="color: #ddd">${r.laps}</td> <td style="color: #ddd">${timeUntilStart < 0 ? "waiting" : formatTime(timeUntilStart)}</td> </tr>`; }); html += `</table>`; tableWrap.innerHTML = html; } function isWaitingForRace() { return document.querySelector(".pd-position") !== null; } function isRaceEligible(race) { const { status, schedule, requirements, participants } = race; // Exclude finished or already ended races if (status === "finished" || schedule.end != null) return false; // Exclude password-protected races if (requirements.requires_password === true) return false; // Exclude races with restricted car class (allow only A or null - no restrictions) if (requirements.car_class !== null && requirements.car_class !== "A") return false; // Exclude races that require stock cars if (requirements.requires_stock_car === true) return false; // Exclude races that are already full if (participants.current >= participants.maximum) return false; // Exclude races tied to a specific car item // Needs logic to allow permitted track and car combinations // if (requirements.car_item_id != null) return false; return true; } let lastFetch = 0; const observer = new MutationObserver(() => { const now = Date.now(); if (now - lastFetch >= FETCH_INTERVAL) { lastFetch = now; if (isWaitingForRace()) { fetchAllRaces(RACE_COUNT).then((allRaces) => { const openRaces = allRaces.filter(isRaceEligible); renderTable(openRaces); }); } } }); observer.observe(document.body, { childList: true, subtree: true }); })();