UNPKG

igir

Version:

🕹 A zero-setup ROM collection manager that sorts, filters, extracts or archives, patches, and reports on collections of any size on any OS.

258 lines (257 loc) • 10.7 kB
import { writeToString } from '@fast-csv/format'; import chalk from 'chalk'; import ArrayPoly from '../polyfill/arrayPoly.js'; const ROMType = { GAME: 'games', BIOS: 'BIOSes', DEVICE: 'devices', RETAIL: 'retail releases', PATCHED: 'patched games', }; export const GameStatus = { // The Game wanted to be written, and it has no ROMs or every ROM was found FOUND: 1, // Only some of the Game's ROMs were found INCOMPLETE: 2, // The Game wanted to be written, but there was no matching ReleaseCandidate MISSING: 3, // The input file was not used in any ReleaseCandidate, but a duplicate file was DUPLICATE: 4, // The input File was not used in any ReleaseCandidate, and neither was any duplicate file UNUSED: 5, // The output File was not from any ReleaseCandidate, so it was deleted DELETED: 6, }; const GameStatusInverted = Object.fromEntries(Object.entries(GameStatus).map(([key, value]) => [value, key])); /** * Parse and hold information about every {@link Game} in a {@link DAT}, as well as which * {@link Game}s were found (had a {@link WriteCandidate} created for it). */ export default class DATStatus { dat; allRomTypesToGames = new Map(); foundRomTypesToCandidates = new Map(); incompleteRomTypesToCandidates = new Map(); constructor(dat, candidates) { this.dat = dat; const indexedCandidates = candidates.reduce((map, candidate) => { const key = candidate.getGame().hashCode(); if (map.has(key)) { map.get(key)?.push(candidate); } else { map.set(key, [candidate]); } return map; }, new Map()); // Un-patched ROMs dat.getGames().forEach((game) => { DATStatus.pushValueIntoMap(this.allRomTypesToGames, game, game); const gameCandidates = indexedCandidates.get(game.hashCode()); if (gameCandidates !== undefined || game.getRoms().length === 0) { const gameCandidate = gameCandidates?.at(0); if (gameCandidate && gameCandidate.getRomsWithFiles().length !== game.getRoms().length) { // The found ReleaseCandidate is incomplete DATStatus.pushValueIntoMap(this.incompleteRomTypesToCandidates, game, gameCandidate); return; } // The found ReleaseCandidate is complete DATStatus.pushValueIntoMap(this.foundRomTypesToCandidates, game, gameCandidate); return; } }); // Patched ROMs for (const candidate of candidates.filter((candidate) => candidate.isPatched())) { const game = candidate.getGame(); DATStatus.append(this.allRomTypesToGames, ROMType.PATCHED, game); DATStatus.append(this.foundRomTypesToCandidates, ROMType.PATCHED, candidate); } } static pushValueIntoMap(map, game, value) { DATStatus.append(map, ROMType.GAME, value); if (game.getIsBios()) { DATStatus.append(map, ROMType.BIOS, value); } if (game.getIsDevice()) { DATStatus.append(map, ROMType.DEVICE, value); } if (game.isRetail()) { DATStatus.append(map, ROMType.RETAIL, value); } } static append(map, romType, value) { if (map.has(romType)) { map.get(romType)?.push(value); } else { map.set(romType, [value]); } } getDATName() { return this.dat.getName(); } getInputFiles() { return [ ...this.foundRomTypesToCandidates.values(), ...this.incompleteRomTypesToCandidates.values(), ] .flat() .filter((candidate) => candidate !== undefined) .flatMap((candidate) => candidate.getRomsWithFiles()) .map((romWithFiles) => romWithFiles.getInputFile()); } /** * If any {@link Game} in the entire {@link DAT} was found in the input files. */ anyGamesFound(options) { return DATStatus.getAllowedTypes(options).reduce((result, romType) => { const foundCandidates = this.foundRomTypesToCandidates.get(romType)?.length ?? 0; return result || foundCandidates > 0; }, false); } /** * Return a string of CLI-friendly output to be printed by a {@link Logger}. */ toConsole(options) { return `${DATStatus.getAllowedTypes(options) .filter((type) => { const games = this.allRomTypesToGames.get(type); return games !== undefined && games.length > 0; }) .map((type) => { const found = this.foundRomTypesToCandidates.get(type) ?? []; const all = this.allRomTypesToGames.get(type) ?? []; if (!options.usingDats()) { return `${found.length.toLocaleString()} ${type}`; } const percentage = (found.length / all.length) * 100; let color; if (percentage >= 100) { color = chalk.rgb(0, 166, 0); // macOS terminal green } else if (percentage >= 75) { color = chalk.rgb(153, 153, 0); // macOS terminal yellow } else if (percentage >= 50) { color = chalk.rgb(160, 124, 0); } else if (percentage >= 25) { color = chalk.rgb(162, 93, 0); } else if (percentage > 0) { color = chalk.rgb(160, 59, 0); } else { color = chalk.rgb(153, 0, 0); // macOS terminal red } // Patched ROMs are always found===all if (type === ROMType.PATCHED) { return `${color(all.length.toLocaleString())} ${type}`; } return `${color(found.length.toLocaleString())}/${all.length.toLocaleString()} ${type}`; }) .filter((string_) => string_.length > 0) .join(', ')} ${options.shouldWrite() ? 'written' : 'found'}`; } /** * Return the file contents of a CSV with status information for every {@link Game}. */ async toCsv(options) { const foundCandidates = DATStatus.getValuesForAllowedTypes(options, this.foundRomTypesToCandidates); const incompleteCandidates = DATStatus.getValuesForAllowedTypes(options, this.incompleteRomTypesToCandidates); const rows = DATStatus.getValuesForAllowedTypes(options, this.allRomTypesToGames) .reduce(ArrayPoly.reduceUnique(), []) .sort((a, b) => a.getName().localeCompare(b.getName())) .map((game) => { let status = GameStatus.MISSING; const incompleteCandidate = incompleteCandidates.find((candidate) => candidate.getGame().equals(game)); if (incompleteCandidate) { status = GameStatus.INCOMPLETE; } const foundCandidate = foundCandidates.find((candidate) => candidate?.getGame().equals(game)); if (foundCandidate !== undefined || game.getRoms().length === 0) { status = GameStatus.FOUND; } const filePaths = [ ...(incompleteCandidate ? incompleteCandidate.getRomsWithFiles() : []), ...(foundCandidate ? foundCandidate.getRomsWithFiles() : []), ] .map((romWithFiles) => options.shouldWrite() ? romWithFiles.getOutputFile() : romWithFiles.getInputFile()) .map((file) => file.getFilePath()) .reduce(ArrayPoly.reduceUnique(), []); return DATStatus.buildCsvRow(this.getDATName(), game.getName(), status, filePaths, foundCandidate?.isPatched() ?? false, game.getIsBios(), game.isRetail(), game.isUnlicensed(), game.isDebug(), game.isDemo(), game.isBeta(), game.isSample(), game.isPrototype(), game.isProgram(), game.isAftermarket(), game.isHomebrew(), game.isBad()); }); return writeToString(rows, { headers: [ 'DAT Name', 'Game Name', 'Status', 'ROM Files', 'Patched', 'BIOS', 'Retail Release', 'Unlicensed', 'Debug', 'Demo', 'Beta', 'Sample', 'Prototype', 'Program', 'Aftermarket', 'Homebrew', 'Bad', ], }); } /** * Return a string of CSV rows without headers for a certain {@link GameStatusValue}. */ static async filesToCsv(filePaths, status) { return writeToString(filePaths.map((filePath) => this.buildCsvRow('', '', status, [filePath]))); } static buildCsvRow(datName, gameName, status, filePaths = [], patched = false, bios = false, retail = false, unlicensed = false, debug = false, demo = false, beta = false, sample = false, prototype = false, test = false, aftermarket = false, homebrew = false, bad = false) { return [ datName, gameName, GameStatusInverted[status], filePaths.join('|'), String(patched), String(bios), String(retail), String(unlicensed), String(debug), String(demo), String(beta), String(sample), String(prototype), String(test), String(aftermarket), String(homebrew), String(bad), ]; } static getValuesForAllowedTypes(options, romTypesToValues) { return DATStatus.getAllowedTypes(options) .flatMap((type) => romTypesToValues.get(type)) .filter((value) => value !== undefined) .reduce(ArrayPoly.reduceUnique(), []) .sort(); } static getAllowedTypes(options) { return [ !options.getOnlyBios() && !options.getOnlyDevice() && !options.getOnlyRetail() ? ROMType.GAME : undefined, options.getOnlyBios() || (!options.getNoBios() && !options.getOnlyDevice()) ? ROMType.BIOS : undefined, options.getOnlyDevice() || (!options.getOnlyBios() && !options.getNoDevice()) ? ROMType.DEVICE : undefined, options.getOnlyRetail() || (!options.getOnlyBios() && !options.getOnlyDevice()) ? ROMType.RETAIL : undefined, ROMType.PATCHED, ].filter((romType) => romType !== undefined); } }