Skip to content

Instantly share code, notes, and snippets.

@SavageCore
Last active October 5, 2025 20:12
Show Gist options
  • Save SavageCore/2e96af3c572ec27e9db7182cd06683d8 to your computer and use it in GitHub Desktop.
Save SavageCore/2e96af3c572ec27e9db7182cd06683d8 to your computer and use it in GitHub Desktop.

Why?

The script here was broken and also the filename didn't end .user.js so you were unable to one click install.

Hopefully the Collection owner sorts this out :) But for now...

Install

Download Violentmonkey or similar userscript manager and click here.

Changelog

1.0.02: Fix some querySelector's and wait for full page load before modifying the DOM

1.0.03: Re-try code injection to workaround React Hydration issues

// ==UserScript==
// @name TamperMonkeyRetroachievements
// @namespace https://archive.org/details/retroachievements_collection_v5
// @updateURL https://gist.github.com/SavageCore/2e96af3c572ec27e9db7182cd06683d8/raw/TamperMonkeyRetroachievements.user.js
// @downloadURL https://gist.github.com/SavageCore/2e96af3c572ec27e9db7182cd06683d8/raw/TamperMonkeyRetroachievements.user.js
// @version 1.0.03
// @description Add download links to retroachievements.org Supported Game Files page e.g. https://retroachievements.org/game/19339/hashes
// @author wholee
// @match https://retroachievements.org/game/*/hashes
// @icon https://archive.org/images/glogo.jpg
// @grant none
// @run-at document-end
// ==/UserScript==
//
// 0.7: Updated archiveOrgLastModified URL
// 0.8: Don't call archive.org with every page refresh
// 0.9: Refactor code
// 0.9.1: Use {cache: 'no-cache'} for retroachievementsHashList download
// 0.9.2: Updated disclaimer
// 0.9.3: Split PS2 to new archive.org collection
// 0.9.4: Refactor PS2
// 0.9.5: Add note for FLYCAST ROMs
// 0.9.6: Added descriptive error messages
// 0.9.7: Added FBNeoZipLink
// 0.9.8: Due to page changes, updated disclaimer position
// 0.9.9: Cosmetic code changes, FBNeo link updates and disclaimer text
// 0.9.91: Cosmetic code changes, fix typo in PS2 download link
// 0.9.92: Separated NES and SNES to their own archive items
// 0.9.93: Separated Playstation
// 0.9.94: Separated Playstation Portable
// 0.9.95: Small code refactor
// 0.9.96: Added GameCube
// 0.9.97: HTML-encode links to archive.org
// 0.9.98: Remove HTML-encode links
// 1.0.00: Six months hiatus updates
// Update download link position due to site changes
// Add missing and Paid Hash info
// Split PlayStation 2 in two due to the size
// Rename NES and SNES collections to match ConsoleName update
// 1.0.01: wrapped download links in <div>
// 1.0.02: Fix some querySelector's and wait for full page load before modifying the DOM (SavageCore)
// 1.0.03: Re-try code injection to workaround React Hydration issues (SavageCore)
(async function () {
'use strict';
const collectionName = 'retroachievements_collection';
const mainCollectionItem = 'v5';
const separateCollectionItems = ['NES-Famicom', 'SNES-Super Famicom', 'PlayStation', 'PlayStation 2', 'PlayStation Portable', 'GameCube'];
const collectionDownloadURL = 'https://archive.org/download/' + collectionName;
const collectionDetailsURL = 'https://archive.org/details/' + collectionName + '_' + mainCollectionItem;
const collectionLastModifiedURL = 'https://archive.org/metadata/' + collectionName + '_' + mainCollectionItem + '/item_last_updated';
const FBNeoROMSDownloadURL = 'https://archive.org/download/2020_01_06_fbn/roms/';
const FBNeoROMSDetailsURL = 'https://archive.org/details/2020_01_06_fbn/';
const retroachievementsHashList = 'TamperMonkeyRetroachievements.json';
const updateInterval = 86400; // 24 hours
const currentUnixTimestamp = Math.floor(Date.now() / 1000);
const collectionLastUpdated = parseInt(localStorage.getItem('collectionLastUpdated'));
const collectionLastModified = parseInt(localStorage.getItem('collectionLastModified'));
function addDisclaimer() {
// add disclaimer
const disclaimer = '<b>Downloads are provided through <a href="' + collectionDetailsURL + '">' + collectionDetailsURL + '</a> TamperMonkey script</br>and are not endorsed or supported by retroachievements.org</br></br>Please respect retroachievements.org\'s policies and do not post links to ROMs on their website or Discord.</b>';
document.querySelector("#app > div > main > article > div > div.flex.flex-col.gap-5 > div.-mx-3.rounded.bg-embed.px-3.py-4.sm\\:mx-0.sm\\:px-4.flex.flex-col.gap-4").insertAdjacentHTML('afterend', '<p class="embedded" id="ia_disclaimer>' + disclaimer + '</p>');
}
if (isNaN(collectionLastUpdated) || currentUnixTimestamp > collectionLastUpdated + updateInterval) {
fetch(collectionLastModifiedURL)
.then(response => response.json())
.then(output => {
if (output.result === undefined) { // archive.org returns 200/OK and {"error" : "*error description*"} on errors
throw 'Can\'t get last modified date from archive.org. ' + output.error;
} else {
localStorage.setItem('collectionLastModified', output.result);
}
if (parseInt(output.result) === collectionLastModified) { // don't download retroachievementsHashList if we already have the latest
localStorage.setItem('collectionLastUpdated', currentUnixTimestamp);
injectArchiveGames(JSON.parse(localStorage.getItem('collectionROMList')));
} else {
fetch(collectionDownloadURL + '_' + mainCollectionItem + '/' + retroachievementsHashList, { cache: 'no-cache' })
.then(response => response.json())
.then(output => {
injectArchiveGames(output);
localStorage.setItem('collectionROMList', JSON.stringify(output));
localStorage.setItem('collectionLastUpdated', currentUnixTimestamp);
})
.catch(error => {
// if we can't download retroachievementsHashList
injectArchiveGames(null, true, 'Can\'t get retroachievements hash list from archive.org. Please try again later.');
localStorage.removeItem('collectionLastModified');
localStorage.removeItem('collectionLastUpdated');
localStorage.removeItem('collectionROMList');
});
}
addDisclaimer();
})
.catch(() => {
// we still have to let the end user know that script is working but archive.org is not
injectArchiveGames(null, true, 'Can\'t get required information from archive.org. Please try again later.');
localStorage.removeItem('collectionLastModified');
localStorage.removeItem('collectionLastUpdated');
localStorage.removeItem('collectionROMList');
});
} else {
window.addEventListener('load', () => {
addDisclaimer();
injectArchiveGames(JSON.parse(localStorage.getItem('collectionROMList')));
// After half a second, check if the page now includes the injected disclaimer and download links, retry if not
// Fix for React Hydration failing resulting in removed links and disclaimer (https://reactjs.org/docs/error-decoder.html?invariant=418 in console)
let attempts = 0;
const maxAttempts = 10;
const interval = 500;
const tryInjection = () => {
if (document.querySelector("#ia_disclaimer") === null) {
addDisclaimer();
injectArchiveGames(JSON.parse(localStorage.getItem('collectionROMList')));
} else if (attempts < maxAttempts) {
attempts++;
setTimeout(tryInjection, interval);
}
};
setTimeout(tryInjection, interval);
});
}
function injectArchiveGames(gameData, boolArchiveOrgDown = false, message = '') {
let hashLists = document.querySelector("#app > div > main > article > div > div.flex.flex-col.gap-5 > div.flex.flex-col.gap-1 > div > ul").getElementsByTagName('li'); // get hash list
let gameId = window.location.pathname.split("/")[2]; // get gameID from URL
for (let x = 0; x < hashLists.length; ++x) {
let retroHashNode = hashLists[x].childNodes[1];
let retroHash = retroHashNode.childNodes[0].innerText.trim().toUpperCase();
retroHashNode.childNodes[0].innerText = retroHash;// fix hash capitalization on the page
if (boolArchiveOrgDown) {
retroHashNode.insertAdjacentHTML("beforeend", '<b>' + message + '</b>');
} else {
try {
if (gameData[gameId] != undefined && gameData[gameId][0][retroHash] != undefined) {
let hashData = gameData[gameId][0][retroHash]; // for now, we only have one item in the gameData[gameId] array
let link, appendExtraInfo = '';
let ROMdataArray = hashData.split('/');
let system = ROMdataArray[0];
let fileName = ROMdataArray[ROMdataArray.length - 1];
switch (true) {
case hashData.indexOf('\\') !== -1: // '\' is used to easily identify FBNeo ROMs in retroachievementsHashList, 'arcade\10yard.zip', 'nes\finalfaniii.zip'
ROMdataArray = hashData.split('\\');
system = ROMdataArray[0].replace('megadriv', 'megadrive');
fileName = ROMdataArray[ROMdataArray.length - 1];
// example link: https://archive.org/download/2020_01_06_fbn/roms/nes.zip/nes/finalfaniii.zip
link = FBNeoROMSDownloadURL + system + '.zip/' + system + '/' + fileName;
appendExtraInfo = '<u><b>FBNeo ' + system.toUpperCase() + ' ROM set maintained by a 3rd party at</u></b> <a href="' + FBNeoROMSDetailsURL + '">' + FBNeoROMSDetailsURL + '</a></br>Download FULL ' + system.toUpperCase() + ' SET: <a href="' + FBNeoROMSDownloadURL + system + '.zip">' + system + '.zip</a>'; // add a note for FBNeo ROMs
retroHashNode.insertAdjacentHTML("beforeend", '<div><b><a href="' + link + '">Download ' + fileName + '</a></b></br>' + appendExtraInfo + '</div>');
break;
case hashData.startsWith('Dreamcast/!_flycast/'):
link = collectionDownloadURL + '_' + mainCollectionItem + '/' + hashData;
appendExtraInfo = '<b>Use <a href="https://github.com/flyinghead/flycast">https://github.com/flyinghead/flycast</a> or <a href="https://github.com/libretro/flycast">https://github.com/libretro/flycast</a> to run this ROM.</b>'; // add a note for FLYCAST ROMs
retroHashNode.insertAdjacentHTML("beforeend", '<div><b><a href="' + link + '">Download ' + fileName + '</a></b></br>' + appendExtraInfo + '</div>');
break;
case separateCollectionItems.includes(system):
// PlayStation 2 is split based on filename over two archive.org items due to it's size
if (system == 'PlayStation 2') {
/^[n-z].*$/gim.test(fileName) ? system = 'PlayStation_2_N-Z' : system = 'PlayStation_2_A-M';
}
link = collectionDownloadURL + '_' + system.replace(' ', '_') + '/' + hashData; // archive.org is not allowing spaces in item name
// appendExtraInfo = '<b>Download provided through <a href=' + collectionDetailsURL + '>' + collectionDetailsURL + '</a></b>';
retroHashNode.insertAdjacentHTML("beforeend", '<div><b><a href="' + link + '">Download ' + fileName + '</a></b></br>' + appendExtraInfo + '</div>');
break;
case hashData.startsWith('missing'):
// TODO
retroHashNode.insertAdjacentHTML("beforeend", '');
break;
case hashData.startsWith('paid'):
//TODO
retroHashNode.insertAdjacentHTML("beforeend", '');
break;
case hashData.startsWith('ignore'):
//TODO
retroHashNode.insertAdjacentHTML("beforeend", '');
break;
default:
link = collectionDownloadURL + '_' + mainCollectionItem + '/' + hashData;
//appendExtraInfo = '<b>Download provided through <a href=' + collectionDetailsURL + '>' + collectionDetailsURL + '</a></b>';
retroHashNode.insertAdjacentHTML("beforeend", '<div><b><a href="' + link + '">Download ' + fileName + '</a></b></br>' + appendExtraInfo + '</div>');
break;
}
} else {
console.log('Hash not found: ' + retroHash);
retroHashNode.insertAdjacentHTML("beforeend", '<div><b>Download not available.</b></div>');
}
} catch (error) {
console.log('Error processing hashData: ' + hashData);
console.log(error);
}
}
}
}
})();
@skodds
Copy link

skodds commented Sep 8, 2025

  • Script is failing to insert download URL's
  • not correctly implementing disclaimer text in it's entirety

helpers.js:95 Uncaught ReferenceError: hashData is not defined
Line:237 console.log('Error processing hashData: ' + hashData);
Line:123 injectArchiveGames(JSON.parse(localStorage.getItem('collectionROMList')));

image

@SavageCore
Copy link
Author

910f71ea-063c-485d-89f8-550f79e565ee

Still working ok here Brave + Violentmonkey

What should the full disclaimer text be? I've matched the original afaik. If it was my script I'd remove it anyway.

@skodds
Copy link

skodds commented Sep 11, 2025

Checked again and the links are being properly inserted now.
In regards to the disclaimer, there was a small syntax error on line 68. missing a closing quote for the id attribute. Disclaimer is now being displayed how it was intended.
image
ps. thank you for spending the time and fixing this script, with my lack of skill I tried and failed many times, but found yours and has saved me a lot of time. Cool to see it working.

@SavageCore
Copy link
Author

Ah I see, will get that updated soon!

Glad you found it, wasn't sure where to post it...

@rgquitter11
Copy link

Hello, I'm trying to use the script and in tampermonkey it doesn't load the url and says "can't connect to archive.org at the moment." Violentmonkey works fine on showing the url but when I click on it, the archive oage says item has been removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment