UNPKG

@cruncheevos/cli

Version:

Maintain achievement sets for RetroAchievements.org using JavaScript, an alternative to RATools

155 lines (154 loc) 5.23 kB
import { AchievementSet } from '@cruncheevos/core'; import { wrappedError } from '@cruncheevos/core/util'; import nodeFetch, { AbortError } from 'node-fetch'; import chalk from 'chalk'; import { getFs, log, resolveRACache } from './mockable.js'; const fs = getFs(); let cachedCredentials; function getCredentials() { if (cachedCredentials) { return cachedCredentials; } const configFile = fs .readdirSync(resolveRACache('./')) .filter(x => x.match(/^raprefs.*\.cfg$/i))[0]; if (!configFile) { throw new Error(`expected RAPrefs.cfg file, but found none`); } const configJSON = fs.readFileSync(resolveRACache(`./${configFile}`)).toString(); try { var config = JSON.parse(configJSON); } catch (err) { throw wrappedError(err, `${configFile}: ${err.message}`); } if (!config.Username) { throw new Error(`${configFile}: expected Username property as string, but got ${config.Username}`); } if (!config.Token) { throw new Error(`${configFile}: expected Token property as string, but got ${config.Token}`); } cachedCredentials = { username: config.Username, token: config.Token, }; return cachedCredentials; } async function fetchRemoteData(opts) { const { gameId, timeout } = opts; const { username, token } = getCredentials(); const abortController = new AbortController(); const timeoutHandle = setTimeout(() => abortController.abort(), timeout); const payload = await nodeFetch(`https://retroachievements.org/dorequest.php?r=patch&t=${token}&u=${username}&g=${gameId}`, { headers: { 'User-Agent': 'cruncheevos-cli', }, signal: abortController.signal, }) .then(x => { if (x.ok) { return x.json(); } else { throw new Error(`failed to fetch remote data: HTTP ${x.status}`); } }) .catch(err => { if (err instanceof AbortError) { throw wrappedError(err, `failed to fetch remote data: timed out`); } else { throw err; } }) .finally(() => clearTimeout(timeoutHandle)); if (payload.Success !== true) { throw new Error(`failed to fetch remote data: expected payload.Success to be true, but got ${payload.Success}`); } return payload.PatchData; } export default async function fetch(opts) { const { gameId } = opts; log(`fetching remote data for gameId ${gameId}`); const gameData = await fetchRemoteData(opts); const filePath = resolveRACache(`./RACache/Data/${gameId}.json`); fs.writeFileSync(filePath, JSON.stringify(gameData)); log(`dumped remote data for gameId ${gameId}: ${filePath}`); return gameData; } export async function getRemoteData({ gameId, refetch, timeout, }) { const filePath = resolveRACache(`./RACache/Data/${gameId}.json`); if (!refetch && fs.existsSync(filePath)) { return JSON.parse(fs.readFileSync(filePath).toString()); } return fetch({ gameId, timeout, }); } async function _getSetFromRemote(opts) { const { gameId } = opts; const gameData = await getRemoteData(opts); const set = new AchievementSet({ gameId, title: gameData.Title, }); gameData.Achievements.forEach((ach, i) => { if (ach.Flags === 5 && opts.excludeUnofficial) { return; } if (ach.Flags !== 3 && ach.Flags !== 5) { return; } try { set.addAchievement({ id: ach.ID, title: ach.Title, description: ach.Description, points: ach.Points, type: ach.Type || '', author: ach.Author, badge: ach.BadgeName, conditions: ach.MemAddr, }); } catch (err) { throw wrappedError(err, `Achievements[${i}]: ${err.message}`); } }); gameData.Leaderboards.forEach((lb, i) => { if (lb.Hidden && opts.excludeUnofficial) { return; } const { ID } = lb; const leaderboardType = lb.Format === 'TIME' ? 'FRAMES' : lb.Format; try { set.addLeaderboard({ id: ID, title: lb.Title, description: lb.Description, lowerIsBetter: Boolean(lb.LowerIsBetter), conditions: lb.Mem, type: leaderboardType, }); } catch (err) { throw wrappedError(err, `Leaderboards[${i}]: ${err.message}`); } }); return set; } export async function getSetFromRemote(opts) { const { gameId, excludeUnofficial, refetch, timeout } = opts; try { return await _getSetFromRemote({ gameId, excludeUnofficial, refetch, timeout }); } catch (err) { if (refetch) { throw err; } log(chalk.yellowBright(err.message)); log(chalk.yellowBright(`remote data got issues, will attempt to refetch it`)); return await _getSetFromRemote({ gameId, excludeUnofficial, refetch: true, timeout }); } }