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
JavaScript
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;
});
}
}