// ==UserScript== // @name Torn: Racing enhancements (Compatible with Torn PDA) // @namespace ltcabel.racing_enhancements // @version 2.0.6 // @description Show car's current speed, precise skill, official race penalty, racing skill of others and race car skins. // @author Lugburz, modified by Reshula & LtCabel // @match https://www.torn.com/loader.php?sid=racing* // @match https://www.torn.com/page.php?sid=racing* // @updateURL https://raw.githubusercontent.com/LtCabel/torn-userscripts/master/racing_enhancements_pda_compatible.user.js // @downloadURL https://raw.githubusercontent.com/LtCabel/torn-userscripts/master/racing_enhancements_pda_compatible.user.js // @connect api.torn.com // @connect race-skins.brainslug.nl // @grant GM_setValue // @grant GM_getValue // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_addStyle // @run-at document-start // ==/UserScript== console.log("[Racing Enhancements PDA] starting"); // -------------------- Toggles / settings -------------------- const NOTIFICATIONS = GM_getValue('showNotifChk') != 0; const SHOW_RESULTS = GM_getValue('showResultsChk') != 0; const SHOW_SPEED = GM_getValue('showSpeedChk') != 0; const SHOW_POSITION_ICONS = GM_getValue('showPositionIconChk') != 0; let FETCH_RS = !!(GM_getValue('apiKey') && GM_getValue('apiKey').length > 0); const SHOW_SKINS = GM_getValue('showSkinsChk') != 0; // -------------------- Skins config -------------------- const SKIN_AWARDS = 'https://race-skins.brainslug.nl/custom/data'; const SKIN_IMAGE = id => `https://race-skins.brainslug.nl/assets/${id}`; const userID = getUserIdFromCookie(); let RACE_ID = '*'; const period = 1000; let last_compl = -1.0; let x = 0; let penaltyNotif = 0; let lastRenderedRaceKey = null; let racingPageObserver = null; let observedLeaderboard = null; let lastSeenLeaderboardSignature = ''; let lastSeenPageHref = location.href; const RS_CACHE_KEY = 'racingSkillCachePersisted'; const RS_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // -------------------- Helpers -------------------- function maybeClear() { if (x != 0 ) { clearInterval(x); last_compl = -1.0; x = 0; } } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function updateRsEnabledClass() { const rsEnabled = !!(GM_getValue('apiKey') && GM_getValue('apiKey').length > 0); document.documentElement.classList.toggle('racing-rs-enabled', rsEnabled); } // -------------------- Racing Skill cache -------------------- const racingSkillFetchInFlight = new Set(); const racingSkillCacheByDriverId = new Map(); let updating = false; let updateDriversListQueued = false; let rsFetchInProgress = false; // ---- RS persistent cache helpers ---- function loadPersistedRacingSkillCache() { const persisted = GM_getValue(RS_CACHE_KEY, {}); const now = Date.now(); for (const [driverId, entry] of Object.entries(persisted)) { if (!entry || typeof entry.skill === 'undefined' || typeof entry.ts !== 'number') continue; if ((now - entry.ts) > RS_CACHE_MAX_AGE_MS) continue; racingSkillCacheByDriverId.set(+driverId, entry.skill); } } function persistRacingSkill(driverId, skill) { const persisted = GM_getValue(RS_CACHE_KEY, {}); persisted[String(driverId)] = { skill: skill, ts: Date.now() }; GM_setValue(RS_CACHE_KEY, persisted); } // Load cache immediately after defining helpers loadPersistedRacingSkillCache(); updateRsEnabledClass(); function queueUpdateDriversList() { if (updateDriversListQueued) return; updateDriversListQueued = true; requestAnimationFrame(() => { updateDriversListQueued = false; updateDriversList(); }); } function getLeaderboardSignature(driversList) { if (!driversList) return ''; return Array.from(driversList.querySelectorAll('.driver-item')) .map(driver => getDriverId(driver)) .join(','); } function resetRaceUiState() { lastRenderedRaceKey = null; observedLeaderboard = null; lastSeenLeaderboardSignature = ''; _skinned = false; updating = false; const updatingNode = document.getElementById('updating'); if (updatingNode) updatingNode.remove(); const raceLink = document.getElementById('raceLink'); if (raceLink) raceLink.remove(); } function ensureLeaderboardWatcher() { const driversList = document.getElementById('leaderBoard'); if (!driversList) return; if (observedLeaderboard !== driversList) { observedLeaderboard = driversList; watchForDriversListContentChanges(driversList); lastSeenLeaderboardSignature = ''; } const signature = getLeaderboardSignature(driversList); if (signature && signature !== lastSeenLeaderboardSignature) { lastSeenLeaderboardSignature = signature; queueUpdateDriversList(); } } function ensureRacingPageObserver() { if (racingPageObserver) return; racingPageObserver = new MutationObserver(() => { if (!location.href.includes('sid=racing')) return; if (location.href !== lastSeenPageHref) { lastSeenPageHref = location.href; resetRaceUiState(); } ensureLeaderboardWatcher(); }); racingPageObserver.observe(document.body, { childList: true, subtree: true }); } async function updateDriversList() { const driversList = document.getElementById('leaderBoard'); if (driversList === null) { observedLeaderboard = null; lastSeenLeaderboardSignature = ''; return; } if (updating) return; FETCH_RS = !!(GM_getValue('apiKey') && GM_getValue('apiKey').length > 0); watchForDriversListContentChanges(driversList); const driverIds = getDriverIds(driversList); if (!driverIds || !driverIds.length) return; updating = true; $('#updating').size() < 1 && $('#racingupdatesnew').prepend('
Updating drivers\' RS and skins...
'); let racingSkins = {}; const driverNodes = driversList.querySelectorAll('.driver-item'); function paintDriverRow(driver) { const driverId = getDriverId(driver); const nameDiv = driver.querySelector('.name'); if (!nameDiv) return; // RS: show immediately if already cached if (FETCH_RS) { const cachedSkill = racingSkillCacheByDriverId.get(driverId); let rsSpan = nameDiv.querySelector('.rs-display'); if (cachedSkill) { nameDiv.style.position = 'relative'; const rsText = `RS:${cachedSkill}`; if (!rsSpan) { rsSpan = document.createElement('span'); rsSpan.className = 'rs-display'; rsSpan.textContent = rsText; nameDiv.appendChild(rsSpan); } else if (rsSpan.textContent !== rsText) { rsSpan.textContent = rsText; } } else if (rsSpan) { rsSpan.remove(); } } else { const rsSpan = nameDiv.querySelector('.rs-display'); if (rsSpan) rsSpan.remove(); } // Skin if (SHOW_SKINS && racingSkins[driverId]) { const carImg = driver.querySelector('.car img'); if (carImg) { const carId = carImg.getAttribute('src').replace(/[^0-9]*/g, ''); const skinId = racingSkins[driverId][carId]; if (skinId) { const skinSrc = SKIN_IMAGE(skinId); if (carImg.getAttribute('src') !== skinSrc) { carImg.setAttribute('src', skinSrc); } if (driverId == userID) skinCarSidebar(skinId); } } } } // First pass: paint immediately using cache + skins for (const driver of driverNodes) { paintDriverRow(driver); } if (SHOW_SKINS) { getRacingSkinOwners(driverIds) .then(skins => { racingSkins = skins || {}; const freshDriversList = document.getElementById('leaderBoard'); if (!freshDriversList) return; const freshDriverNodes = freshDriversList.querySelectorAll('.driver-item'); for (const driver of freshDriverNodes) { paintDriverRow(driver); } }) .catch(err => { console.error('[Racing Enhancements PDA] Skin fetch failed', err); }); } if (FETCH_RS && !rsFetchInProgress) { const driverIdsToFetch = driverIds.filter(driverId => !racingSkillCacheByDriverId.has(driverId) && !racingSkillFetchInFlight.has(driverId) ); if (driverIdsToFetch.length) { rsFetchInProgress = true; driverIdsToFetch.forEach(driverId => racingSkillFetchInFlight.add(driverId)); // Allow future DOM refreshes to repaint cached values immediately updating = false; getRacingSkillForDrivers(driverIdsToFetch, (fetchedDriverId) => { const freshDriversList = document.getElementById('leaderBoard'); if (!freshDriversList) return; const freshDriverNodes = freshDriversList.querySelectorAll('.driver-item'); for (const driver of freshDriverNodes) { if (getDriverId(driver) === fetchedDriverId) { paintDriverRow(driver); break; } } }) .catch(err => { console.error('[Racing Enhancements PDA] RS background fetch failed', err); driverIdsToFetch.forEach(driverId => racingSkillFetchInFlight.delete(driverId)); }) .finally(() => { rsFetchInProgress = false; if (racingSkillFetchInFlight.size === 0) { $('#updating').size() > 0 && $('#updating').remove(); } }); return; } } updating = false; $('#updating').size() > 0 && $('#updating').remove(); } function watchForDriversListContentChanges(driversList) { if (!driversList) return; if (driversList.dataset.hasWatcher !== undefined) return; new MutationObserver(() => { queueUpdateDriversList(); }).observe(driversList, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'srcset', 'class'] }); driversList.dataset.hasWatcher = 'true'; } function getDriverIds(driversList) { return Array.from(driversList.querySelectorAll('.driver-item')).map(driver => getDriverId(driver)); } function getDriverId(driverUl) { return +driverUl.closest('li').id.substr(4); } let racersCount = 0; async function getRacingSkillForDrivers(driverIds, onDriverFetched) { racersCount = 0; const driverIdsToFetch = driverIds.filter(driverId => !racingSkillCacheByDriverId.has(driverId) ); for (const driverId of driverIdsToFetch) { const json = await fetchRacingSkillForDrivers(driverId); if (json && json.error) { $('#racingupdatesnew').prepend(`
API error: ${JSON.stringify(json.error)}
`); racingSkillCacheByDriverId.delete(+driverId); racingSkillFetchInFlight.delete(+driverId); driverIdsToFetch.forEach(id => racingSkillFetchInFlight.delete(+id)); break; } const fetchedSkill = json && json.personalstats && json.personalstats.racingskill ? json.personalstats.racingskill : 'N/A'; racingSkillCacheByDriverId.set(+driverId, fetchedSkill); if (fetchedSkill !== 'N/A') { persistRacingSkill(+driverId, fetchedSkill); } if (onDriverFetched) { try { onDriverFetched(+driverId); } catch (err) { console.error('[Racing Enhancements PDA] onDriverFetched callback failed', err); } } racingSkillFetchInFlight.delete(+driverId); await sleep(1300); } const resultHash = {}; for (const driverId of driverIds) { const skill = racingSkillCacheByDriverId.get(driverId); if (!!skill) resultHash[driverId] = skill; } return resultHash; } let _skinOwnerCache = null; async function getRacingSkinOwners(driverIds) { function filterSkins(skins) { let result = {}; for (const driverId of driverIds) { if (skins?.['*']?.[driverId]) result[driverId] = skins['*'][driverId]; if (skins?.[RACE_ID]?.[driverId]) result[driverId] = skins[RACE_ID][driverId]; } return result; } return new Promise(resolve => { if (!!_skinOwnerCache) return resolve(_skinOwnerCache); GM_xmlhttpRequest({ method: 'GET', url: SKIN_AWARDS, headers: {'Content-Type': 'application/json'}, onload: ({responseText}) => { _skinOwnerCache = JSON.parse(responseText); resolve(_skinOwnerCache); }, onerror: (err) => { console.error(err); resolve({}); }, }); }).then(filterSkins); } let _skinned = false; function skinCarSidebar(carSkin) { const carSelected = document.querySelector('.car-selected'); if (!carSelected) return; const tornItem = carSelected.querySelector('.torn-item'); if (!tornItem) return; if (tornItem !== _skinned) { try { tornItem.setAttribute('src', SKIN_IMAGE(carSkin)); tornItem.setAttribute('srcset', SKIN_IMAGE(carSkin)); tornItem.style.display = 'block'; tornItem.style.opacity = 1; const canvas = carSelected.querySelector('canvas'); if (!!canvas) canvas.style.display = 'none'; _skinned = tornItem; } catch (err) { console.error(err); } } } // -------------------- Utility -------------------- function getUserIdFromCookie() { const userIdString = document.cookie.split(';') .map(entry => entry.trim()) .find(entry => entry.indexOf('uid=') === 0) ?.replace('uid=', '') || '0'; return parseInt(userIdString, 10); } function pad(num, size) { return ('000000000' + num).substr(-size); } function formatTime(date) { return pad(date.getUTCHours(), 2) + ':' + pad(date.getUTCMinutes(), 2) + ':' + pad(date.getUTCSeconds(), 2); } function formatTimeMsec(msec, alwaysShowHours = false) { const hours = Math.floor((msec % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((msec % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((msec % (1000 * 60)) / 1000); const mseconds = Math.floor(msec % 1000); return (alwaysShowHours ? pad(hours, 2) + ":" : (hours > 0 ? hours + ":" : '')) + (hours > 0 || minutes > 0 ? pad(minutes, 2) + ":" : '') + pad(seconds, 2) + "." + pad(mseconds, 3); } function formatTimeSecWithLetters(msec) { const hours = Math.floor((msec % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((msec % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((msec % (1000 * 60)) / 1000); return (hours > 0 ? hours + "h " : '') + (hours > 0 || minutes > 0 ? minutes + "min " : '') + seconds + "s"; } function decode64(input) { var output = ''; var chr1, chr2, chr3 = ''; var enc1, enc2, enc3, enc4 = ''; var i = 0; var keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; var base64test = /[^A-Za-z0-9\+\/\=]/g; if (base64test.exec(input)) { console.log('Invalid base64 characters detected. Expect possible decode issues.'); } input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ''); do { enc1 = keyStr.indexOf(input.charAt(i++)); enc2 = keyStr.indexOf(input.charAt(i++)); enc3 = keyStr.indexOf(input.charAt(i++)); enc4 = keyStr.indexOf(input.charAt(i++)); chr1 = (enc1 << 2) | (enc2 >> 4); chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); chr3 = ((enc3 & 3) << 6) | enc4; output = output + String.fromCharCode(chr1); if (enc3 != 64) output = output + String.fromCharCode(chr2); if (enc4 != 64) output = output + String.fromCharCode(chr3); chr1 = chr2 = chr3 = ''; enc1 = enc2 = enc3 = enc4 = ''; } while (i < input.length); return unescape(output); } // -------------------- API -------------------- function fetchRacingSkillForDrivers(driverIds) { const apiKey = GM_getValue('apiKey'); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: `https://api.torn.com/user/${driverIds}?selections=personalstats&comment=RacingUiUx&key=${apiKey}`, headers: { 'Content-Type': 'application/json' }, onload: (response) => { try { resolve(JSON.parse(response.responseText)); } catch(err) { reject(err); } }, onerror: (err) => { reject(err); } }); }); } // -------------------- UI bits -------------------- function showSpeed() { if (!SHOW_SPEED || $('#racingdetails').size() < 1 || $('#racingdetails').find('#speed_mph').size() > 0) return; $('#racingdetails').find('li.pd-name').each(function() { if ($(this).text() == 'Name:') $(this).hide(); if ($(this).text() == 'Position:') $(this).text('Pos:'); if ($(this).text() == 'Completion:') $(this).text('Compl:'); }); $('#racingdetails').append('
  • '); maybeClear(); x = setInterval(function() { if ($('#racingupdatesnew').find('div.track-info').size() < 1) { maybeClear(); return; } let laps = $('#racingupdatesnew').find('div.title-black').text().split(" - ")[1].split(" ")[0]; let len = $('#racingupdatesnew').find('div.track-info').attr('data-length').replace('mi', ''); let compl = $('#racingdetails').find('li.pd-completion').text().replace('%', ''); if (last_compl >= 0) { let speed = (compl - last_compl) / 100 * laps * len * 60 * 60 * 1000 / period; $('#speed_mph').text(speed.toFixed(2) + 'mph'); } last_compl = compl; }, period); } function showPenalty() { if ($('#racingAdditionalContainer').find('div.msg.right-round').size() > 0 && $('#racingAdditionalContainer').find('div.msg.right-round').text().trim().startsWith('You have recently left')) { const penalty = GM_getValue('leavepenalty') * 1000; const now = Date.now(); if (penalty > now) { const date = new Date(penalty); $('#racingAdditionalContainer').find('div.msg.right-round').text('You may join an official race at ' + formatTime(date) + '.'); } } } function checkPenalty() { if (penaltyNotif) clearTimeout(penaltyNotif); const leavepenalty = GM_getValue('leavepenalty'); const penaltyLeft = leavepenalty * 1000 - Date.now(); if (NOTIFICATIONS && penaltyLeft > 0) { penaltyNotif = setTimeout(function() { GM_notification("You may join an official race now.", "Torn: Racing enhancements"); }, penaltyLeft); } } function updateSkill(level) { const skill = Number(level).toFixed(5); const prev = GM_getValue('racinglevel'); const now = Date.now(); const lastDaysRs = GM_getValue('lastDaysRs'); if (lastDaysRs && lastDaysRs.includes(':')) { const ts = lastDaysRs.split(':')[0]; const dateTs = new Date(); dateTs.setTime(ts); if ((new Date(now).setUTCHours(0,0,0,0)) - (dateTs.setUTCHours(0,0,0,0)) >= 24*60*60*1000) { GM_setValue('lastDaysRs', `${now}:${prev ? prev : skill}`); } } else { GM_setValue('lastDaysRs', `${now}:${prev ? prev : skill}`); } if (prev !== "undefined" && typeof prev !== "undefined" && level > prev) { const inc = Number(level - prev).toFixed(5); if (NOTIFICATIONS) GM_notification("Your racing skill has increased by " + inc + "!", "Torn: Racing enhancements"); GM_setValue('lastRSincrement', inc); } GM_setValue('racinglevel', level); if ($('#racingMainContainer').find('div.skill').size() > 0) { if ($("#sidebarroot").find("a[class^='menu-value']").size() > 0) { $('#racingMainContainer').find('div.skill-desc').css('left', '5px'); $('#racingMainContainer').find('div.skill').css('left', '5px').text(skill); } else { $('#racingMainContainer').find('div.skill').text(skill); } const lastInc = GM_getValue('lastRSincrement'); if (lastInc) { $('div.skill').find('.last-gain').remove(); $('div.skill').append(`
    Last gain: ${lastInc}
    `); } } } function updatePoints(pointsearned) { const now = Date.now(); const lastDaysPoints = GM_getValue('lastDaysPoints'); const prev = GM_getValue('pointsearned'); if (lastDaysPoints && lastDaysPoints.includes(':')) { const ts = lastDaysPoints.split(':')[0]; const dateTs = new Date(); dateTs.setTime(ts); if ((new Date(now).setUTCHours(0,0,0,0)) - (dateTs.setUTCHours(0,0,0,0)) >= 24*60*60*1000) { GM_setValue('lastDaysPoints', `${now}:${prev ? prev : pointsearned}`); } } else { GM_setValue('lastDaysPoints', `${now}:${prev ? prev : pointsearned}`); } GM_setValue('pointsearned', pointsearned); } // -------------------- Results -------------------- function parseRacingData(data) { // no sidebar in phone mode const my_name = $("#sidebarroot").find("a[class^='menu-value']").html() || data.user.playername; updateSkill(data.user.racinglevel); updatePoints(data.user.pointsearned); const leavepenalty = data.user.leavepenalty; GM_setValue('leavepenalty', leavepenalty); checkPenalty(); // race link RACE_ID = data.raceID; const raceUrl = `https://www.torn.com/page.php?sid=racing&tab=log&raceID=${RACE_ID}`; if ($('#raceLink').size() < 1) { const raceLink = $('Copy link to the race'); raceLink.on('click', async function(e) { e.preventDefault(); try { await navigator.clipboard.writeText(raceUrl); if (typeof GM_notification === 'function') { GM_notification("Race link copied to clipboard!", "Torn Racing"); } else { alert("Race link copied to clipboard!"); } } catch (err) { prompt("Copy this race link manually:", raceUrl); } }); raceLink.insertAfter('#racingEnhSettings'); } else { $('#raceLink') .text('Copy link to the race') .attr('href', '#') .off('click') .on('click', async function(e) { e.preventDefault(); try { await navigator.clipboard.writeText(raceUrl); if (typeof GM_notification === 'function') { GM_notification("Race link copied to clipboard!", "Torn Racing"); } else { alert("Race link copied to clipboard!"); } } catch (err) { prompt("Copy this race link manually:", raceUrl); } }); } // results when race finished if (data.timeData.status >= 3) { const raceKey = `${data.raceID}:${data.timeData.status}:${data.timeData.timeEnded}`; const resultsAlreadyInDom = !!document.querySelector('#leaderBoard .name-scroll'); if (lastRenderedRaceKey === raceKey && resultsAlreadyInDom) return; lastRenderedRaceKey = raceKey; const carsData = data.raceData.cars; const carInfo = data.raceData.carInfo; const trackIntervals = data.raceData.trackData.intervals.length; let results = [], crashes = []; for (const playername in carsData) { const userId = carInfo[playername].userID; const intervals = decode64(carsData[playername]).split(','); let raceTime = 0; let bestLap = 9999999999; if (intervals.length / trackIntervals == data.laps) { for (let i = 0; i < data.laps; i++) { let lapTime = 0; for (let j = 0; j < trackIntervals; j++) { lapTime += Number(intervals[i * trackIntervals + j]); } bestLap = Math.min(bestLap, lapTime); raceTime += Number(lapTime); } results.push([playername, userId, raceTime, bestLap]); } else { crashes.push([playername, userId, 'crashed']); } } results.sort(compare); addExportButton(results, crashes, my_name, data.raceID, data.timeData.timeEnded); if (SHOW_RESULTS) { showResults(results); showResults(crashes, results.length); queueUpdateDriversList(); } } } function compare(a, b) { if (a[2] > b[2]) return 1; if (b[2] > a[2]) return -1; return 0; } function showResults(results, start = 0) { const board = document.getElementById('leaderBoard'); if (!board) return; // Build row map once const rowByUserId = {}; board.querySelectorAll(':scope > li').forEach(row => { const m = (row.id || '').match(/(\d+)/); if (!m) return; const nameLi = row.querySelector('li.name'); if (nameLi) rowByUserId[+m[1]] = nameLi; }); for (let i = 0; i < results.length; i++) { const userId = +results[i][1]; const nameLi = rowByUserId[userId]; if (!nameLi) continue; const name = results[i][0]; const p = i + start + 1; const position = p === 1 ? 'gold' : p === 2 ? 'silver' : p === 3 ? 'bronze' : ''; const place = (p != 11 && p % 10 == 1) ? p + 'st' : (p != 12 && p % 10 == 2) ? p + 'nd' : (p != 13 && p % 10 == 3) ? p + 'rd' : p + 'th'; const result = (typeof results[i][2] === 'number') ? formatTimeMsec(results[i][2] * 1000) : results[i][2]; const bestLap = results[i][3] ? ` (best: ${formatTimeMsec(results[i][3] * 1000)})` : ''; const iconHtml = (SHOW_POSITION_ICONS && position) ? `` : ''; const textHtml = `${iconHtml}${name} ${place} ${result}${bestLap}`; let scrollSpan = nameLi.querySelector('.name-scroll'); let rsBadge = nameLi.querySelector('.rs-display'); if (!scrollSpan) { scrollSpan = document.createElement('span'); scrollSpan.className = 'name-scroll'; // Move all non-RS children into name-scroll instead of wiping the node const children = Array.from(nameLi.childNodes).filter(node => { return !(node.nodeType === 1 && node.classList.contains('rs-display')); }); for (const child of children) { scrollSpan.appendChild(child); } nameLi.insertBefore(scrollSpan, rsBadge || null); } if (scrollSpan.innerHTML !== textHtml) { scrollSpan.innerHTML = textHtml; } } } function addSettingsDiv() { if ($("#racingupdatesnew").size() > 0 && $('#racingEnhSettings').size() < 1) { const div = '
    ' + 'Settings' + '
    '; $('#racingupdatesnew').prepend(div); $('#racingEnhSettingsContainer').find('input[type=checkbox]').each(function() { $(this).prop('checked', GM_getValue($(this).attr('id')) != 0); }); $('#apiKey').val(GM_getValue('apiKey')); $('#racingEnhSettings').on('click', () => $('#racingEnhSettingsContainer').toggle()); $('#racingEnhSettingsContainer').on('click', 'input', function() { const id = $(this).attr('id'); const checked = $(this).prop('checked'); GM_setValue(id, checked ? 1 : 0); }); $('#saveApiKey').click(event => { event.preventDefault(); event.stopPropagation(); GM_setValue('apiKey', $('#apiKey').val()); FETCH_RS = !!(GM_getValue('apiKey') && GM_getValue('apiKey').length > 0); updateRsEnabledClass(); // Immediately remove RS if disabled if (!FETCH_RS) { document.querySelectorAll('.rs-display').forEach(el => el.remove()); } queueUpdateDriversList(); }); } } function addExportButton(results, crashes, my_name, race_id, time_ended) { if ($("#racingupdatesnew").size() > 0 && $('#downloadAsCsv').size() < 1 && $('#copyCsvBtn').size() < 1) { let csv = 'position,name,id,time,best_lap\n'; for (let i = 0; i < results.length; i++) { const timeStr = formatTimeMsec(results[i][2] * 1000, true); const bestLap = formatTimeMsec(results[i][3] * 1000); csv += [i+1, results[i][0], results[i][1], timeStr, bestLap].join(',') + '\n'; } for (let i = 0; i < crashes.length; i++) { csv += [results.length + i + 1, crashes[i][0], crashes[i][1], crashes[i][2], ''].join(',') + '\n'; } const timeE = new Date(); timeE.setTime(time_ended * 1000); const fileName = `${timeE.getUTCFullYear()}${pad(timeE.getUTCMonth() + 1, 2)}${pad(timeE.getUTCDate(), 2)}-race_${race_id}.csv`; const isWebView = /TornPDA|wv;|; wv|FBAN|FBAV|Line\/|Instagram/i.test(navigator.userAgent); if (!isWebView) { // Desktop flow: download CSV const myblob = new Blob([csv], { type: 'text/csv' }); const myurl = window.URL.createObjectURL(myblob); const exportBtn = `Download results as CSV`; $(exportBtn).insertAfter('#racingEnhSettings'); } else { // Torn PDA flow: copy CSV const copyBtn = $('Copy results as CSV'); copyBtn.on('click', function(e) { e.preventDefault(); navigator.clipboard.writeText(csv).then(() => { alert("Race results copied to clipboard!"); }).catch(() => { prompt("Copy race results manually:", csv); }); }); $('#racingEnhSettings').after(copyBtn); } } } function addPlaybackButton() { if ($("#racingupdatesnew").size() > 0 && $('div.race-player-container').size() < 1) { $('div.drivers-list > div.cont-black').prepend( `
    x1
    00:00:00
    `); } } function displayDailyGains() { $('#mainContainer').find('div.content').find('span.label').each((i, el) => { if ($(el).text().includes('Racing')) { const racingLi = $(el).parent().parent(); // RS gain const desc = $(racingLi).find('span.desc'); if ($(desc).size() > 0) { const rsText = $(desc).text(); const currentRs = GM_getValue('racinglevel'); const lastDaysRs = GM_getValue('lastDaysRs'); const oldRs = lastDaysRs && lastDaysRs.includes(':') ? lastDaysRs.split(':')[1] : undefined; $(desc).text(`${rsText} / Daily gain: ${currentRs && oldRs ? (1*currentRs - 1*oldRs).toFixed(5) : 'N/A'}`); $(desc).attr('title', 'Daily gain: How much your racing skill has increased since yesterday.'); } // points gain const lastDaysPoints = GM_getValue('lastDaysPoints'); const currentPoints = GM_getValue('pointsearned'); const oldPoints = lastDaysPoints && lastDaysPoints.includes(':') ? lastDaysPoints.split(':')[1] : undefined; let pointsTitle = 'Racing points earned: How many points you have earned throughout your career.'; for (const x of [ {points: 25, class: 'D'}, {points: 100, class: 'C'}, {points: 250, class: 'B'}, {points: 475, class: 'A'} ]) { if (currentPoints && currentPoints < x.points) pointsTitle += `
    Till class ${x.class}: ${1*x.points - 1*currentPoints}`; } const pointsLi = `
  • Racing points earned ${currentPoints ? currentPoints : 'N/A'} / Daily gain: ${currentPoints && oldPoints ? 1*currentPoints - 1*oldPoints : 'N/A'}
  • `; $(pointsLi).insertAfter(racingLi); return false; } }); } // -------------------- PDA-safe ajax hook (with retry) -------------------- function ajax(callback) { try { $(document).ajaxComplete((event, xhr, settings) => { if (xhr.readyState > 3 && xhr.status == 200) { let url = settings.url; if (url.indexOf("torn.com/") < 0) url = "torn.com" + (url.startsWith("/") ? "" : "/") + url; const page = url.substring(url.indexOf("torn.com/") + "torn.com/".length, url.indexOf(".php")); callback(page, xhr, settings); } }); } catch (e) { // keep trying until jQuery is ready in PDA if (e instanceof ReferenceError) { setTimeout(ajax, 250, callback); } else { console.warn('[Racing Enhancements PDA] ajax hook error', e); } } } function showCsvModal(csvText, fileName) { // If already present, just update + show let modal = document.getElementById('csv-viewer-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'csv-viewer-modal'; modal.innerHTML = `
    ${fileName}
    
          
    `; document.body.appendChild(modal); // Wire buttons modal.querySelector('#csv-close-btn').addEventListener('click', () => { modal.style.display = 'none'; }); modal.querySelector('.csv-backdrop').addEventListener('click', () => { modal.style.display = 'none'; }); modal.querySelector('#csv-copy-btn').addEventListener('click', async () => { try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(csvText); alert('CSV copied to clipboard.'); } else { prompt('Copy the CSV:', csvText); } } catch { prompt('Copy the CSV:', csvText); } }); // Optional: system share on devices that support it if (navigator.share) { const shareBtn = modal.querySelector('#csv-share-btn'); shareBtn.style.display = ''; shareBtn.addEventListener('click', async () => { try { await navigator.share({ title: fileName, text: csvText }); } catch(_) {} }); } } // Update contents + show modal.querySelector('.csv-title').textContent = fileName; modal.querySelector('#csv-body').textContent = csvText; modal.style.display = 'block'; } // -------------------- Main wiring -------------------- 'use strict'; ajax((page, xhr) => { if (page != "loader" && page != "page") return; if ($(location).attr('href').includes('sid=racing')) { ensureRacingPageObserver(); ensureLeaderboardWatcher(); } $("#racingupdatesnew").ready(addSettingsDiv); $("#racingupdatesnew").ready(showSpeed); $('#racingAdditionalContainer').ready(showPenalty); if ($(location).attr('href').includes('sid=racing&tab=log&raceID=')) { $('#racingupdatesnew').ready(addPlaybackButton); } try { const parsed = JSON.parse(xhr.responseText); requestAnimationFrame(() => { try { parseRacingData(parsed); } catch (e) { console.debug('[Racing Enhancements PDA] Could not parse racing data', e); } }); } catch (e) { console.debug('[Racing Enhancements PDA] Could not parse racing data', e); } // Highlight JLT custom events const JltColor = '#fff200'; if ($('#racingAdditionalContainer').size() > 0 && $('#racingAdditionalContainer').find('div.custom-events-wrap').size() > 0) { $('#racingAdditionalContainer').find('div.custom-events-wrap').find('ul.events-list > li').each((i, li) => { if ($(li).find('li.name').size() > 0 && $(li).find('li.name').text().trim().startsWith('JLT-')) { $(li).addClass('gold'); $(li).css('color', JltColor).css('text-shadow', `0 0 1px ${JltColor}`); $(li).find('span.laps').css('color', JltColor); } }); } }); setInterval(() => { if (!location.href.includes('sid=racing')) return; ensureRacingPageObserver(); ensureLeaderboardWatcher(); if (document.getElementById('leaderBoard')) { queueUpdateDriversList(); } }, 3000); checkPenalty(); // Set up things that depend on jQuery being around in PDA jqueryDependantInitializations(); function jqueryDependantInitializations() { try { $("#racingupdatesnew").ready(addSettingsDiv); $("#racingupdatesnew").ready(showSpeed); $('#racingAdditionalContainer').ready(showPenalty); if ($(location).attr('href').includes('index.php')) { $('#mainContainer').ready(displayDailyGains); } if ($(location).attr('href').includes('sid=racing&tab=log&raceID=')) { $('#racingupdatesnew').ready(addPlaybackButton); } // Hide playback button when changing race tabs $('#racingupdatesnew').ready(function() { $('div.racing-main-wrap').find('ul.categories > li > a').on('click', function() { $('#racingupdatesnew').find('div.race-player-container').hide(); }); }); if ((FETCH_RS || SHOW_SKINS) && $(location).attr('href').includes('sid=racing')) { $("#racingupdatesnew").ready(function() { ensureRacingPageObserver(); ensureLeaderboardWatcher(); const racingAdditionalContainer = document.getElementById('racingAdditionalContainer'); if (racingAdditionalContainer && racingAdditionalContainer.dataset.rsWatcher === undefined) { new MutationObserver(() => { ensureLeaderboardWatcher(); }).observe(racingAdditionalContainer, { childList: true, subtree: true }); racingAdditionalContainer.dataset.rsWatcher = 'true'; } queueUpdateDriversList(); }); } // Styles GM_addStyle(` /* Name cell: clip long text and reserve space for RS */ ul.driver-item > li.name{ position: relative !important; overflow: hidden !important; padding-right: 0 !important; box-sizing: border-box !important; border-bottom: none !important; } ul.driver-item > li.name .name-scroll, ul.driver-item > li.name .rs-display { border-bottom: none !important; box-shadow: none !important; } html.racing-rs-enabled ul.driver-item > li.name{ padding-right: 55px !important; } /* Only this child scrolls horizontally */ ul.driver-item > li.name .name-scroll{ display: block !important; max-width: 100% !important; white-space: nowrap !important; overflow-x: auto !important; -webkit-overflow-scrolling: touch !important; scrollbar-width: none !important; touch-action: pan-x !important; } ul.driver-item > li.name .name-scroll::-webkit-scrollbar{ display:none; } /* RS badge fixed on the right, never scrolls */ ul.driver-item > li.name .rs-display{ position: absolute !important; right: 8px !important; top: 50% !important; transform: translateY(-50%) !important; white-space: nowrap !important; pointer-events: none !important; } /* Icons (unchanged) */ li.name .race_position{ background:url(/images/v2/racing/car_status.svg) 0 0 no-repeat; display:inline-block; width:20px; height:18px; vertical-align:text-bottom; } li.name .race_position.gold{ background-position:0 0; } li.name .race_position.silver{ background-position:0 -22px; } li.name .race_position.bronze{ background-position:0 -44px; } `); GM_addStyle(` #csv-viewer-modal { position: fixed; inset: 0; z-index: 99999; display: none; } #csv-viewer-modal .csv-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,.55); } #csv-viewer-modal .csv-panel { position: absolute; left: 5%; right: 5%; top: 10%; bottom: 10%; background: #1c1c1c; color: #eaeaea; border-radius: 8px; display: flex; flex-direction: column; box-shadow: 0 10px 30px rgba(0,0,0,.5); border: 1px solid #333; } #csv-viewer-modal .csv-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid #333; font-weight: 600; } #csv-viewer-modal .csv-actions button { margin-left: 8px; padding: 6px 10px; background: #2a2a2a; color: #eaeaea; border: 1px solid #444; border-radius: 6px; } #csv-viewer-modal .csv-body { flex: 1; margin: 0; padding: 10px 12px; overflow: auto; white-space: pre; font: 12px/1.4 monospace; -webkit-overflow-scrolling: touch; } @media (min-width: 860px){ #csv-viewer-modal .csv-panel { left: 15%; right: 15%; top: 10%; bottom: 10%; } } `); } catch(e) { // keep trying until jQuery is defined in PDA shell if (e instanceof ReferenceError) { setTimeout(jqueryDependantInitializations, 1000); } } }