|
| 1 | +import ProgressBar, { ProgressBarSymbol } from '../../console/progressBar.js'; |
| 2 | +import DAT from '../../types/dats/dat.js'; |
| 3 | +import Game from '../../types/dats/game.js'; |
| 4 | +import LogiqxDAT from '../../types/dats/logiqx/logiqxDat.js'; |
| 5 | +import Options from '../../types/options.js'; |
| 6 | +import Module from '../module.js'; |
| 7 | + |
| 8 | +/** |
| 9 | + * Merge multi-disc {@link Game}s in a {@link DAT} into one game. |
| 10 | + */ |
| 11 | +export default class DATDiscMerger extends Module { |
| 12 | + private readonly options: Options; |
| 13 | + |
| 14 | + constructor(options: Options, progressBar: ProgressBar) { |
| 15 | + super(progressBar, DATDiscMerger.name); |
| 16 | + this.options = options; |
| 17 | + } |
| 18 | + |
| 19 | + /** |
| 20 | + * Merge {@link Game}s. |
| 21 | + */ |
| 22 | + merge(dat: DAT): DAT { |
| 23 | + if (!this.options.getMergeDiscs()) { |
| 24 | + this.progressBar.logTrace(`${dat.getName()}: not merging discs`); |
| 25 | + return dat; |
| 26 | + } |
| 27 | + |
| 28 | + if (dat.getGames().length === 0) { |
| 29 | + this.progressBar.logTrace(`${dat.getName()}: no games to merge`); |
| 30 | + return dat; |
| 31 | + } |
| 32 | + |
| 33 | + this.progressBar.logTrace( |
| 34 | + `${dat.getName()}: merging ${dat.getGames().length.toLocaleString()} game${dat.getGames().length !== 1 ? 's' : ''}`, |
| 35 | + ); |
| 36 | + this.progressBar.setSymbol(ProgressBarSymbol.DAT_MERGE_SPLIT); |
| 37 | + this.progressBar.reset(dat.getGames().length); |
| 38 | + |
| 39 | + const groupedGames = this.groupGames(dat.getGames()); |
| 40 | + const newDat = new LogiqxDAT(dat.getHeader(), groupedGames); |
| 41 | + this.progressBar.logTrace( |
| 42 | + `${newDat.getName()}: merged to ${newDat.getGames().length.toLocaleString()} game${newDat.getGames().length !== 1 ? 's' : ''}`, |
| 43 | + ); |
| 44 | + |
| 45 | + this.progressBar.logTrace(`${newDat.getName()}: done merging`); |
| 46 | + return newDat; |
| 47 | + } |
| 48 | + |
| 49 | + private groupGames(games: Game[]): Game[] { |
| 50 | + const gameNamesToGames = games.reduce((map, game) => { |
| 51 | + let gameNameStripped = game |
| 52 | + .getName() |
| 53 | + // Redump |
| 54 | + .replace(/ ?\(Disc [0-9]+\)/i, '') |
| 55 | + // TOSEC |
| 56 | + .replace(/ ?\(Disc [0-9]+ of [0-9]+\)/i, ''); |
| 57 | + |
| 58 | + if (gameNameStripped !== game.getName()) { |
| 59 | + // The game is a multi-disc game, strip some additional patterns |
| 60 | + gameNameStripped = gameNameStripped |
| 61 | + // TOSEC |
| 62 | + .replace(/\[[0-9]+S\]/, '') // Dreamcast ring code |
| 63 | + .trim(); |
| 64 | + } |
| 65 | + |
| 66 | + if (!map.has(gameNameStripped)) { |
| 67 | + map.set(gameNameStripped, [game]); |
| 68 | + } else { |
| 69 | + map.get(gameNameStripped)?.push(game); |
| 70 | + } |
| 71 | + |
| 72 | + return map; |
| 73 | + }, new Map<string, Game[]>()); |
| 74 | + |
| 75 | + return [...gameNamesToGames.entries()].flatMap(([gameName, games]) => { |
| 76 | + if (games.length === 1) { |
| 77 | + return games[0]; |
| 78 | + } |
| 79 | + |
| 80 | + const roms = games.flatMap((game) => game.getRoms()); |
| 81 | + |
| 82 | + const romNamesToCount = roms.reduce((map, rom) => { |
| 83 | + map.set(rom.getName(), (map.get(rom.getName()) ?? 0) + 1); |
| 84 | + return map; |
| 85 | + }, new Map<string, number>()); |
| 86 | + const duplicateRomNames = [...romNamesToCount.entries()] |
| 87 | + .filter(([, count]) => count > 1) |
| 88 | + .map(([romName]) => romName) |
| 89 | + .sort(); |
| 90 | + if (duplicateRomNames.length > 1) { |
| 91 | + this.progressBar.logError( |
| 92 | + `${gameName}: can't group discs, games have duplicate ROM filenames:\n${duplicateRomNames.map((name) => ` ${name}`).join('\n')}`, |
| 93 | + ); |
| 94 | + return games; |
| 95 | + } |
| 96 | + |
| 97 | + return new Game({ |
| 98 | + name: gameName, |
| 99 | + rom: roms, |
| 100 | + }); |
| 101 | + }); |
| 102 | + } |
| 103 | +} |
0 commit comments