// ==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 = '';
$('#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(
``);
}
}
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 = `
`;
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);
}
}
}