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.

134 lines (133 loc) • 6.92 kB
import path from 'node:path'; import { ProgressBarSymbol } from '../../console/progressBar.js'; import ROM from '../../types/dats/rom.js'; import ArchiveEntry from '../../types/files/archives/archiveEntry.js'; import File from '../../types/files/file.js'; import ROMWithFiles from '../../types/romWithFiles.js'; import WriteCandidate from '../../types/writeCandidate.js'; import Module from '../module.js'; /** * For each {@link Patch} that matches a {@link ROM}, generate a new {@link Game} and * {@link WriteCandidate} of that {@link Game}. */ export default class CandidatePatchGenerator extends Module { constructor(progressBar) { super(progressBar, CandidatePatchGenerator.name); } /** * Generate the patched candidates. */ async generate(dat, candidates, patches) { if (candidates.length === 0) { this.progressBar.logTrace(`${dat.getName()}: no candidates to make patched candidates for`); return candidates; } if (patches.length === 0) { this.progressBar.logTrace(`${dat.getName()}: no patches to make patched candidates for`); return candidates; } this.progressBar.logTrace(`${dat.getName()}: generating patched candidates`); this.progressBar.setSymbol(ProgressBarSymbol.CANDIDATE_GENERATING); this.progressBar.resetProgress(candidates.length); const crcToPatches = CandidatePatchGenerator.indexPatchesByCrcBefore(patches); this.progressBar.logTrace(`${dat.getName()}: ${crcToPatches.size} unique patch${crcToPatches.size === 1 ? '' : 'es'} found`); const patchedCandidates = this.build(dat, candidates, crcToPatches); this.progressBar.logTrace(`${dat.getName()}: done generating patched candidates`); return patchedCandidates; } static indexPatchesByCrcBefore(patches) { return patches.reduce((map, patch) => { const key = patch.getCrcBefore(); if (map.has(key)) { map.get(key)?.push(patch); } else { map.set(key, [patch]); } return map; }, new Map()); } async build(dat, candidates, crcToPatches) { return (await Promise.all(candidates.map(async (unpatchedCandidate) => { // Possibly generate multiple new patched candidates for the ReleaseCandidates const patchedCandidates = await this.buildPatchedCandidates(dat, unpatchedCandidate, crcToPatches); return [unpatchedCandidate, ...patchedCandidates]; }))).flat(); } async buildPatchedCandidates(dat, unpatchedCandidate, crcToPatches) { // Get all patch files relevant to any ROM in the ReleaseCandidate const candidatePatches = unpatchedCandidate .getRomsWithFiles() .flatMap((romWithFiles) => romWithFiles.getInputFile()) .flatMap((inputFile) => { const inputFileCrc32 = inputFile.getCrc32(); if (inputFileCrc32 === undefined) { return []; } return crcToPatches.get(inputFileCrc32); }) .filter((patch) => patch !== undefined); // No relevant patches found, no new candidates generated if (candidatePatches.length === 0) { return []; } // Generate new, patched candidates for each patch return Promise.all(candidatePatches.map(async (patch) => { const patchedRomName = patch.getRomName(); const romsWithFiles = await Promise.all(unpatchedCandidate.getRomsWithFiles().map(async (romWithFiles) => { // Apply the new filename let rom = romWithFiles.getRom(); let inputFile = romWithFiles.getInputFile(); let outputFile = romWithFiles.getOutputFile(); // Apply the patch to the appropriate file if (patch.getCrcBefore() === romWithFiles.getRom().getCrc32()) { // Attach the patch to the input file inputFile = inputFile.withPatch(patch); // Build a new output file const extMatch = /[^.]+((\.[a-zA-Z0-9]+)+)$/.exec(romWithFiles.getRom().getName()); const extractedFileName = patchedRomName + (extMatch === null ? '' : extMatch[1]); if (outputFile instanceof ArchiveEntry) { outputFile = await ArchiveEntry.entryOf({ archive: outputFile.getArchive().withFilePath(patchedRomName), // Output is an archive of a single file, the entry path should also change entryPath: unpatchedCandidate.getRomsWithFiles().length === 1 ? extractedFileName : outputFile.getEntryPath(), size: patch.getSizeAfter(), crc32: patch.getCrcAfter(), fileHeader: outputFile.getFileHeader(), paddings: outputFile.getPaddings(), patch: outputFile.getPatch(), }); } else { const dirName = path.dirname(outputFile.getFilePath()); outputFile = await File.fileOf({ filePath: path.join(dirName, extractedFileName), size: patch.getSizeAfter(), crc32: patch.getCrcAfter(), fileHeader: outputFile.getFileHeader(), paddings: outputFile.getPaddings(), patch: outputFile.getPatch(), }); } // Build a new ROM from the output file's info const romName = path.join(path.dirname(rom.getName().replaceAll(/[\\/]/g, path.sep)), path.basename(outputFile.getExtractedFilePath())); rom = new ROM({ name: romName, size: outputFile.getSize(), crc32: outputFile.getCrc32(), }); this.progressBar.logTrace(`${dat.getName()}: ${inputFile.toString()}: patch candidate generated: ${outputFile.toString()}`); } return new ROMWithFiles(rom, inputFile, outputFile); })); // Build a new Game from the ROM's info const gameName = path.join(path.dirname(unpatchedCandidate.getGame().getName().replaceAll(/[\\/]/g, path.sep)), patchedRomName); const patchedGame = unpatchedCandidate.getGame().withProps({ name: gameName, }); return new WriteCandidate(patchedGame, romsWithFiles); })); } }