// ==UserScript== // @name GhostHax // @namespace http://tampermonkey.net/ // @version 0.4 // @description Advanced ghost assistance detection for FRHD // @author 1s3k3b // @include /^https?:\/\/(www\.)?freeriderhd\.com/ // @grant none // ==/UserScript== (function main() { const lastHref = window.location.href; const reloadInv = setInterval(() => { if (lastHref !== window.location.href) { clearInterval(reloadInv); main(); } }, 1000); if (!/^https?:\/\/(www\.)?freeriderhd\.com\/t\//.test(window.location.href)) return; const genHaste = (code, lang = '', host = 'hasteb.in') => fetch(`https://cors-anywhere.herokuapp.com/https://${host}/documents`, { method: 'POST', body: code }) .then(r => r.json()) .then(({ key }) => `https://${host}/${key}${lang && `.${lang}`}`) .catch(() => host === 'hasteb.in' ? genHaste(code, lang, 'hastebin.com') : ''); const inv = setInterval(() => { if (document.querySelector('table')) { clearInterval(inv); Array .from(document.querySelectorAll('table')) .forEach(table => table .querySelectorAll('[style="text-align:left"]') .forEach(t => { // eslint-disable-next-line prefer-const let [,,, name,,,, time ] = t.parentElement.childNodes; name.querySelector('span').style.overflow = 'visible'; name.childNodes[3].innerHTML += '\n</>'; name.childNodes[3].querySelector('.ghax').onclick = async () => { time = time.innerText.split(':'); const { user: { u_name, u_id } } = await fetch(name.childNodes[1].getAttribute('href') + '?ajax=true').then(d => d.json()); const data = await fetch('https://www.freeriderhd.com/track_api/load_races', { 'headers': { 'accept': 'application/json, text/javascript, */*; q=0.01', 'accept-language': 'en-US,en;q=0.9,hu;q=0.8', 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin', 'x-requested-with': 'XMLHttpRequest', }, 'referrer': 'https://www.freeriderhd.com/', 'referrerPolicy': 'origin', 'body': `t_id=${GameSettings.track.id}&u_ids=${u_id}&ajax=true&t_1=ref&t_2=desk`, 'method': 'POST', 'mode': 'cors', 'credentials': 'include', }) .then(d => d.json()); const duplicateInputs = (d => d.length - [...new Set(d)].length)( Object .entries(JSON.parse(data.data[0].race.code)) .filter(([k]) => k.endsWith('_up')) .map(([, v]) => v) .flat(Infinity), ); const avgTicks = (a => a.reduce((a, b) => a + b) / a.length)( Object .entries(JSON.parse(data.data[0].race.code)) .filter(([k]) => /(up|down|left|right|z|backspace|enter)_(up|down)/.test(k)) .map(([, v]) => v) .flat(Infinity) .sort((a, b) => a - b) .reduce((a, b, c, d) => [...a, (d[c + 1] || b) - b], []), ); const haste = await genHaste(`/*** Ghost Data Breakdown *** Track: https://frhd.co/t/${GameSettings.track.id}/r/${u_name} User: https://frhd.co/u/${u_name} User ID: ${u_id} Track title: ${GameSettings.track.title} Duplicate inputs: ${duplicateInputs} Average ticks between inputs: ${avgTicks} Time: ${time.join(':')} Ticks: ${data.data[0].race.run_ticks} Data: ${ Object .entries(JSON.parse(data.data[0].race.code)) .filter(([k]) => /(up|down|left|right|z|backspace|enter)_(up|down)/.test(k)) .map(([k, v]) => `${k.split('_').map(x => `${x[0].toUpperCase()}${x.slice(1)}`).join(' ')}: ${v.join(', ')}`) .sort((a, b) => a.charCodeAt(0) - b.charCodeAt(0) - a.includes('Down')) .join('\n') } */ raw = ${JSON.stringify(JSON.parse(data.data[0].race.code), undefined, 2)}`, 'js'); window.open(haste); }; })); } }, 1000); })();