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.

180 lines (179 loc) • 8.59 kB
import { ProgressBarSymbol } from '../../console/progressBar.js'; import ArrayPoly from '../../polyfill/arrayPoly.js'; import Game from '../../types/dats/game.js'; import ROM from '../../types/dats/rom.js'; import { MergeMode } from '../../types/options.js'; import Module from '../module.js'; /** * Process a {@link DAT} with the ROM merge mode specified. */ export default class DATMergerSplitter extends Module { options; constructor(options, progressBar) { super(progressBar, DATMergerSplitter.name); this.options = options; } /** * Un-merge, split, or merge the {@link Game}s within a {@link DAT}. */ merge(dat) { // Don't do anything if no type provided if (this.options.getMergeRoms() === undefined) { this.progressBar.logTrace(`${dat.getName()}: no ROM merge option provided, doing nothing`); return dat; } // Parent/clone information is required to merge & split if (!dat.hasParentCloneInfo()) { this.progressBar.logTrace(`${dat.getName()}: DAT doesn't have parent/clone info, doing nothing`); return dat; } const gameNamesToGames = dat.getGames().reduce((map, game) => { map.set(game.getName(), game); return map; }, new Map()); this.progressBar.logTrace(`${dat.getName()}: merging & splitting ${dat.getGames().length.toLocaleString()} game${dat.getGames().length === 1 ? '' : 's'}`); this.progressBar.setSymbol(ProgressBarSymbol.DAT_MERGE_SPLIT); this.progressBar.resetProgress(dat.getGames().length); const newGames = dat .getParents() .flatMap((parent) => this.mergeParent(dat, parent, gameNamesToGames)); const newDat = dat.withGames(newGames); this.progressBar.logTrace(`${newDat.getName()}: merged/split to ${newDat.getGames().length.toLocaleString()} game${newDat.getGames().length === 1 ? '' : 's'}`); this.progressBar.logTrace(`${newDat.getName()}: done merging & splitting`); return newDat; } mergeParent(dat, parent, gameNamesToGames) { let games = parent.getGames(); // Sanitization games = games.map((game) => game.withProps({ roms: game .getRoms() // Get rid of ROMs that haven't been dumped yet .filter((rom) => rom.getStatus() !== 'nodump') // Get rid of duplicate ROMs. MAME will sometimes duplicate a file with the exact same // name, size, and checksum but with a different "region" (e.g. neogeo). .filter(ArrayPoly.filterUniqueMapped((rom) => rom.getName())), disks: game .getDisks() // Get rid of disks that haven't been dumped yet .filter((disk) => disk.getStatus() !== 'nodump'), })); // 'full' types expect device ROMs to be included if (this.options.getMergeRoms() === MergeMode.FULLNONMERGED) { games = games.map((game) => game.withProps({ roms: [ ...game .getDeviceRefs() // De-duplicate DeviceRef names .map((deviceRef) => deviceRef.getName()) .reduce(ArrayPoly.reduceUnique(), []) // Get ROMs from the DeviceRef .map((deviceRefName) => gameNamesToGames.get(deviceRefName)) .filter((deviceGame) => deviceGame !== undefined) .flatMap((deviceGame) => deviceGame.getRoms().filter((rom) => rom.getStatus() !== 'nodump')), ...game.getRoms(), ], })); } // Non-'full' types expect BIOS files to be in their own set if (this.options.getMergeRoms() !== MergeMode.FULLNONMERGED) { games = games.map((game) => { if (game.getRomOf() === undefined) { // This game doesn't use an external BIOS return game; } // Look for this game's root ancestor, which might be a BIOS let biosGame; let romOf = game.getRomOf(); while (romOf !== undefined) { const romOfGame = gameNamesToGames.get(romOf); if (romOfGame === undefined) { // Invalid romOf attribute, external BIOS not found this.progressBar.logTrace(`${dat.getName()}: invalid romOf: ${romOf}`); return game; } biosGame = romOfGame; romOf = biosGame.getRomOf(); } if (biosGame === undefined) { // This shouldn't happen, but if it does, just ignore return game; } // If the referenced `romOf` game is not a BIOS, then it must be a parent game. // Reduce the non-BIOS parent to only its BIOS ROMs, so that they can be excluded from // the child. if (!biosGame.getIsBios()) { biosGame = biosGame.withProps({ roms: biosGame.getRoms().filter((rom) => rom.getBios() !== undefined), }); } return game.withProps({ roms: DATMergerSplitter.diffGameRoms(biosGame.getRoms(), game.getRoms()), }); }); } // 'split' and 'merged' types should exclude ROMs & disks found in their parent if (this.options.getMergeRoms() === MergeMode.SPLIT || this.options.getMergeRoms() === MergeMode.MERGED) { games = games.map((game) => { const cloneOf = game.getCloneOf(); if (!cloneOf) { // This game doesn't have a parent return game; } const parentGame = gameNamesToGames.get(cloneOf); if (!parentGame) { // Invalid cloneOf attribute, parent not found this.progressBar.logTrace(`${dat.getName()}: ${game.getName()} references an invalid parent: ${cloneOf}`); return game; } return game.withProps({ roms: DATMergerSplitter.diffGameRoms(parentGame.getRoms(), game.getRoms()), disks: DATMergerSplitter.diffGameRoms(parentGame.getDisks(), game.getDisks()), }); }); } const parentGames = games.filter((game) => game.isParent()); const cloneGames = games.filter((game) => game.isClone()); // For everything other than 'merged' we keep the same number of games if (this.options.getMergeRoms() !== MergeMode.MERGED) { if (parentGames.length > 0) { return [...parentGames, ...cloneGames]; } return cloneGames; } // For 'merged' we reduce to one game const cloneRoms = cloneGames.flatMap((game) => game.getRoms().map((rom) => new ROM({ ...rom, name: `${game.getName()}\\${rom.getName()}`, }))); const allRoms = [...cloneRoms, ...parentGames.flatMap((parentGame) => parentGame.getRoms())]; // And remove any duplicate ROMs, even if the duplicates exist only in clones and not the parent const allRomsDeduplicated = allRoms.filter(ArrayPoly.filterUniqueMapped((rom) => rom.hashCode())); return [ new Game({ ...parentGames[0], roms: allRomsDeduplicated, }), ]; } static diffGameRoms(parentRoms, childRoms) { const parentRomNamesToHashCodes = parentRoms.reduce((map, rom) => { map.set(rom.getName(), rom.hashCode()); return map; }, new Map()); return childRoms.filter((rom) => { const parentName = rom.getMerge() ?? rom.getName(); const parentHashCode = parentRomNamesToHashCodes.get(parentName); if (!parentHashCode) { // Parent doesn't have a ROM of the same name -> keep it return true; } if (parentHashCode !== rom.hashCode()) { // Parent has a ROM of the same name, but a different checksum -> keep it return true; } return false; }); } }