Skip to content

Instantly share code, notes, and snippets.

@SavageCore
Last active December 1, 2024 03:41
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);
}
}
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment