diff --git a/docs/usage/arcade.md b/docs/usage/arcade.md index a14424a7c..d8f4880a1 100644 --- a/docs/usage/arcade.md +++ b/docs/usage/arcade.md @@ -82,6 +82,12 @@ The ROM merge type can be specified with the `--merge-roms ` option: --merge-roms split ``` +## CHD disks + +As arcade machines got more complicated, their storage requirements grew beyond what ROM chips can handle cost effectively. Cabinets started embedding hard drives, optical drives, laser disc drives, and more. Because backup images of these media types can get large, the MAME developers created a new compression format called "compressed hunks of data" (CHD). + +MAME DATs catalog these "disks" separately from "ROMs", which lets users choose whether to care about them or not. Typically, games that require disks will not run without them, so Igir requires them for a game to be considered present/complete. You can use the `--exclude-disks` option to exclude disks and only process ROMs to save some space. + ## Example: building a new ROM set Let's say we want to build an arcade ROM set that's compatible with the most recent version of [RetroArch](desktop/retroarch.md). The steps would look like this: diff --git a/src/igir.ts b/src/igir.ts index 0cb7faad3..c3bf0b2eb 100644 --- a/src/igir.ts +++ b/src/igir.ts @@ -307,17 +307,33 @@ export default class Igir { } dats.forEach((dat) => { - const datMinimumBitmask = dat.getRequiredChecksumBitmask(); + const datMinimumRomBitmask = dat.getRequiredRomChecksumBitmask(); Object.keys(ChecksumBitmask) .filter((bitmask): bitmask is keyof typeof ChecksumBitmask => Number.isNaN(Number(bitmask))) // Has not been enabled yet .filter((bitmask) => ChecksumBitmask[bitmask] > minimumChecksum) .filter((bitmask) => !(matchChecksum & ChecksumBitmask[bitmask])) // Should be enabled for this DAT - .filter((bitmask) => datMinimumBitmask & ChecksumBitmask[bitmask]) + .filter((bitmask) => datMinimumRomBitmask & ChecksumBitmask[bitmask]) .forEach((bitmask) => { matchChecksum |= ChecksumBitmask[bitmask]; - this.logger.trace(`${dat.getNameShort()}: needs ${bitmask} file checksums, enabling`); + this.logger.trace(`${dat.getNameShort()}: needs ${bitmask} file checksums for ROMs, enabling`); + }); + + if (this.options.getExcludeDisks()) { + return; + } + const datMinimumDiskBitmask = dat.getRequiredDiskChecksumBitmask(); + Object.keys(ChecksumBitmask) + .filter((bitmask): bitmask is keyof typeof ChecksumBitmask => Number.isNaN(Number(bitmask))) + // Has not been enabled yet + .filter((bitmask) => ChecksumBitmask[bitmask] > minimumChecksum) + .filter((bitmask) => !(matchChecksum & ChecksumBitmask[bitmask])) + // Should be enabled for this DAT + .filter((bitmask) => datMinimumDiskBitmask & ChecksumBitmask[bitmask]) + .forEach((bitmask) => { + matchChecksum |= ChecksumBitmask[bitmask]; + this.logger.trace(`${dat.getNameShort()}: needs ${bitmask} file checksums for disks, enabling`); }); }); diff --git a/src/modules/argumentsParser.ts b/src/modules/argumentsParser.ts index 59d7f7bfe..f4707de9c 100644 --- a/src/modules/argumentsParser.ts +++ b/src/modules/argumentsParser.ts @@ -79,7 +79,7 @@ export default class ArgumentsParser { const groupRomZip = 'zip command options:'; const groupRomLink = 'link command options:'; const groupRomHeader = 'ROM header options:'; - const groupRomSet = 'ROM set options:'; + const groupRomSet = 'ROM set options (requires DATs):'; const groupRomFiltering = 'ROM filtering options:'; const groupRomPriority = 'One game, one ROM (1G1R) options:'; const groupReport = 'report command options:'; @@ -520,15 +520,30 @@ export default class ArgumentsParser { requiresArg: true, default: MergeMode[MergeMode.FULLNONMERGED].toLowerCase(), }) + .check((checkArgv) => { + // Re-implement `implies: 'dat'`, which isn't possible with a default value + if (checkArgv['merge-roms'] !== MergeMode[MergeMode.FULLNONMERGED].toLowerCase() && !checkArgv.dat) { + throw new ExpectedError('Missing dependent arguments:\n merge-roms -> dat'); + } + return true; + }) + .option('exclude-disks', { + group: groupRomSet, + description: 'Exclude CHD disks in DATs from processing & writing', + type: 'boolean', + implies: 'dat', + }) .option('allow-excess-sets', { group: groupRomSet, description: 'Allow writing archives that have excess files when not extracting or zipping', type: 'boolean', + implies: 'dat', }) .option('allow-incomplete-sets', { group: groupRomSet, description: 'Allow writing games that don\'t have all of their ROMs', type: 'boolean', + implies: 'dat', }) .option('filter-regex', { diff --git a/src/modules/candidateGenerator.ts b/src/modules/candidateGenerator.ts index 9a8b82694..f20f77804 100644 --- a/src/modules/candidateGenerator.ts +++ b/src/modules/candidateGenerator.ts @@ -120,9 +120,14 @@ export default class CandidateGenerator extends Module { ): Promise { const romsToInputFiles = this.getInputFilesForGame(dat, game, indexedFiles); + const gameRoms = [ + ...game.getRoms(), + ...(this.options.getExcludeDisks() ? [] : game.getDisks()), + ]; + // For each Game's ROM, find the matching File const romFiles = await Promise.all( - game.getRoms().map(async (rom) => { + gameRoms.map(async (rom) => { if (!romsToInputFiles.has(rom)) { return [rom, undefined]; } @@ -183,15 +188,18 @@ export default class CandidateGenerator extends Module { * Matches {@link ROMHeaderProcessor.getFileWithHeader} */ if (inputFile instanceof ArchiveEntry - && !this.options.shouldZipFile(rom.getName()) - && !this.options.shouldExtract() + && !this.options.shouldZipRom(rom) + && !this.options.shouldExtractRom(rom) ) { try { // Note: we're delaying checksum calculation for now, {@link CandidateArchiveFileHasher} // will handle it later inputFile = new ArchiveFile( inputFile.getArchive(), - { checksumBitmask: inputFile.getChecksumBitmask() }, + { + size: await fsPoly.size(inputFile.getFilePath()), + checksumBitmask: inputFile.getChecksumBitmask(), + }, ); } catch (error) { this.progressBar.logWarn(`${dat.getNameShort()}: ${game.getName()}: ${error}`); @@ -270,7 +278,12 @@ export default class CandidateGenerator extends Module { game: Game, indexedFiles: IndexedFiles, ): Map { - const romsAndInputFiles = game.getRoms().map((rom) => ([ + const gameRoms = [ + ...game.getRoms(), + ...(this.options.getExcludeDisks() ? [] : game.getDisks()), + ]; + + const romsAndInputFiles = gameRoms.map((rom) => ([ rom, indexedFiles.findFiles(rom) ?? [], ])) satisfies [ROM, File[]][]; @@ -311,8 +324,8 @@ export default class CandidateGenerator extends Module { // If there is a CHD with every .bin file, and we're raw-copying it, then assume its .cue // file is accurate return archive instanceof Chd - && !game.getRoms().some((rom) => this.options.shouldZipFile(rom.getName())) - && !this.options.shouldExtract() + && !game.getRoms().some((rom) => this.options.shouldZipRom(rom)) + && !game.getRoms().some((rom) => this.options.shouldExtractRom(rom)) && CandidateGenerator.onlyCueFilesMissingFromChd(game, roms) && this.options.getAllowExcessSets(); }) @@ -407,7 +420,7 @@ export default class CandidateGenerator extends Module { } // Determine the output file type - if (this.options.shouldZipFile(rom.getName())) { + if (this.options.shouldZipRom(rom)) { // Should zip, return an archive entry within an output zip return ArchiveEntry.entryOf({ archive: new Zip(outputFilePath), diff --git a/src/modules/candidateWriter.ts b/src/modules/candidateWriter.ts index cb11e8475..7bc55c5f7 100644 --- a/src/modules/candidateWriter.ts +++ b/src/modules/candidateWriter.ts @@ -482,7 +482,7 @@ export default class CandidateWriter extends Module { inputRomFile: File, outputFilePath: string, ): Promise { - this.progressBar.logInfo(`${dat.getNameShort()}: ${releaseCandidate.getName()}: copying file '${inputRomFile.toString()}' (${fsPoly.sizeReadable(inputRomFile.getSize())}) -> '${outputFilePath}'`); + this.progressBar.logInfo(`${dat.getNameShort()}: ${releaseCandidate.getName()}: ${inputRomFile instanceof ArchiveEntry ? 'extracting' : 'copying'} file '${inputRomFile.toString()}' (${fsPoly.sizeReadable(inputRomFile.getSize())}) -> '${outputFilePath}'`); try { await CandidateWriter.ensureOutputDirExists(outputFilePath); diff --git a/src/modules/datMergerSplitter.ts b/src/modules/datMergerSplitter.ts index bec1f68ae..cb7383456 100644 --- a/src/modules/datMergerSplitter.ts +++ b/src/modules/datMergerSplitter.ts @@ -65,6 +65,9 @@ export default class DATMergerSplitter extends Module { // 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())), + disk: game.getDisks() + // Get rid of ROMs that haven't been dumped yet + .filter((disk) => disk.getStatus() !== 'nodump'), })); // 'full' types expect device ROMs to be included @@ -115,33 +118,33 @@ export default class DATMergerSplitter extends Module { } return game.withProps({ - rom: DATMergerSplitter.diffGameRoms(biosGame, game), + rom: DATMergerSplitter.diffGameRoms(biosGame.getRoms(), game.getRoms()), }); }); } - // 'split' and 'merged' types should exclude ROMs found in their parent + // '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) => { - if (!game.getParent()) { - // This game doesn't have a parent - return game; - } + games = games.map((game) => { + if (!game.getParent()) { + // This game doesn't have a parent + return game; + } - const parentGame = gameNamesToGames.get(game.getParent()); - if (!parentGame) { - // Invalid cloneOf attribute, parent not found - this.progressBar.logTrace(`${dat.getNameShort()}: ${game.getName()} references an invalid parent: ${game.getParent()}`); - return game; - } + const parentGame = gameNamesToGames.get(game.getParent()); + if (!parentGame) { + // Invalid cloneOf attribute, parent not found + this.progressBar.logTrace(`${dat.getNameShort()}: ${game.getName()} references an invalid parent: ${game.getParent()}`); + return game; + } - return game.withProps({ - rom: DATMergerSplitter.diffGameRoms(parentGame, game), - }); + return game.withProps({ + rom: DATMergerSplitter.diffGameRoms(parentGame.getRoms(), game.getRoms()), + disk: DATMergerSplitter.diffGameRoms(parentGame.getDisks(), game.getDisks()), }); + }); } const parentGame = games.find((game) => game.isParent()); @@ -173,13 +176,13 @@ export default class DATMergerSplitter extends Module { })]; } - private static diffGameRoms(parent: Game, child: Game): ROM[] { - const parentRomNamesToHashCodes = parent.getRoms().reduce((map, rom) => { + private static diffGameRoms(parentRoms: ROM[], childRoms: ROM[]): ROM[] { + const parentRomNamesToHashCodes = parentRoms.reduce((map, rom) => { map.set(rom.getName(), rom.hashCode()); return map; }, new Map()); - return child.getRoms().filter((rom) => { + return childRoms.filter((rom) => { const parentName = rom.getMerge() ?? rom.getName(); const parentHashCode = parentRomNamesToHashCodes.get(parentName); if (!parentHashCode) { diff --git a/src/modules/datScanner.ts b/src/modules/datScanner.ts index 1565502c3..86d79639a 100644 --- a/src/modules/datScanner.ts +++ b/src/modules/datScanner.ts @@ -8,9 +8,15 @@ import DriveSemaphore from '../driveSemaphore.js'; import ArrayPoly from '../polyfill/arrayPoly.js'; import bufferPoly from '../polyfill/bufferPoly.js'; import fsPoly from '../polyfill/fsPoly.js'; -import CMProParser, { DATProps, GameProps, ROMProps } from '../types/dats/cmpro/cmProParser.js'; +import CMProParser, { + DATProps, + DiskProps, + GameProps, + ROMProps, +} from '../types/dats/cmpro/cmProParser.js'; import DAT from '../types/dats/dat.js'; import DATObject, { DATObjectProps } from '../types/dats/datObject.js'; +import Disk from '../types/dats/disk.js'; import Game from '../types/dats/game.js'; import Header from '../types/dats/logiqx/header.js'; import LogiqxDAT from '../types/dats/logiqx/logiqxDat.js'; @@ -333,6 +339,8 @@ export default class DATScanner extends Scanner { } const games = cmproDatGames.flatMap((game) => { + const gameName = game.name ?? game.comment; + let gameRoms: ROMProps[] = []; if (game.rom) { if (Array.isArray(game.rom)) { @@ -341,16 +349,29 @@ export default class DATScanner extends Scanner { gameRoms = [game.rom]; } } - const gameName = game.name ?? game.comment; + const roms = gameRoms.map((entry) => new ROM({ + name: entry.name ?? '', + size: Number.parseInt(entry.size ?? '0', 10), + crc32: entry.crc, + md5: entry.md5, + sha1: entry.sha1, + })); - const roms = gameRoms - .map((entry) => new ROM({ - name: entry.name ?? '', - size: Number.parseInt(entry.size ?? '0', 10), - crc32: entry.crc, - md5: entry.md5, - sha1: entry.sha1, - })); + let gameDisks: DiskProps[] = []; + if (game.disk) { + if (Array.isArray(game.disk)) { + gameDisks = game.disk; + } else { + gameDisks = [game.disk]; + } + } + const disks = gameDisks.map((entry) => new Disk({ + name: entry.name ?? '', + size: Number.parseInt(entry.size ?? '0', 10), + crc32: entry.crc, + md5: entry.md5, + sha1: entry.sha1, + })); return new Game({ name: gameName, @@ -365,6 +386,7 @@ export default class DATScanner extends Scanner { genre: game.genre?.toString(), release: undefined, rom: roms, + disk: disks, }); }); diff --git a/src/types/dats/cmpro/cmProParser.ts b/src/types/dats/cmpro/cmProParser.ts index d36de7633..71bc86a85 100644 --- a/src/types/dats/cmpro/cmProParser.ts +++ b/src/types/dats/cmpro/cmProParser.ts @@ -35,26 +35,26 @@ export interface GameProps extends CMProObject { sample?: SampleProps | SampleProps[], // NON-STANDARD PROPERTIES comment?: string, - serial?: string, - publisher?: string, - releaseyear?: string, - releasemonth?: string, - developer?: string, - users?: string, - esrbrating?: string, + // serial?: string, + // publisher?: string, + // releaseyear?: string, + // releasemonth?: string, + // developer?: string, + // users?: string, + // esrbrating?: string, genre?: string, } export interface ROMProps extends CMProObject { name?: string, - merge?: string, + // merge?: string, size?: string, crc?: string, - flags?: string, + // flags?: string, md5?: string, sha1?: string, // NON-STANDARD PROPERTIES - serial?: string, + // serial?: string, } export interface DiskProps extends ROMProps {} diff --git a/src/types/dats/dat.ts b/src/types/dats/dat.ts index 9d1b4aeb7..857439b9e 100644 --- a/src/types/dats/dat.ts +++ b/src/types/dats/dat.ts @@ -133,7 +133,7 @@ export default abstract class DAT { return this.getName().match(/\(headerless\)/i) !== null; } - getRequiredChecksumBitmask(): number { + getRequiredRomChecksumBitmask(): number { let checksumBitmask = 0; this.getGames().forEach((game) => game.getRoms().forEach((rom) => { if (rom.getCrc32() && rom.getSize()) { @@ -149,6 +149,22 @@ export default abstract class DAT { return checksumBitmask; } + getRequiredDiskChecksumBitmask(): number { + let checksumBitmask = 0; + this.getGames().forEach((game) => game.getDisks().forEach((disk) => { + if (disk.getCrc32() && disk.getSize()) { + checksumBitmask |= ChecksumBitmask.CRC32; + } else if (disk.getMd5()) { + checksumBitmask |= ChecksumBitmask.MD5; + } else if (disk.getSha1()) { + checksumBitmask |= ChecksumBitmask.SHA1; + } else if (disk.getSha256()) { + checksumBitmask |= ChecksumBitmask.SHA256; + } + })); + return checksumBitmask; + } + /** * Serialize this {@link DAT} to the file contents of an XML file. */ diff --git a/src/types/dats/disk.ts b/src/types/dats/disk.ts index 55b530ee1..4cd60a95e 100644 --- a/src/types/dats/disk.ts +++ b/src/types/dats/disk.ts @@ -1,11 +1,10 @@ -import { Expose } from 'class-transformer'; +import ROM, { ROMProps } from './rom.js'; -interface DiskOptions { - readonly name?: string; - readonly sha1?: string; - readonly md5?: string; - readonly merge?: string; - readonly status?: 'baddump' | 'nodump' | 'good' | 'verified'; +interface DiskProps extends Omit { + size?: number, + // region?: string, + // index?: number, + // writable?: 'yes' | 'no', } /** @@ -13,27 +12,11 @@ interface DiskOptions { * SHA1 do not both need to be specified in the data file:" * @see http://www.logiqx.com/DatFAQs/CMPro.php */ -export default class Disk implements DiskOptions { - @Expose({ name: 'name' }) - readonly name: string; - - @Expose({ name: 'sha1' }) - readonly sha1: string; - - @Expose({ name: 'md5' }) - readonly md5: string; - - @Expose({ name: 'merge' }) - readonly merge: string; - - @Expose({ name: 'status' }) - readonly status: 'baddump' | 'nodump' | 'good' | 'verified'; - - constructor(options?: DiskOptions) { - this.name = options?.name ?? ''; - this.sha1 = options?.sha1 ?? ''; - this.md5 = options?.md5 ?? ''; - this.merge = options?.merge ?? ''; - this.status = options?.status ?? 'good'; +export default class Disk extends ROM implements DiskProps { + constructor(props?: DiskProps) { + super(props ? { + ...props, + size: props?.size ?? 0, + } : undefined); } } diff --git a/src/types/dats/game.ts b/src/types/dats/game.ts index b2a65970f..ae882359d 100644 --- a/src/types/dats/game.ts +++ b/src/types/dats/game.ts @@ -4,6 +4,7 @@ import { Expose, Transform, Type } from 'class-transformer'; import ArrayPoly from '../../polyfill/arrayPoly.js'; import Internationalization from '../internationalization.js'; +import Disk from './disk.js'; import Release from './release.js'; import ROM from './rom.js'; @@ -67,7 +68,7 @@ export interface GameProps { // readonly manufacturer?: string, readonly release?: Release | Release[], readonly rom?: ROM | ROM[], - // readonly disk?: Disk | Disk[], + readonly disk?: Disk | Disk[], } /** @@ -124,6 +125,11 @@ export default class Game implements GameProps { @Transform(({ value }) => value || []) readonly rom?: ROM | ROM[]; + @Expose() + @Type(() => Disk) + @Transform(({ value }) => value || []) + readonly disk?: Disk | Disk[]; + constructor(props?: GameProps) { this.name = props?.name ?? ''; this.category = props?.category ?? ''; @@ -134,8 +140,9 @@ export default class Game implements GameProps { this.romOf = props?.romOf; this.sampleOf = props?.sampleOf; this.genre = props?.genre; - this.release = props?.release ?? []; - this.rom = props?.rom ?? []; + this.release = props?.release; + this.rom = props?.rom; + this.disk = props?.disk; } /** @@ -212,6 +219,15 @@ export default class Game implements GameProps { return []; } + getDisks(): Disk[] { + if (Array.isArray(this.disk)) { + return this.disk; + } if (this.disk) { + return [this.disk]; + } + return []; + } + // Computed getters getRevision(): number { diff --git a/src/types/files/archives/archive.ts b/src/types/files/archives/archive.ts index 8eece28b7..7e3d9f382 100644 --- a/src/types/files/archives/archive.ts +++ b/src/types/files/archives/archive.ts @@ -34,7 +34,7 @@ export default abstract class Archive { ): Promise { const tempFile = await fsPoly.mktemp(path.join( Temp.getTempDir(), - path.basename(entryPath), + fsPoly.makeLegal(path.basename(entryPath) || path.parse(this.getFilePath()).name), )); try { diff --git a/src/types/files/archives/archiveEntry.ts b/src/types/files/archives/archiveEntry.ts index 569d29579..d74dcd38a 100644 --- a/src/types/files/archives/archiveEntry.ts +++ b/src/types/files/archives/archiveEntry.ts @@ -31,7 +31,9 @@ export default class ArchiveEntry extends File implements Arc filePath: archiveEntryProps.archive.getFilePath(), }); this.archive = archiveEntryProps.archive; - this.entryPath = path.normalize(archiveEntryProps.entryPath); + this.entryPath = archiveEntryProps.entryPath + ? path.normalize(archiveEntryProps.entryPath) + : archiveEntryProps.entryPath; } static async entryOf( diff --git a/src/types/files/archives/chd/chd.ts b/src/types/files/archives/chd/chd.ts index c226c2106..3888e14fe 100644 --- a/src/types/files/archives/chd/chd.ts +++ b/src/types/files/archives/chd/chd.ts @@ -59,9 +59,13 @@ export default class Chd extends Archive { const rawEntry = await ArchiveEntry.entryOf({ archive: this, entryPath: '', - size: info.logicalSize, sha1: info.sha1, - }, ChecksumBitmask.NONE); + // There isn't a way for us to calculate these other checksums, so fill it in with garbage + size: 0, + crc32: checksumBitmask & ChecksumBitmask.CRC32 ? 'x'.repeat(8) : undefined, + md5: checksumBitmask & ChecksumBitmask.MD5 ? 'x'.repeat(32) : undefined, + sha256: checksumBitmask & ChecksumBitmask.SHA256 ? 'x'.repeat(64) : undefined, + }, checksumBitmask); const extractedEntry = await ArchiveEntry.entryOf({ archive: this, @@ -77,10 +81,24 @@ export default class Chd extends Archive { return [rawEntry, extractedEntry]; } - async extractEntryToStream( + async extractEntryToFile( + entryPath: string, + extractedFilePath: string, + ): Promise { + return this.extractEntryToStreamCached( + entryPath, + async (stream) => new Promise((resolve, reject) => { + const writeStream = fs.createWriteStream(extractedFilePath); + writeStream.on('close', resolve); + writeStream.on('error', reject); + stream.pipe(writeStream); + }), + ); + } + + private async extractEntryToStreamCached( entryPath: string, callback: (stream: Readable) => (Promise | T), - start: number = 0, ): Promise { await this.tempSingletonMutex.runExclusive(async () => { this.tempSingletonHandles += 1; @@ -139,12 +157,16 @@ export default class Chd extends Archive { const [extractedEntryPath, sizeAndOffset] = entryPath.split('|'); let filePath = this.tempSingletonFilePath as string; - if (await FsPoly.exists(path.join(this.tempSingletonDirPath as string, extractedEntryPath))) { + if (extractedEntryPath + && await FsPoly.exists(path.join(this.tempSingletonDirPath as string, extractedEntryPath)) + ) { + // The entry path is the name of a real extracted file, use that filePath = path.join(this.tempSingletonDirPath as string, extractedEntryPath); } + // Parse the entry path for any extra start/stop parameters const [trackSize, trackOffset] = (sizeAndOffset ?? '').split('@'); - const streamStart = Number.parseInt(trackOffset ?? '0', 10) + start; + const streamStart = Number.parseInt(trackOffset ?? '0', 10); const streamEnd = !trackSize || Number.isNaN(Number(trackSize)) ? undefined : Number.parseInt(trackOffset ?? '0', 10) + Number.parseInt(trackSize, 10) - 1; @@ -159,32 +181,19 @@ export default class Chd extends Archive { } catch (error) { throw new ExpectedError(`failed to read ${this.getFilePath()}|${entryPath} at ${filePath}: ${error}`); } finally { - await this.tempSingletonMutex.runExclusive(async () => { - this.tempSingletonHandles -= 1; - if (this.tempSingletonHandles <= 0) { - await FsPoly.rm(this.tempSingletonDirPath as string, { recursive: true, force: true }); - this.tempSingletonDirPath = undefined; - } - }); + // Give a grace period before deleting the temp file, the next read may be of the same file + setTimeout(async () => { + await this.tempSingletonMutex.runExclusive(async () => { + this.tempSingletonHandles -= 1; + if (this.tempSingletonHandles <= 0) { + await FsPoly.rm(this.tempSingletonDirPath as string, { recursive: true, force: true }); + this.tempSingletonDirPath = undefined; + } + }); + }, 5000); } } - // eslint-disable-next-line class-methods-use-this,@typescript-eslint/require-await - async extractEntryToFile( - entryPath: string, - extractedFilePath: string, - ): Promise { - return this.extractEntryToStream( - entryPath, - async (stream) => new Promise((resolve, reject) => { - const writeStream = fs.createWriteStream(extractedFilePath); - writeStream.on('close', resolve); - writeStream.on('error', reject); - stream.pipe(writeStream); - }), - ); - } - @Memoize() private async getInfo(): Promise { return chdman.info({ inputFilename: this.getFilePath() }); diff --git a/src/types/options.ts b/src/types/options.ts index 566788367..8d1e50fdb 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -19,6 +19,8 @@ import ArrayPoly from '../polyfill/arrayPoly.js'; import fsPoly, { FsWalkCallback } from '../polyfill/fsPoly.js'; import URLPoly from '../polyfill/urlPoly.js'; import DAT from './dats/dat.js'; +import Disk from './dats/disk.js'; +import ROM from './dats/rom.js'; import ExpectedError from './expectedError.js'; import File from './files/file.js'; import { ChecksumBitmask } from './files/fileChecksums.js'; @@ -105,6 +107,7 @@ export interface OptionsProps { readonly removeHeaders?: string[], readonly mergeRoms?: string, + readonly excludeDisks?: boolean, readonly allowExcessSets?: boolean, readonly allowIncompleteSets?: boolean, @@ -246,6 +249,8 @@ export default class Options implements OptionsProps { readonly mergeRoms?: string; + readonly excludeDisks: boolean; + readonly allowExcessSets: boolean; readonly allowIncompleteSets: boolean; @@ -408,6 +413,7 @@ export default class Options implements OptionsProps { this.removeHeaders = options?.removeHeaders; this.mergeRoms = options?.mergeRoms; + this.excludeDisks = options?.excludeDisks ?? false; this.allowExcessSets = options?.allowExcessSets ?? false; this.allowIncompleteSets = options?.allowIncompleteSets ?? false; @@ -553,6 +559,16 @@ export default class Options implements OptionsProps { return this.getCommands().has('extract'); } + /** + * Should a given ROM be extracted? + */ + shouldExtractRom(rom: ROM): boolean { + if (rom instanceof Disk) { + return false; + } + return this.shouldExtract(); + } + /** * Was the `zip` command provided? */ @@ -563,10 +579,14 @@ export default class Options implements OptionsProps { /** * Should a given output file path be zipped? */ - shouldZipFile(filePath: string): boolean { + shouldZipRom(rom: ROM): boolean { + if (rom instanceof Disk) { + return false; + } + return this.shouldZip() && (!this.getZipExclude() || !micromatch.isMatch( - filePath.replace(/^.[\\/]/, ''), + rom.getName().replace(/^.[\\/]/, ''), this.getZipExclude(), )); } @@ -1010,6 +1030,10 @@ export default class Options implements OptionsProps { return MergeMode[mergeMode as keyof typeof MergeMode]; } + getExcludeDisks(): boolean { + return this.excludeDisks; + } + getAllowExcessSets(): boolean { return this.allowExcessSets; } diff --git a/src/types/outputFactory.ts b/src/types/outputFactory.ts index f4b6584a0..817c12602 100644 --- a/src/types/outputFactory.ts +++ b/src/types/outputFactory.ts @@ -4,6 +4,7 @@ import path, { ParsedPath } from 'node:path'; import ArrayPoly from '../polyfill/arrayPoly.js'; import fsPoly from '../polyfill/fsPoly.js'; import DAT from './dats/dat.js'; +import Disk from './dats/disk.js'; import Game from './dats/game.js'; import Release from './dats/release.js'; import ROM from './dats/rom.js'; @@ -169,7 +170,7 @@ export default class OutputFactory { result = this.replaceGameTokens(result, game); result = this.replaceDatTokens(result, dat); result = this.replaceInputTokens(result, inputRomPath); - result = this.replaceOutputTokens(result, options, outputRomFilename); + result = this.replaceOutputTokens(result, options, game, outputRomFilename); result = this.replaceOutputGameConsoleTokens(result, dat, outputRomFilename); const leftoverTokens = result.match(/\{[a-zA-Z]+\}/g); @@ -256,6 +257,7 @@ export default class OutputFactory { private static replaceOutputTokens( input: string, options: Options, + game?: Game, outputRomFilename?: string, ): string { if (!outputRomFilename && options.getFixExtension() === FixExtension.NEVER) { @@ -268,7 +270,7 @@ export default class OutputFactory { return input .replace('{outputBasename}', outputRom.base) .replace('{outputName}', outputRom.name) - .replace('{outputExt}', outputRom.ext.replace(/^\./, '')); + .replace('{outputExt}', outputRom.ext.replace(/^\./, '') || '-'); } private static replaceOutputGameConsoleTokens( @@ -488,8 +490,10 @@ export default class OutputFactory { if ((options.getDirGameSubdir() === GameSubdirMode.MULTIPLE && game.getRoms().length > 1 // Output file is an archive - && !(FileFactory.isExtensionArchive(ext) || inputFile instanceof ArchiveFile)) + && !FileFactory.isExtensionArchive(ext) + && !(inputFile instanceof ArchiveFile)) || options.getDirGameSubdir() === GameSubdirMode.ALWAYS + || rom instanceof Disk ) { output = path.join(game.getName(), output); } @@ -509,7 +513,7 @@ export default class OutputFactory { inputFile: File, ): string { // Determine the output path of the file - if (options.shouldZipFile(rom.getName())) { + if (options.shouldZipRom(rom)) { // Should zip, generate the zip name from the game name return `${game.getName()}.zip`; } @@ -518,6 +522,7 @@ export default class OutputFactory { if (!(inputFile instanceof ArchiveEntry || inputFile instanceof ArchiveFile) || options.shouldExtract() + || rom instanceof Disk ) { // Should extract (if needed), generate the file name from the ROM name return romBasename; @@ -541,7 +546,7 @@ export default class OutputFactory { inputFile: File, ): string { const romBasename = this.getRomBasename(rom, inputFile); - if (!options.shouldZipFile(rom.getName())) { + if (!options.shouldZipRom(rom)) { return romBasename; } diff --git a/test/fixtures/dats/one.dat b/test/fixtures/dats/one.dat index 6d4d791e4..1d3e67d66 100644 --- a/test/fixtures/dats/one.dat +++ b/test/fixtures/dats/one.dat @@ -42,6 +42,8 @@ + + Device diff --git a/test/fixtures/roms/chd/2048.chd b/test/fixtures/roms/chd/2048.chd new file mode 100644 index 000000000..2fb9770e6 Binary files /dev/null and b/test/fixtures/roms/chd/2048.chd differ diff --git a/test/fixtures/roms/chd/4096.chd b/test/fixtures/roms/chd/4096.chd new file mode 100644 index 000000000..d7db6f941 Binary files /dev/null and b/test/fixtures/roms/chd/4096.chd differ diff --git a/test/igir.test.ts b/test/igir.test.ts index a275a97fc..b8f6ad209 100644 --- a/test/igir.test.ts +++ b/test/igir.test.ts @@ -180,6 +180,8 @@ describe('with explicit DATs', () => { [`${path.join('One', 'Lorem Ipsum.zip')}|loremipsum.rom`, '70856527'], [path.join('One', 'One Three', 'One.rom'), 'f817a89f'], [path.join('One', 'One Three', 'Three.rom'), 'ff46c5d8'], + [`${path.join('One', 'Three Four Five', '2048')}|`, 'xxxxxxxx'], // hard disk + [`${path.join('One', 'Three Four Five', '4096')}|`, 'xxxxxxxx'], // hard disk [path.join('One', 'Three Four Five', 'Five.rom'), '3e5daf67'], [path.join('One', 'Three Four Five', 'Four.rom'), '1cf3ca74'], [path.join('One', 'Three Four Five', 'Three.rom'), 'ff46c5d8'], @@ -233,6 +235,8 @@ describe('with explicit DATs', () => { ['Lorem Ipsum.zip|loremipsum.rom', '70856527'], [path.join('One Three', 'One.rom'), 'f817a89f'], [path.join('One Three', 'Three.rom'), 'ff46c5d8'], + [`${path.join('Three Four Five', '2048')}|`, 'xxxxxxxx'], // hard disk + [`${path.join('Three Four Five', '4096')}|`, 'xxxxxxxx'], // hard disk [path.join('Three Four Five', 'Five.rom'), '3e5daf67'], [path.join('Three Four Five', 'Four.rom'), '1cf3ca74'], [path.join('Three Four Five', 'Three.rom'), 'ff46c5d8'], @@ -272,6 +276,8 @@ describe('with explicit DATs', () => { }); expect(result.outputFilesAndCrcs).toEqual([ + [`${path.join('-', 'One', 'Three Four Five', '2048')}|`, 'xxxxxxxx'], // hard disk + [`${path.join('-', 'One', 'Three Four Five', '4096')}|`, 'xxxxxxxx'], // hard disk [`${path.join('7z', 'Headered', 'diagnostic_test_cartridge.a78.7z')}|diagnostic_test_cartridge.a78`, 'f6cc9b1c'], [path.join('bin', 'One', 'CD-ROM', 'CD-ROM (Track 1).bin'), '49ca35fb'], [path.join('bin', 'One', 'CD-ROM', 'CD-ROM (Track 2).bin'), '0316f720'], @@ -344,6 +350,7 @@ describe('with explicit DATs', () => { output: outputTemp, dirDatName: true, dirGameSubdir: GameSubdirMode[GameSubdirMode.MULTIPLE].toLowerCase(), + excludeDisks: true, }); expect(result.outputFilesAndCrcs).toEqual([ @@ -382,6 +389,7 @@ describe('with explicit DATs', () => { output: outputTemp, dirDatName: true, dirGameSubdir: GameSubdirMode[GameSubdirMode.MULTIPLE].toLowerCase(), + excludeDisks: true, }); expect(result.outputFilesAndCrcs).toEqual([ @@ -452,12 +460,16 @@ describe('with explicit DATs', () => { [path.join('igir combined', 'One Three', 'One.rom'), 'f817a89f'], [path.join('igir combined', 'One Three', 'Three.rom'), 'ff46c5d8'], [path.join('igir combined', 'speed_test_v51.smc'), '9adca6cc'], + [`${path.join('igir combined', 'Three Four Five', '2048')}|`, 'xxxxxxxx'], // hard disk + [`${path.join('igir combined', 'Three Four Five', '4096')}|`, 'xxxxxxxx'], // hard disk [path.join('igir combined', 'Three Four Five', 'Five.rom'), '3e5daf67'], [path.join('igir combined', 'Three Four Five', 'Four.rom'), '1cf3ca74'], [path.join('igir combined', 'Three Four Five', 'Three.rom'), 'ff46c5d8'], ]); expect(result.cwdFilesAndCrcs).toHaveLength(0); expect(result.movedFiles).toEqual([ + path.join('chd', '2048.chd'), + path.join('chd', '4096.chd'), path.join('discs', 'CD-ROM (Track 1).bin'), path.join('discs', 'CD-ROM (Track 2).bin'), path.join('discs', 'CD-ROM (Track 3).bin'), @@ -608,6 +620,8 @@ describe('with explicit DATs', () => { [`${path.join('One', 'Three Four Five.zip')}|Five.rom`, '3e5daf67'], [`${path.join('One', 'Three Four Five.zip')}|Four.rom`, '1cf3ca74'], [`${path.join('One', 'Three Four Five.zip')}|Three.rom`, 'ff46c5d8'], + [`${path.join('One', 'Three Four Five', '2048')}|`, 'xxxxxxxx'], // hard disk + [`${path.join('One', 'Three Four Five', '4096')}|`, 'xxxxxxxx'], // hard disk [`${path.join('Patchable', '0F09A40.zip')}|0F09A40.rom`, '2f943e86'], [`${path.join('Patchable', '3708F2C.zip')}|3708F2C.rom`, '20891c9f'], [`${path.join('Patchable', '612644F.zip')}|612644F.rom`, 'f7591b29'], @@ -687,6 +701,8 @@ describe('with explicit DATs', () => { ['Patchable.zip|Best.rom', '1e3d78cf'], ['Patchable.zip|C01173E.rom', 'dfaebe28'], ['Patchable.zip|KDULVQN.rom', 'b1c303e4'], + [`${path.join('Three Four Five', '2048')}|`, 'xxxxxxxx'], // hard disk + [`${path.join('Three Four Five', '4096')}|`, 'xxxxxxxx'], // hard disk ]); expect(result.cwdFilesAndCrcs).toHaveLength(0); expect(result.movedFiles).toHaveLength(0); @@ -731,6 +747,8 @@ describe('with explicit DATs', () => { [`${path.join('One', 'Lorem Ipsum.zip')}|loremipsum.rom -> ${path.join('', 'zip', 'loremipsum.zip')}|loremipsum.rom`, '70856527'], [`${path.join('One', 'One Three', 'One.rom')} -> ${path.join('', 'raw', 'one.rom')}`, 'f817a89f'], [`${path.join('One', 'One Three', 'Three.rom')} -> ${path.join('', 'raw', 'three.rom')}`, 'ff46c5d8'], + [`${path.join('One', 'Three Four Five', '2048')}| -> ${path.join('', 'chd', '2048.chd')}|`, 'xxxxxxxx'], // hard disk + [`${path.join('One', 'Three Four Five', '4096')}| -> ${path.join('', 'chd', '4096.chd')}|`, 'xxxxxxxx'], // hard disk [`${path.join('One', 'Three Four Five', 'Five.rom')} -> ${path.join('', 'raw', 'five.rom')}`, '3e5daf67'], [`${path.join('One', 'Three Four Five', 'Four.rom')} -> ${path.join('', 'raw', 'four.rom')}`, '1cf3ca74'], [`${path.join('One', 'Three Four Five', 'Three.rom')} -> ${path.join('', 'raw', 'three.rom')}`, 'ff46c5d8'], @@ -798,6 +816,8 @@ describe('with explicit DATs', () => { [`${path.join('One', 'Lorem Ipsum.zip')}|loremipsum.rom`, '70856527'], [path.join('One', 'One Three', 'One.rom'), 'f817a89f'], [path.join('One', 'One Three', 'Three.rom'), 'ff46c5d8'], + [`${path.join('One', 'Three Four Five', '2048')}|`, 'xxxxxxxx'], // hard disk + [`${path.join('One', 'Three Four Five', '4096')}|`, 'xxxxxxxx'], // hard disk [path.join('One', 'Three Four Five', 'Five.rom'), '3e5daf67'], [path.join('One', 'Three Four Five', 'Four.rom'), '1cf3ca74'], [path.join('One', 'Three Four Five', 'Three.rom'), 'ff46c5d8'], @@ -932,7 +952,9 @@ describe('with inferred DATs', () => { expect(result.outputFilesAndCrcs).toEqual([ ['0F09A40.rom', '2f943e86'], + ['2048.chd|', 'xxxxxxxx'], // hard disk ['3708F2C.rom', '20891c9f'], + ['4096.chd|', 'xxxxxxxx'], // hard disk ['5bc2ce5b.nkit.iso|5bc2ce5b.iso', '5bc2ce5b'], ['612644F.rom', 'f7591b29'], ['65D1206.rom', '20323455'], @@ -1112,7 +1134,9 @@ describe('with inferred DATs', () => { expect(result.outputFilesAndCrcs).toEqual([ ['0F09A40.zip|0F09A40.rom', '2f943e86'], + ['2048.zip|2048.rom', 'd774f042'], ['3708F2C.zip|3708F2C.rom', '20891c9f'], + ['4096.zip|4096.rom', '2e19ca09'], ['612644F.zip|612644F.rom', 'f7591b29'], ['65D1206.zip|65D1206.rom', '20323455'], ['92C85C9.zip|92C85C9.rom', '06692159'], @@ -1170,7 +1194,9 @@ describe('with inferred DATs', () => { expect(result.outputFilesAndCrcs).toEqual([ [`0F09A40.rom -> ${path.join('..', 'input', 'roms', 'patchable', '0F09A40.rom')}`, '2f943e86'], + [`2048.chd| -> ${path.join('..', 'input', 'roms', 'chd', '2048.chd|')}`, 'xxxxxxxx'], // hard disk [`3708F2C.rom -> ${path.join('..', 'input', 'roms', 'patchable', '3708F2C.rom')}`, '20891c9f'], + [`4096.chd| -> ${path.join('..', 'input', 'roms', 'chd', '4096.chd|')}`, 'xxxxxxxx'], // hard disk [`5bc2ce5b.nkit.iso|5bc2ce5b.iso -> ${path.join('..', 'input', 'roms', 'nkit', '5bc2ce5b.nkit.iso')}|5bc2ce5b.iso`, '5bc2ce5b'], [`612644F.rom -> ${path.join('..', 'input', 'roms', 'patchable', '612644F.rom')}`, 'f7591b29'], [`65D1206.rom -> ${path.join('..', 'input', 'roms', 'patchable', '65D1206.rom')}`, '20323455'], @@ -1287,7 +1313,9 @@ describe('with inferred DATs', () => { .sort(); expect(roms).toEqual([ '0F09A40.rom', + '2048.rom', '3708F2C.rom', + '4096.rom', '612644F.rom', '65D1206.rom', '92C85C9.rom', diff --git a/test/modules/argumentsParser.test.ts b/test/modules/argumentsParser.test.ts index de9dbba69..2a254b557 100644 --- a/test/modules/argumentsParser.test.ts +++ b/test/modules/argumentsParser.test.ts @@ -8,6 +8,7 @@ import ArgumentsParser from '../../src/modules/argumentsParser.js'; import FsPoly from '../../src/polyfill/fsPoly.js'; import Header from '../../src/types/dats/logiqx/header.js'; import LogiqxDAT from '../../src/types/dats/logiqx/logiqxDat.js'; +import ROM from '../../src/types/dats/rom.js'; import { ChecksumBitmask } from '../../src/types/files/fileChecksums.js'; import { FixExtension, @@ -52,7 +53,7 @@ describe('commands', () => { expect(argumentsParser.parse(['move', ...dummyRequiredArgs]).shouldCopy()).toEqual(false); expect(argumentsParser.parse(['copy', ...dummyRequiredArgs]).shouldMove()).toEqual(false); expect(argumentsParser.parse(['copy', ...dummyRequiredArgs]).shouldExtract()).toEqual(false); - expect(argumentsParser.parse(['copy', ...dummyRequiredArgs]).shouldZipFile('')).toEqual(false); + expect(argumentsParser.parse(['copy', ...dummyRequiredArgs]).shouldZipRom(new ROM({ name: '', size: 0 }))).toEqual(false); expect(argumentsParser.parse(['copy', ...dummyRequiredArgs]).shouldTest()).toEqual(false); expect(argumentsParser.parse(['copy', ...dummyRequiredArgs]).shouldDir2Dat()).toEqual(false); expect(argumentsParser.parse(['copy', ...dummyRequiredArgs]).shouldFixdat()).toEqual(false); @@ -65,7 +66,7 @@ describe('commands', () => { expect(argumentsParser.parse(datCommands).shouldCopy()).toEqual(true); expect(argumentsParser.parse(datCommands).shouldMove()).toEqual(false); expect(argumentsParser.parse(datCommands).shouldExtract()).toEqual(true); - expect(argumentsParser.parse(datCommands).shouldZipFile('')).toEqual(false); + expect(argumentsParser.parse(datCommands).shouldZipRom(new ROM({ name: '', size: 0 }))).toEqual(false); expect(argumentsParser.parse(datCommands).shouldTest()).toEqual(true); expect(argumentsParser.parse(datCommands).shouldDir2Dat()).toEqual(false); expect(argumentsParser.parse(datCommands).shouldFixdat()).toEqual(false); @@ -76,7 +77,7 @@ describe('commands', () => { expect(argumentsParser.parse(nonDatCommands).shouldCopy()).toEqual(true); expect(argumentsParser.parse(nonDatCommands).shouldMove()).toEqual(false); expect(argumentsParser.parse(nonDatCommands).shouldExtract()).toEqual(true); - expect(argumentsParser.parse(nonDatCommands).shouldZipFile('')).toEqual(false); + expect(argumentsParser.parse(nonDatCommands).shouldZipRom(new ROM({ name: '', size: 0 }))).toEqual(false); expect(argumentsParser.parse(nonDatCommands).shouldTest()).toEqual(true); expect(argumentsParser.parse(nonDatCommands).shouldDir2Dat()).toEqual(true); expect(argumentsParser.parse(nonDatCommands).shouldFixdat()).toEqual(false); @@ -87,7 +88,7 @@ describe('commands', () => { expect(argumentsParser.parse(moveZip).shouldCopy()).toEqual(false); expect(argumentsParser.parse(moveZip).shouldMove()).toEqual(true); expect(argumentsParser.parse(moveZip).shouldExtract()).toEqual(false); - expect(argumentsParser.parse(moveZip).shouldZipFile('')).toEqual(true); + expect(argumentsParser.parse(moveZip).shouldZipRom(new ROM({ name: '', size: 0 }))).toEqual(true); expect(argumentsParser.parse(moveZip).shouldTest()).toEqual(true); expect(argumentsParser.parse(moveZip).shouldDir2Dat()).toEqual(false); expect(argumentsParser.parse(moveZip).shouldFixdat()).toEqual(true); @@ -154,6 +155,7 @@ describe('options', () => { expect(options.getSymlinkRelative()).toEqual(false); expect(options.getMergeRoms()).toEqual(MergeMode.FULLNONMERGED); + expect(options.getExcludeDisks()).toEqual(false); expect(options.getAllowExcessSets()).toEqual(false); expect(options.getAllowIncompleteSets()).toEqual(false); @@ -580,12 +582,12 @@ describe('options', () => { }); it('should parse "zip-exclude"', () => { - const filePath = 'roms/test.rom'; - expect(argumentsParser.parse(['copy', 'zip', '--input', os.devNull, '--output', os.devNull]).shouldZipFile(filePath)).toEqual(true); - expect(argumentsParser.parse(['copy', 'zip', '--input', os.devNull, '--output', os.devNull, '-Z', os.devNull]).shouldZipFile(filePath)).toEqual(true); - expect(argumentsParser.parse(['copy', 'zip', '--input', os.devNull, '--output', os.devNull, '-Z', '**/*']).shouldZipFile(filePath)).toEqual(false); - expect(argumentsParser.parse(['copy', 'zip', '--input', os.devNull, '--output', os.devNull, '-Z', '**/*.rom']).shouldZipFile(filePath)).toEqual(false); - expect(argumentsParser.parse(['copy', 'zip', '--input', os.devNull, '--output', os.devNull, '--zip-exclude', '**/*.rom']).shouldZipFile(filePath)).toEqual(false); + const rom = new ROM({ name: 'roms/test.rom', size: 0 }); + expect(argumentsParser.parse(['copy', 'zip', '--input', os.devNull, '--output', os.devNull]).shouldZipRom(rom)).toEqual(true); + expect(argumentsParser.parse(['copy', 'zip', '--input', os.devNull, '--output', os.devNull, '-Z', os.devNull]).shouldZipRom(rom)).toEqual(true); + expect(argumentsParser.parse(['copy', 'zip', '--input', os.devNull, '--output', os.devNull, '-Z', '**/*']).shouldZipRom(rom)).toEqual(false); + expect(argumentsParser.parse(['copy', 'zip', '--input', os.devNull, '--output', os.devNull, '-Z', '**/*.rom']).shouldZipRom(rom)).toEqual(false); + expect(argumentsParser.parse(['copy', 'zip', '--input', os.devNull, '--output', os.devNull, '--zip-exclude', '**/*.rom']).shouldZipRom(rom)).toEqual(false); }); it('should parse "zip-dat-name"', () => { @@ -794,32 +796,45 @@ describe('options', () => { }); it('should parse "merge-roms"', () => { + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--merge-roms', 'merged'])).toThrow(/dependent|implication/i); expect(argumentsParser.parse(dummyCommandAndRequiredArgs).getMergeRoms()) .toEqual(MergeMode.FULLNONMERGED); expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--merge-roms', 'foobar']).getMergeRoms()).toThrow(/invalid values/i); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--merge-roms', 'fullnonmerged']).getMergeRoms()).toEqual(MergeMode.FULLNONMERGED); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--merge-roms', 'nonmerged']).getMergeRoms()).toEqual(MergeMode.NONMERGED); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--merge-roms', 'split']).getMergeRoms()).toEqual(MergeMode.SPLIT); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--merge-roms', 'merged']).getMergeRoms()).toEqual(MergeMode.MERGED); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--merge-roms', 'merged', '--merge-roms', 'split']).getMergeRoms()).toEqual(MergeMode.SPLIT); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--merge-roms', 'fullnonmerged']).getMergeRoms()).toEqual(MergeMode.FULLNONMERGED); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--merge-roms', 'nonmerged']).getMergeRoms()).toEqual(MergeMode.NONMERGED); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--merge-roms', 'split']).getMergeRoms()).toEqual(MergeMode.SPLIT); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--merge-roms', 'merged']).getMergeRoms()).toEqual(MergeMode.MERGED); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--merge-roms', 'merged', '--merge-roms', 'split']).getMergeRoms()).toEqual(MergeMode.SPLIT); + }); + + it('should parse "exclude-disks"', () => { + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--exclude-disks'])).toThrow(/dependent|implication/i); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--exclude-disks']).getExcludeDisks()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--exclude-disks', 'true']).getExcludeDisks()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--exclude-disks', 'false']).getExcludeDisks()).toEqual(false); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--exclude-disks', '--exclude-disks']).getExcludeDisks()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--exclude-disks', 'false', '--exclude-disks', 'true']).getExcludeDisks()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--exclude-disks', 'true', '--exclude-disks', 'false']).getExcludeDisks()).toEqual(false); }); it('should parse "allow-excess-sets"', () => { - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--allow-excess-sets']).getAllowExcessSets()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--allow-excess-sets', 'true']).getAllowExcessSets()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--allow-excess-sets', 'false']).getAllowExcessSets()).toEqual(false); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--allow-excess-sets', '--allow-excess-sets']).getAllowExcessSets()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--allow-excess-sets', 'false', '--allow-excess-sets', 'true']).getAllowExcessSets()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--allow-excess-sets', 'true', '--allow-excess-sets', 'false']).getAllowExcessSets()).toEqual(false); + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--allow-excess-sets'])).toThrow(/dependent|implication/i); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--allow-excess-sets']).getAllowExcessSets()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--allow-excess-sets', 'true']).getAllowExcessSets()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--allow-excess-sets', 'false']).getAllowExcessSets()).toEqual(false); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--allow-excess-sets', '--allow-excess-sets']).getAllowExcessSets()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--allow-excess-sets', 'false', '--allow-excess-sets', 'true']).getAllowExcessSets()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--allow-excess-sets', 'true', '--allow-excess-sets', 'false']).getAllowExcessSets()).toEqual(false); }); it('should parse "allow-incomplete-sets"', () => { - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--allow-incomplete-sets']).getAllowIncompleteSets()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--allow-incomplete-sets', 'true']).getAllowIncompleteSets()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--allow-incomplete-sets', 'false']).getAllowIncompleteSets()).toEqual(false); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--allow-incomplete-sets', '--allow-incomplete-sets']).getAllowIncompleteSets()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--allow-incomplete-sets', 'false', '--allow-incomplete-sets', 'true']).getAllowIncompleteSets()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--allow-incomplete-sets', 'true', '--allow-incomplete-sets', 'false']).getAllowIncompleteSets()).toEqual(false); + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--allow-incomplete-sets'])).toThrow(/dependent|implication/i); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--allow-incomplete-sets']).getAllowIncompleteSets()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--allow-incomplete-sets', 'true']).getAllowIncompleteSets()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--allow-incomplete-sets', 'false']).getAllowIncompleteSets()).toEqual(false); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--allow-incomplete-sets', '--allow-incomplete-sets']).getAllowIncompleteSets()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--allow-incomplete-sets', 'false', '--allow-incomplete-sets', 'true']).getAllowIncompleteSets()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--allow-incomplete-sets', 'true', '--allow-incomplete-sets', 'false']).getAllowIncompleteSets()).toEqual(false); }); it('should parse "filter-regex"', async () => { diff --git a/test/modules/candidateGenerator.test.ts b/test/modules/candidateGenerator.test.ts index 05de2c29a..c10db7b49 100644 --- a/test/modules/candidateGenerator.test.ts +++ b/test/modules/candidateGenerator.test.ts @@ -2,10 +2,14 @@ import path from 'node:path'; import CandidateGenerator from '../../src/modules/candidateGenerator.js'; import ROMIndexer from '../../src/modules/romIndexer.js'; +import ArrayPoly from '../../src/polyfill/arrayPoly.js'; import DAT from '../../src/types/dats/dat.js'; +import Disk from '../../src/types/dats/disk.js'; import Game from '../../src/types/dats/game.js'; import Header from '../../src/types/dats/logiqx/header.js'; import LogiqxDAT from '../../src/types/dats/logiqx/logiqxDat.js'; +import Machine from '../../src/types/dats/mame/machine.js'; +import MameDAT from '../../src/types/dats/mame/mameDat.js'; import Parent from '../../src/types/dats/parent.js'; import Release from '../../src/types/dats/release.js'; import ROM from '../../src/types/dats/rom.js'; @@ -16,6 +20,7 @@ import Tar from '../../src/types/files/archives/tar.js'; import Zip from '../../src/types/files/archives/zip.js'; import File from '../../src/types/files/file.js'; import ROMHeader from '../../src/types/files/romHeader.js'; +import IndexedFiles from '../../src/types/indexedFiles.js'; import Options, { GameSubdirMode } from '../../src/types/options.js'; import ReleaseCandidate from '../../src/types/releaseCandidate.js'; import ProgressBarFake from '../console/progressBarFake.js'; @@ -700,3 +705,130 @@ describe.each(['copy', 'move'])('raw writing: %s', (command) => { }); }); }); + +describe('MAME v0.260', () => { + const mameDat = new MameDAT([ + new Machine({ + name: '2spicy', + romOf: 'lindbios', + description: '2 Spicy', + rom: [ + new ROM({ name: '6.0.0010a.bin', size: 1_048_576, crc32: '10dd9b76' }), + new ROM({ name: '6.0.0009.bin', size: 1_048_576, crc32: '5ffdfbf8' }), + new ROM({ name: '6.0.0010.bin', size: 1_048_576, crc32: 'ea2bf888' }), + new ROM({ name: 'fpr-24370b.ic6', size: 4_194_304, crc32: 'c3b021a4' }), + new ROM({ name: 'vid_bios.u504', size: 65_536, crc32: 'f78d14d7' }), + // new ROM({ name: '317-0491-com.bin', size: 8192 }), + ], + disk: [ + new Disk({ name: 'mda-c0004a_revb_lindyellow_v2.4.20_mvl31a_boot_2.01', sha1: 'e13da5f827df852e742b594729ee3f933b387410' }), + new Disk({ name: 'dvp-0027a', sha1: 'da1aacee9e32e813844f4d434981e69cc5c80682' }), + ], + }), + new Machine({ + name: 'area51mx', + description: 'Area 51 / Maximum Force Duo v2.0', + rom: [ + new ROM({ name: '2.0_68020_max-a51_kit_3h.3h', size: 524_288, crc32: '47cbf30b' }), + new ROM({ name: '2.0_68020_max-a51_kit_3p.3p', size: 524_288, crc32: 'a3c93684' }), + new ROM({ name: '2.0_68020_max-a51_kit_3m.3m', size: 524_288, crc32: 'd800ac17' }), + new ROM({ name: '2.0_68020_max-a51_kit_3k.3k', size: 524_288, crc32: '0e78f308' }), + new ROM({ name: 'jagwave.rom', size: 4096, crc32: '7a25ee5b' }), + ], + disk: new Disk({ name: 'area51mx', sha1: '5ff10f4e87094d4449eabf3de7549564ca568c7e' }), + }), + new Machine({ + name: 'a51mxr3k', + cloneOf: 'area51mx', + romOf: 'area51mx', + description: 'Area 51 / Maximum Force Duo (R3000, 2/10/98)', + rom: [ + new ROM({ name: '1.0_r3k_max-a51_kit_hh.hh', size: 524_288, crc32: 'a984dab2' }), + new ROM({ name: '1.0_r3k_max-a51_kit_hl.hl', size: 524_288, crc32: '0af49d74' }), + new ROM({ name: '1.0_r3k_max-a51_kit_lh.lh', size: 524_288, crc32: 'd7d94dac' }), + new ROM({ name: '1.0_r3k_max-a51_kit_ll.ll', size: 524_288, crc32: 'ece9e5ae' }), + new ROM({ name: 'jagwave.rom', size: 4096, crc32: '7a25ee5b' }), + ], + disk: new Disk({ name: 'area51mx', sha1: '5ff10f4e87094d4449eabf3de7549564ca568c7e' }), + }), + ]); + + const mameIndexedFiles = Promise.all( + mameDat.getGames() + .flatMap((game) => [...game.getRoms(), ...game.getDisks()]) + .map(async (rom) => rom.toFile()), + ) + .then((files) => files.filter(ArrayPoly.filterUniqueMapped((file) => file.hashCode()))) + .then((files) => IndexedFiles.fromFiles(files)); + + it('should include disks by default', async () => { + const options = new Options({ + commands: ['copy', 'zip'], + dirGameSubdir: GameSubdirMode[GameSubdirMode.MULTIPLE].toLowerCase(), + }); + + const candidates = await new CandidateGenerator(options, new ProgressBarFake()) + .generate(mameDat, await mameIndexedFiles); + + const outputFiles = [...candidates.values()] + .flat() + .flatMap((releaseCandidate) => releaseCandidate.getRomsWithFiles()) + .map((romWithFiles) => romWithFiles.getOutputFile().toString()) + .sort(); + expect(outputFiles).toEqual([ + '2spicy.zip|6.0.0009.bin', + '2spicy.zip|6.0.0010.bin', + '2spicy.zip|6.0.0010a.bin', + '2spicy.zip|fpr-24370b.ic6', + '2spicy.zip|vid_bios.u504', + path.join('2spicy', 'dvp-0027a'), + path.join('2spicy', 'mda-c0004a_revb_lindyellow_v2.4.20_mvl31a_boot_2.01'), + 'a51mxr3k.zip|1.0_r3k_max-a51_kit_hh.hh', + 'a51mxr3k.zip|1.0_r3k_max-a51_kit_hl.hl', + 'a51mxr3k.zip|1.0_r3k_max-a51_kit_lh.lh', + 'a51mxr3k.zip|1.0_r3k_max-a51_kit_ll.ll', + 'a51mxr3k.zip|jagwave.rom', + path.join('a51mxr3k', 'area51mx'), + 'area51mx.zip|2.0_68020_max-a51_kit_3h.3h', + 'area51mx.zip|2.0_68020_max-a51_kit_3k.3k', + 'area51mx.zip|2.0_68020_max-a51_kit_3m.3m', + 'area51mx.zip|2.0_68020_max-a51_kit_3p.3p', + 'area51mx.zip|jagwave.rom', + path.join('area51mx', 'area51mx'), + ]); + }); + + it('should not include disks', async () => { + const options = new Options({ + commands: ['copy'], + dirGameSubdir: GameSubdirMode[GameSubdirMode.MULTIPLE].toLowerCase(), + excludeDisks: true, + }); + + const candidates = await new CandidateGenerator(options, new ProgressBarFake()) + .generate(mameDat, await mameIndexedFiles); + + const outputFiles = [...candidates.values()] + .flat() + .flatMap((releaseCandidate) => releaseCandidate.getRomsWithFiles()) + .map((romWithFiles) => romWithFiles.getOutputFile().toString()) + .sort(); + expect(outputFiles).toEqual([ + path.join('2spicy', '6.0.0009.bin'), + path.join('2spicy', '6.0.0010.bin'), + path.join('2spicy', '6.0.0010a.bin'), + path.join('2spicy', 'fpr-24370b.ic6'), + path.join('2spicy', 'vid_bios.u504'), + path.join('a51mxr3k', '1.0_r3k_max-a51_kit_hh.hh'), + path.join('a51mxr3k', '1.0_r3k_max-a51_kit_hl.hl'), + path.join('a51mxr3k', '1.0_r3k_max-a51_kit_lh.lh'), + path.join('a51mxr3k', '1.0_r3k_max-a51_kit_ll.ll'), + path.join('a51mxr3k', 'jagwave.rom'), + path.join('area51mx', '2.0_68020_max-a51_kit_3h.3h'), + path.join('area51mx', '2.0_68020_max-a51_kit_3k.3k'), + path.join('area51mx', '2.0_68020_max-a51_kit_3m.3m'), + path.join('area51mx', '2.0_68020_max-a51_kit_3p.3p'), + path.join('area51mx', 'jagwave.rom'), + ]); + }); +}); diff --git a/test/modules/candidateWriter.test.ts b/test/modules/candidateWriter.test.ts index 172268cb9..e6042b80e 100644 --- a/test/modules/candidateWriter.test.ts +++ b/test/modules/candidateWriter.test.ts @@ -4,6 +4,7 @@ import path from 'node:path'; import Temp from '../../src/globals/temp.js'; import CandidateCombiner from '../../src/modules/candidateCombiner.js'; +import CandidateExtensionCorrector from '../../src/modules/candidateExtensionCorrector.js'; import CandidateGenerator from '../../src/modules/candidateGenerator.js'; import CandidatePatchGenerator from '../../src/modules/candidatePatchGenerator.js'; import CandidateWriter from '../../src/modules/candidateWriter.js'; @@ -114,6 +115,8 @@ async function candidateWriter( candidates = await new CandidatePatchGenerator(new ProgressBarFake()) .generate(dat, candidates, patches); } + candidates = await new CandidateExtensionCorrector(options, new ProgressBarFake()) + .correct(dat, candidates); candidates = await new CandidateCombiner(options, new ProgressBarFake()) .combine(dat, candidates); @@ -435,7 +438,7 @@ describe('zip', () => { test.each([ [ '**/!(header*)/*', - ['0F09A40.zip', '3708F2C.zip', '612644F.zip', '65D1206.zip', '92C85C9.zip', 'C01173E.zip', 'CD-ROM.zip', 'GD-ROM.zip', 'KDULVQN.zip', 'before.zip', 'best.zip', 'empty.zip', 'five.zip', 'fizzbuzz.zip', 'foobar.zip', 'four.zip', 'fourfive.zip', 'loremipsum.zip', 'one.zip', 'onetwothree.zip', 'three.zip', 'two.zip', 'unknown.zip'], + ['0F09A40.zip', '2048.zip', '3708F2C.zip', '4096.zip', '612644F.zip', '65D1206.zip', '92C85C9.zip', 'C01173E.zip', 'CD-ROM.zip', 'GD-ROM.zip', 'KDULVQN.zip', 'before.zip', 'best.zip', 'empty.zip', 'five.zip', 'fizzbuzz.zip', 'foobar.zip', 'four.zip', 'fourfive.zip', 'loremipsum.zip', 'one.zip', 'onetwothree.zip', 'three.zip', 'two.zip', 'unknown.zip'], ], [ '7z/*', @@ -485,7 +488,7 @@ describe('zip', () => { test.each([ [ '**/!(header*)/*', - ['0F09A40.zip', '3708F2C.zip', '612644F.zip', '65D1206.zip', '92C85C9.zip', 'C01173E.zip', 'CD-ROM.zip', 'GD-ROM.zip', 'KDULVQN.zip', 'before.zip', 'best.zip', 'empty.zip', 'five.zip', 'fizzbuzz.zip', 'foobar.zip', 'four.zip', 'fourfive.zip', 'loremipsum.zip', 'one.zip', 'onetwothree.zip', 'three.zip', 'two.zip', 'unknown.zip'], + ['0F09A40.zip', '2048.zip', '3708F2C.zip', '4096.zip', '612644F.zip', '65D1206.zip', '92C85C9.zip', 'C01173E.zip', 'CD-ROM.zip', 'GD-ROM.zip', 'KDULVQN.zip', 'before.zip', 'best.zip', 'empty.zip', 'five.zip', 'fizzbuzz.zip', 'foobar.zip', 'four.zip', 'fourfive.zip', 'loremipsum.zip', 'one.zip', 'onetwothree.zip', 'three.zip', 'two.zip', 'unknown.zip'], ['patchable/0F09A40.rom', 'patchable/3708F2C.rom', 'patchable/612644F.rom', 'patchable/65D1206.rom', 'patchable/92C85C9.rom', 'patchable/C01173E.rom', 'patchable/KDULVQN.rom', 'patchable/before.rom', 'patchable/best.gz', 'raw/empty.rom', 'raw/fizzbuzz.nes', 'raw/foobar.lnx', 'raw/loremipsum.rom', 'raw/one.rom', 'raw/three.rom', 'raw/two.rom', 'raw/unknown.rom'], ], [ @@ -743,7 +746,7 @@ describe('extract', () => { it('should write if the output is not expected and overwriting invalid', async () => { await copyFixturesToTemp(async (inputTemp, outputTemp) => { // Given - const options = new Options({ commands: ['copy', 'extract'] }); + const options = new Options({ commands: ['copy', 'extract'], writerThreads: 1 }); const inputFilesBefore = await walkAndStat(inputTemp); await expect(walkAndStat(outputTemp)).resolves.toHaveLength(0); @@ -882,7 +885,7 @@ describe('extract', () => { test.each([ [ '**/!(header*)/*', - ['0F09A40.rom', '3708F2C.rom', '612644F.rom', '65D1206.rom', '92C85C9.rom', 'C01173E.rom', + ['0F09A40.rom', '2048.rom', '3708F2C.rom', '4096.rom', '612644F.rom', '65D1206.rom', '92C85C9.rom', 'C01173E.rom', path.join('CD-ROM', 'CD-ROM (Track 1).bin'), path.join('CD-ROM', 'CD-ROM (Track 2).bin'), path.join('CD-ROM', 'CD-ROM (Track 3).bin'), path.join('CD-ROM', 'CD-ROM.cue'), path.join('GD-ROM', 'track.gdi'), path.join('GD-ROM', 'track01.bin'), path.join('GD-ROM', 'track02.raw'), path.join('GD-ROM', 'track03.bin'), path.join('GD-ROM', 'track04.bin'), 'KDULVQN.rom', 'before.rom', 'best.rom', 'empty.rom', 'five.rom', 'fizzbuzz.nes', 'foobar.lnx', 'four.rom', path.join('fourfive', 'five.rom'), path.join('fourfive', 'four.rom'), 'loremipsum.rom', 'one.rom', path.join('onetwothree', 'one.rom'), path.join('onetwothree', 'three.rom'), path.join('onetwothree', 'two.rom'), 'three.rom', @@ -939,7 +942,7 @@ describe('extract', () => { test.each([ [ '**/!(header*)/*', - ['0F09A40.rom', '3708F2C.rom', '612644F.rom', '65D1206.rom', '92C85C9.rom', 'C01173E.rom', + ['0F09A40.rom', '2048.rom', '3708F2C.rom', '4096.rom', '612644F.rom', '65D1206.rom', '92C85C9.rom', 'C01173E.rom', path.join('CD-ROM', 'CD-ROM (Track 1).bin'), path.join('CD-ROM', 'CD-ROM (Track 2).bin'), path.join('CD-ROM', 'CD-ROM (Track 3).bin'), path.join('CD-ROM', 'CD-ROM.cue'), path.join('GD-ROM', 'track.gdi'), path.join('GD-ROM', 'track01.bin'), path.join('GD-ROM', 'track02.raw'), path.join('GD-ROM', 'track03.bin'), path.join('GD-ROM', 'track04.bin'), 'KDULVQN.rom', 'before.rom', 'best.rom', 'empty.rom', 'five.rom', 'fizzbuzz.nes', 'foobar.lnx', 'four.rom', path.join('fourfive', 'five.rom'), path.join('fourfive', 'four.rom'), 'loremipsum.rom', 'one.rom', path.join('onetwothree', 'one.rom'), path.join('onetwothree', 'three.rom'), path.join('onetwothree', 'two.rom'), 'three.rom', @@ -1249,7 +1252,7 @@ describe('raw', () => { test.each([ [ '**/!(header*)/*', - ['0F09A40.rom', '3708F2C.rom', '612644F.rom', '65D1206.rom', '92C85C9.rom', 'C01173E.rom', + ['0F09A40.rom', '2048.chd', '3708F2C.rom', '4096.chd', '612644F.rom', '65D1206.rom', '92C85C9.rom', 'C01173E.rom', 'CD-ROM (Track 1).bin', 'CD-ROM (Track 2).bin', 'CD-ROM (Track 3).bin', 'CD-ROM.chd', 'CD-ROM.cue', 'GD-ROM.chd', 'KDULVQN.rom', 'before.rom', 'best.gz', 'empty.rom', 'five.rom', 'fizzbuzz.nes', 'foobar.lnx', 'four.rom', 'fourfive.zip', 'loremipsum.rom', 'one.rom', 'onetwothree.zip', 'three.rom', @@ -1303,7 +1306,7 @@ describe('raw', () => { test.each([ [ '**/!(header*)/*', - ['0F09A40.rom', '3708F2C.rom', '612644F.rom', '65D1206.rom', '92C85C9.rom', 'C01173E.rom', + ['0F09A40.rom', '2048.chd', '3708F2C.rom', '4096.chd', '612644F.rom', '65D1206.rom', '92C85C9.rom', 'C01173E.rom', 'CD-ROM (Track 1).bin', 'CD-ROM (Track 2).bin', 'CD-ROM (Track 3).bin', 'CD-ROM.chd', 'CD-ROM.cue', 'GD-ROM.chd', 'KDULVQN.rom', 'before.rom', 'best.gz', 'empty.rom', 'five.rom', 'fizzbuzz.nes', 'foobar.lnx', 'four.rom', 'fourfive.zip', 'loremipsum.rom', 'one.rom', 'onetwothree.zip', 'three.rom', diff --git a/test/modules/datGameInferrer.test.ts b/test/modules/datGameInferrer.test.ts index d23624b6a..42cba418d 100644 --- a/test/modules/datGameInferrer.test.ts +++ b/test/modules/datGameInferrer.test.ts @@ -5,9 +5,9 @@ import ProgressBarFake from '../console/progressBarFake.js'; test.each([ // One input path - [['test/fixtures/roms/**/*'], { roms: 32 }], + [['test/fixtures/roms/**/*'], { roms: 34 }], [['test/fixtures/roms/7z/*'], { '7z': 5 }], - [['test/fixtures/roms/chd/*'], { chd: 2 }], + [['test/fixtures/roms/chd/*'], { chd: 4 }], [['test/fixtures/roms/discs/*'], { discs: 2 }], [['test/fixtures/roms/gz/*'], { gz: 7 }], [['test/fixtures/roms/headered/*'], { headered: 6 }], diff --git a/test/modules/datMergerSplitter.test.ts b/test/modules/datMergerSplitter.test.ts index 9e4be81c7..ac281819e 100644 --- a/test/modules/datMergerSplitter.test.ts +++ b/test/modules/datMergerSplitter.test.ts @@ -1,6 +1,7 @@ import 'jest-extended'; import DATMergerSplitter from '../../src/modules/datMergerSplitter.js'; +import Disk from '../../src/types/dats/disk.js'; import Game from '../../src/types/dats/game.js'; import Header from '../../src/types/dats/logiqx/header.js'; import LogiqxDAT from '../../src/types/dats/logiqx/logiqxDat.js'; @@ -627,6 +628,50 @@ describe('MAME v0.258', () => { ], }), + new Machine({ + // Game with two disks + name: '2spicy', + romOf: 'lindbios', + description: '2 Spicy', + rom: [ + new ROM({ name: '317-0491-com.bin', size: 8192, status: 'nodump' }), + new ROM({ name: '6.0.0009.bin', merge: '6.0.0009.bin', size: 1048576, crc32: '5ffdfbf8', sha1: '605bc4967b749b4e6d13fc2ebb845ba956a259a7' }), + new ROM({ name: '6.0.0010.bin', merge: '6.0.0010.bin', size: 1048576, crc32: 'ea2bf888', sha1: 'c9c5b6f0d4f4f36620939b15dd2f128a74347e37' }), + new ROM({ name: '6.0.0010a.bin', merge: '6.0.0010a.bin', size: 1048576, crc32: '10dd9b76', sha1: '1fdf1f921bc395846a7c3180fbdbc4ca287a9670' }), + new ROM({ name: 'fpr-24370b.ic6', merge: 'fpr-24370b.ic6', size: 4194304, crc32: 'c3b021a4', sha1: '1b6938a50fe0e4ae813864649eb103838c399ac0' }), + new ROM({ name: 'vid_bios.u504', merge: 'vid_bios.u504', size: 65536, crc32: 'f78d14d7', sha1: 'f129787e487984edd23bf344f2e9500c85052275' }), + ], + disk: [ + new Disk({ name: 'dvp-0027a', sha1: 'da1aacee9e32e813844f4d434981e69cc5c80682' }), + new Disk({ name: 'mda-c0004a_revb_lindyellow_v2.4.20_mvl31a_boot_2.01', merge: 'mda-c0004a_revb_lindyellow_v2.4.20_mvl31a_boot_2.01', sha1: 'e13da5f827df852e742b594729ee3f933b387410' }), + ], + deviceRef: [ + new DeviceRef('pentium4'), + new DeviceRef('pci_root'), + new DeviceRef('i82875p_host'), + new DeviceRef('i82875p_agp'), + new DeviceRef('geforce_7600gs'), + new DeviceRef('i82875p_overflow'), + new DeviceRef('pci_bridge'), + new DeviceRef('i82541_device'), + new DeviceRef('usb_uhci'), + new DeviceRef('usb_uhci'), + new DeviceRef('i6300esb_watchdog'), + new DeviceRef('apic'), + new DeviceRef('usb_ehci'), + new DeviceRef('pci_bridge'), + new DeviceRef('sb0400'), + new DeviceRef('lindbergh_baseboard'), + new DeviceRef('i6300esb_lpc'), + new DeviceRef('lpc_acpi'), + new DeviceRef('lpc_rpc'), + new DeviceRef('lpc_pit'), + new DeviceRef('sata'), + new DeviceRef('smbus'), + new DeviceRef('ac97'), + ], + }), + new Machine({ // Game with BIOS files but no romOf BIOS parent name: 'aes', @@ -671,6 +716,77 @@ describe('MAME v0.258', () => { ], }), + new Machine({ + // Game with one disk + name: 'area51mx', + description: 'Area 51 / Maximum Force Duo v2.0', + rom: [ + new ROM({ name: '2.0_68020_max-a51_kit_3h.3h', size: 524288, crc32: '47cbf30b', sha1: '23377bcc65c0fc330d5bc7e76e233bae043ac364' }), + new ROM({ name: '2.0_68020_max-a51_kit_3k.3k', size: 524288, crc32: '0e78f308', sha1: 'adc4c8e441eb8fe525d0a6220eb3a2a8791a7289' }), + new ROM({ name: '2.0_68020_max-a51_kit_3m.3m', size: 524288, crc32: 'd800ac17', sha1: '3d515c8608d8101ee9227116175b3c3f1fe22e0c' }), + new ROM({ name: '2.0_68020_max-a51_kit_3p.3p', size: 524288, crc32: 'a3c93684', sha1: 'f6b3357bb69900a176fd6bc6b819b2f57b7d0f59' }), + new ROM({ name: 'jagwave.rom', size: 4096, crc32: '7a25ee5b', sha1: '58117e11fd6478c521fbd3fdbe157f39567552f0' }), + ], + disk: [ + new Disk({ name: 'area51mx', sha1: '5ff10f4e87094d4449eabf3de7549564ca568c7e' }), + ], + deviceRef: [ + new DeviceRef('m68ec020'), + new DeviceRef('jaguargpu'), + new DeviceRef('jaguardsp'), + new DeviceRef('jag_blitter'), + new DeviceRef('nvram'), + new DeviceRef('watchdog'), + new DeviceRef('vt83c461'), + new DeviceRef('ata_slot'), + new DeviceRef('cojag_hdd'), + new DeviceRef('harddisk_image'), + new DeviceRef('ata_slot'), + new DeviceRef('screen'), + new DeviceRef('palette'), + new DeviceRef('speaker'), + new DeviceRef('speaker'), + new DeviceRef('dac_16bit_r2r_tc'), + new DeviceRef('dac_16bit_r2r_tc'), + ], + }), + new Machine({ + // Clone of a game with the same disk as its parent + name: 'a51mxr3k', + cloneOf: 'area51mx', + romOf: 'area51mx', + description: 'Area 51 / Maximum Force Duo (R3000, 2/10/98)', + rom: [ + new ROM({ name: '1.0_r3k_max-a51_kit_hh.hh', size: 524288, crc32: 'a984dab2', sha1: 'debb3bc11ff49e87a52e89a69533a1bab7db700e' }), + new ROM({ name: '1.0_r3k_max-a51_kit_hl.hl', size: 524288, crc32: '0af49d74', sha1: 'c19f26056a823fd32293e9a7b3ea868640eabf49' }), + new ROM({ name: '1.0_r3k_max-a51_kit_lh.lh', size: 524288, crc32: 'd7d94dac', sha1: '2060a74715f36a0d7f5dd0855eda48ad1f20f095' }), + new ROM({ name: '1.0_r3k_max-a51_kit_ll.ll', size: 524288, crc32: 'ece9e5ae', sha1: '7e44402726f5afa6d1670b27aa43ad13d21c4ad9' }), + new ROM({ name: 'jagwave.rom', merge: 'jagwave.rom', size: 4096, crc32: '7a25ee5b', sha1: '58117e11fd6478c521fbd3fdbe157f39567552f0' }), + ], + disk: [ + new Disk({ name: 'area51mx', merge: 'area51mx', sha1: '5ff10f4e87094d4449eabf3de7549564ca568c7e' }), + ], + deviceRef: [ + new DeviceRef('r3041'), + new DeviceRef('jaguargpu'), + new DeviceRef('jaguardsp'), + new DeviceRef('jag_blitter'), + new DeviceRef('nvram'), + new DeviceRef('watchdog'), + new DeviceRef('vt83c461'), + new DeviceRef('ata_slot'), + new DeviceRef('cojag_hdd'), + new DeviceRef('harddisk_image'), + new DeviceRef('ata_slot'), + new DeviceRef('screen'), + new DeviceRef('palette'), + new DeviceRef('speaker'), + new DeviceRef('speaker'), + new DeviceRef('dac_16bit_r2r_tc'), + new DeviceRef('dac_16bit_r2r_tc'), + ], + }), + new Machine({ // Game with no clones name: 'bbtime', @@ -1357,6 +1473,46 @@ describe('MAME v0.258', () => { new DeviceRef('palette'), ], }), + new Machine({ + name: 'lindbios', + bios: 'yes', + description: 'Sega Lindbergh BIOS', + rom: [ + new ROM({ name: '6.0.0009.bin', size: 1048576, crc32: '5ffdfbf8', sha1: '605bc4967b749b4e6d13fc2ebb845ba956a259a7' }), + new ROM({ name: '6.0.0010.bin', size: 1048576, crc32: 'ea2bf888', sha1: 'c9c5b6f0d4f4f36620939b15dd2f128a74347e37' }), + new ROM({ name: '6.0.0010a.bin', size: 1048576, crc32: '10dd9b76', sha1: '1fdf1f921bc395846a7c3180fbdbc4ca287a9670' }), + new ROM({ name: 'fpr-24370b.ic6', size: 4194304, crc32: 'c3b021a4', sha1: '1b6938a50fe0e4ae813864649eb103838c399ac0' }), + new ROM({ name: 'vid_bios.u504', size: 65536, crc32: 'f78d14d7', sha1: 'f129787e487984edd23bf344f2e9500c85052275' }), + ], + disk: [ + new Disk({ name: 'mda-c0004a_revb_lindyellow_v2.4.20_mvl31a_boot_2.01', sha1: 'e13da5f827df852e742b594729ee3f933b387410' }), + ], + deviceRef: [ + new DeviceRef('ac97'), + new DeviceRef('pci_root'), + new DeviceRef('i82875p_host'), + new DeviceRef('i82875p_agp'), + new DeviceRef('geforce_7600gs'), + new DeviceRef('i82875p_overflow'), + new DeviceRef('pci_bridge'), + new DeviceRef('i82541_device'), + new DeviceRef('usb_uhci'), + new DeviceRef('usb_uhci'), + new DeviceRef('i6300esb_watchdog'), + new DeviceRef('apic'), + new DeviceRef('usb_ehci'), + new DeviceRef('pci_bridge'), + new DeviceRef('sb0400'), + new DeviceRef('lindbergh_baseboard'), + new DeviceRef('i6300esb_lpc'), + new DeviceRef('lpc_acpi'), + new DeviceRef('lpc_rpc'), + new DeviceRef('lpc_pit'), + new DeviceRef('sata'), + new DeviceRef('smbus'), + new DeviceRef('ac97'), + ], + }), // ***** Devices ***** new Machine({ name: '93c46_16', device: 'yes' }), @@ -1574,8 +1730,14 @@ describe('MAME v0.258', () => { return map; }, new Map()); + const gameNamesToDiskNames = result.getGames() + .reduce((map, game) => { + map.set(game.getName(), game.getDisks().map((disk) => disk.getName().replace(/[\\/]/g, '\\'))); + return map; + }, new Map()); + // Includes BIOS files - expect(gameNamesToRomNames.get('100lions')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('100lions')).toIncludeSameMembers([ '01.02.08_left.u3', '01.02.08_right.u2', '01.03.03a_left.u70', '01.03.03a_right.u83', '01.03.03e_left.u70', '01.03.03e_right.u83', '01.03.05_left.u70', '01.03.05_right.u83', '01.03.06_left.u70', '01.03.06_right.u83', '01.03.07_left.u70', '01.03.07_right.u83', @@ -1600,7 +1762,7 @@ describe('MAME v0.258', () => { '21012901_left.u70', '21012901_right.u83', '24010467_left.u70', '24010467_right.u83', '24013001_left.u70', '24013001_right.u83', '25012805_left.u70', '25012805_right.u83', ]); - expect(gameNamesToRomNames.get('100lionsa')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('100lionsa')).toIncludeSameMembers([ '01.02.08_left.u3', '01.02.08_right.u2', '01.03.03a_left.u70', '01.03.03a_right.u83', '01.03.03e_left.u70', '01.03.03e_right.u83', '01.03.05_left.u70', '01.03.05_right.u83', '01.03.06_left.u70', '01.03.06_right.u83', '01.03.07_left.u70', '01.03.07_right.u83', @@ -1625,53 +1787,56 @@ describe('MAME v0.258', () => { '21012901_right.u83', '24010467_left.u70', '24010467_right.u83', '24013001_left.u70', '24013001_right.u83', '25012805_left.u70', '25012805_right.u83', '30223811.u73', '30223811.u86', ]); - expect(gameNamesToRomNames.get('1942')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942')).toIncludeSameMembers([ 'sb-0.f1', 'sb-1.k6', 'sb-2.d1', 'sb-3.d2', 'sb-4.d6', 'sb-5.e8', 'sb-6.e9', 'sb-7.e10', 'sb-8.k3', 'sb-9.m11', 'sr-01.c11', 'sr-02.f2', 'sr-08.a1', 'sr-09.a2', 'sr-10.a3', 'sr-11.a4', 'sr-12.a5', 'sr-13.a6', 'sr-14.l1', 'sr-15.l2', 'sr-16.n1', 'sr-17.n2', 'srb-03.m3', 'srb-04.m4', 'srb-05.m5', 'srb-06.m6', 'srb-07.m7', ]); - expect(gameNamesToRomNames.get('1942a')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942a')).toIncludeSameMembers([ 'sb-0.f1', 'sb-1.k6', 'sb-2.d1', 'sb-3.d2', 'sb-4.d6', 'sb-5.e8', 'sb-6.e9', 'sb-7.e10', 'sb-8.k3', 'sb-9.m11', 'sr-01.c11', 'sr-02.f2', 'sr-04.m4', 'sr-05.m5', 'sr-06.m6', 'sr-07.m7', 'sr-08.a1', 'sr-09.a2', 'sr-10.a3', 'sr-11.a4', 'sr-12.a5', 'sr-13.a6', 'sr-14.l1', 'sr-15.l2', 'sr-16.n1', 'sr-17.n2', 'sra-03.m3', ]); - expect(gameNamesToRomNames.get('1942abl')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942abl')).toIncludeSameMembers([ '1.bin', '2.bin', '3.bin', '5.bin', '7.bin', '9.bin', '11.bin', '13.bin', '14.bin', '16.bin', 'sb-0.f1', 'sb-1.k6', 'sb-2.d1', 'sb-3.d2', 'sb-4.d6', 'sb-5.e8', 'sb-6.e9', 'sb-7.e10', 'sb-8.k3', 'sb-9.m11', ]); - expect(gameNamesToRomNames.get('1942b')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942b')).toIncludeSameMembers([ 'sb-0.f1', 'sb-1.k6', 'sb-2.d1', 'sb-3.d2', 'sb-4.d6', 'sb-5.e8', 'sb-6.e9', 'sb-7.e10', 'sb-8.k3', 'sb-9.m11', 'sr-01.c11', 'sr-02.f2', 'sr-03.m3', 'sr-04.m4', 'sr-05.m5', 'sr-06.m6', 'sr-07.m7', 'sr-08.a1', 'sr-09.a2', 'sr-10.a3', 'sr-11.a4', 'sr-12.a5', 'sr-13.a6', 'sr-14.l1', 'sr-15.l2', 'sr-16.n1', 'sr-17.n2', ]); - expect(gameNamesToRomNames.get('1942h')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942h')).toIncludeSameMembers([ 'sb-0.f1', 'sb-1.k6', 'sb-2.d1', 'sb-3.d2', 'sb-4.d6', 'sb-5.e8', 'sb-6.e9', 'sb-7.e10', 'sb-8.k3', 'sb-9.m11', 'sr-01.c11', 'sr-02.f2', 'sr-08.a1', 'sr-09.a2', 'sr-10.a3', 'sr-11.a4', 'sr-12.a5', 'sr-13.a6', 'sr-14.l1', 'sr-15.l2', 'sr-16.n1', 'sr-17.n2', 'srb-06.m6', 'srb-07.m7', 'supercharger_1942_@3.m3', 'supercharger_1942_@4.m4', 'supercharger_1942_@5.m5', ]); - expect(gameNamesToRomNames.get('1942p')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942p')).toIncludeSameMembers([ '1.bin', '2.bin', '3.bin', '04.bin', '5.bin', '6.bin', '7.bin', '8.bin', '9.bin', '10.bin', '11.bin', '12.bin', 'ic22.bin', ]); - expect(gameNamesToRomNames.get('1942w')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942w')).toIncludeSameMembers([ 'sb-0.f1', 'sb-1.k6', 'sb-2.d1', 'sb-3.d2', 'sb-4.d6', 'sb-5.e8', 'sb-6.e9', 'sb-7.e10', 'sb-8.k3', 'sb-9.m11', 'sr-01.c11', 'sr-08.a1', 'sr-09.a2', 'sr-10.a3', 'sr-11.a4', 'sr-12.a5', 'sr-13.a6', 'sr-14.l1', 'sr-15.l2', 'sr-16.n1', 'sr-17.n2', 'sw-02.f2', 'sw-03.m3', 'sw-04.m4', 'sw-05.m5', 'sw-06.m6', 'sw-07.m7', ]); - expect(gameNamesToRomNames.get('aes')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('2spicy')).toIncludeSameMembers([ + '6.0.0009.bin', '6.0.0010.bin', '6.0.0010a.bin', 'fpr-24370b.ic6', 'vid_bios.u504', + ]); + expect(gameNamesToRomNames.get('aes')).toIncludeSameMembers([ '000-lo.lo', 'neo-epo.bin', 'neo-po.bin', 'neodebug.rom', 'uni-bios_1_3.rom', 'uni-bios_2_0.rom', 'uni-bios_2_1.rom', 'uni-bios_2_2.rom', 'uni-bios_2_3.rom', 'uni-bios_2_3o.rom', 'uni-bios_3_0.rom', 'uni-bios_3_1.rom', 'uni-bios_3_2.rom', 'uni-bios_3_3.rom', 'uni-bios_4_0.rom', ]); - expect(gameNamesToRomNames.get('bbtime')).toIncludeAllMembers(['bbtime.svg', 'hd38820a65']); - expect(gameNamesToRomNames.get('c64')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('bbtime')).toIncludeSameMembers(['bbtime.svg', 'hd38820a65']); + expect(gameNamesToRomNames.get('c64')).toIncludeSameMembers([ '325302-01.uab4', '901225-01.u5', '901226-01.u3', '901227-01.u4', '901227-02.u4', '901227-03.u4', '901229-01.uab5', '901229-02.uab5', '901229-03.uab5', '901229-05 ae.uab5', '901229-06 aa.uab5', '906114-01.u17', 'digidos.u4', 'digidos.uab5', 'dosrom12.u4', 'exos3.u4', @@ -1682,66 +1847,77 @@ describe('MAME v0.258', () => { 'turboaccess26.u4', 'turboaccess301.u4', 'turboaccess302.u4', 'turboprocess.u4', 'turboprocessus.u4', 'turborom2.u4', 'turborom.u4', ]); - expect(gameNamesToRomNames.get('ddonpach')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('ddonpach')).toIncludeSameMembers([ 'b1.u27', 'b2.u26', 'eeprom-ddonpach.bin', 'u6.bin', 'u7.bin', 'u50.bin', 'u51.bin', 'u52.bin', 'u53.bin', 'u60.bin', 'u61.bin', 'u62.bin', ]); - expect(gameNamesToRomNames.get('ddonpacha')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('ddonpacha')).toIncludeSameMembers([ 'arrange_u26.bin', 'arrange_u27.bin', 'arrange_u51.bin', 'arrange_u62.bin', 'eeprom-ddonpach.bin', 'u6.bin', 'u7.bin', 'u50.bin', 'u52.bin', 'u53.bin', 'u60.bin', 'u61.bin', ]); - expect(gameNamesToRomNames.get('ddonpachj')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('ddonpachj')).toIncludeSameMembers([ 'eeprom-ddonpach.bin', 'u6.bin', 'u7.bin', 'u26.bin', 'u27.bin', 'u50.bin', 'u51.bin', 'u52.bin', 'u53.bin', 'u60.bin', 'u61.bin', 'u62.bin', ]); + // Includes device ROMs - expect(gameNamesToRomNames.get('galaga')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('area51mx')).toIncludeSameMembers([ + '2.0_68020_max-a51_kit_3h.3h', '2.0_68020_max-a51_kit_3k.3k', '2.0_68020_max-a51_kit_3m.3m', + '2.0_68020_max-a51_kit_3p.3p', 'jagwave.rom', + ]); + expect(gameNamesToDiskNames.get('area51mx')).toIncludeSameMembers(['area51mx']); + expect(gameNamesToRomNames.get('a51mxr3k')).toIncludeSameMembers([ + '1.0_r3k_max-a51_kit_hh.hh', '1.0_r3k_max-a51_kit_hl.hl', '1.0_r3k_max-a51_kit_lh.lh', + '1.0_r3k_max-a51_kit_ll.ll', 'jagwave.rom', + ]); + expect(gameNamesToDiskNames.get('a51mxr3k')).toIncludeSameMembers(['area51mx']); + expect(gameNamesToRomNames.get('galaga')).toIncludeSameMembers([ '51xx.bin', '54xx.bin', 'gg1_1b.3p', 'gg1_2b.3m', 'gg1_3.2m', 'gg1_4b.2l', 'gg1_5b.3f', 'gg1_7b.2c', 'gg1_9.4l', 'gg1_10.4f', 'gg1_11.4d', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('galagamf')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('galagamf')).toIncludeSameMembers([ '51xx.bin', '54xx.bin', '2600j.bin', '2700k.bin', '2800l.bin', '3200a.bin', '3300b.bin', '3400c.bin', '3500d.bin', '3600fast.bin', '3700g.bin', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('galagamk')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('galagamk')).toIncludeSameMembers([ '51xx.bin', '54xx.bin', '3400c.bin', 'gg1-5.3f', 'gg1-7b.2c', 'gg1-9.4l', 'gg1-10.4f', 'gg1-11.4d', 'mk2-1', 'mk2-2', 'mk2-4', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('galagamw')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('galagamw')).toIncludeSameMembers([ '51xx.bin', '54xx.bin', '2600j.bin', '2700k.bin', '2800l.bin', '3200a.bin', '3300b.bin', '3400c.bin', '3500d.bin', '3600e.bin', '3700g.bin', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('galagao')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('galagao')).toIncludeSameMembers([ '51xx.bin', '54xx.bin', 'gg1-1.3p', 'gg1-2.3m', 'gg1-3.2m', 'gg1-4.2l', 'gg1-5.3f', 'gg1-7.2c', 'gg1-9.4l', 'gg1-10.4f', 'gg1-11.4d', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('gallag')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('gallag')).toIncludeSameMembers([ '51xx.bin', 'gallag.1', 'gallag.2', 'gallag.3', 'gallag.4', 'gallag.5', 'gallag.6', 'gallag.7', 'gallag.8', 'gallag.9', 'gallag.a', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('gatsbee')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('gatsbee')).toIncludeSameMembers([ '1.4b', '2.4c', '3.4d', '4.4e', '8.5r', '9.6a', '10.7a', '51xx.bin', '54xx.bin', 'gallag.6', 'gg1-5.3f', 'gg1-7.2c', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('nebulbee')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('nebulbee')).toIncludeSameMembers([ '1c.bin', '1d.bin', '2n.bin', '5c.bin', '51xx.bin', 'gg1-5', 'gg1-7', 'gg1_3.2m', 'gg1_9.4l', 'gg1_10.4f', 'gg1_11.4d', 'nebulbee.01', 'nebulbee.02', 'nebulbee.04', 'nebulbee.07', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('liblrabl')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('liblrabl')).toIncludeSameMembers([ '2c.rom', '5b.rom', '5c.rom', '5p.rom', '8c.rom', '9t.rom', '10c.rom', 'lr1-1.1t', 'lr1-2.1s', 'lr1-3.1r', 'lr1-4.3d', 'lr1-5.5l', 'lr1-6.2p', ]); // No change to BIOS or devices - expect(result.getGames().filter((game) => game.isBios())).toHaveLength(2); + expect(result.getGames().filter((game) => game.isBios())).toHaveLength(3); expect(result.getGames().filter((game) => game.isDevice())).toHaveLength(65); expect(gameNamesToRomNames.get('aristmk6')).toHaveLength(96); expect(gameNamesToRomNames.get('neogeo')).toHaveLength(34); @@ -1766,56 +1942,73 @@ describe('MAME v0.258', () => { return map; }, new Map()); + const gameNamesToDiskNames = result.getGames() + .reduce((map, game) => { + map.set(game.getName(), game.getDisks().map((disk) => disk.getName().replace(/[\\/]/g, '\\'))); + return map; + }, new Map()); + // Excludes device files - expect(gameNamesToRomNames.get('100lions')).toIncludeAllMembers(['10219211.u73', '10219211.u86']); - expect(gameNamesToRomNames.get('100lionsa')).toIncludeAllMembers(['30223811.u73', '30223811.u86']); - expect(gameNamesToRomNames.get('1942')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('100lions')).toIncludeSameMembers(['10219211.u73', '10219211.u86']); + expect(gameNamesToRomNames.get('100lionsa')).toIncludeSameMembers(['30223811.u73', '30223811.u86']); + expect(gameNamesToRomNames.get('1942')).toIncludeSameMembers([ 'sb-0.f1', 'sb-1.k6', 'sb-2.d1', 'sb-3.d2', 'sb-4.d6', 'sb-5.e8', 'sb-6.e9', 'sb-7.e10', 'sb-8.k3', 'sb-9.m11', 'sr-01.c11', 'sr-02.f2', 'sr-08.a1', 'sr-09.a2', 'sr-10.a3', 'sr-11.a4', 'sr-12.a5', 'sr-13.a6', 'sr-14.l1', 'sr-15.l2', 'sr-16.n1', 'sr-17.n2', 'srb-03.m3', 'srb-04.m4', 'srb-05.m5', 'srb-06.m6', 'srb-07.m7', ]); - expect(gameNamesToRomNames.get('1942a')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942a')).toIncludeSameMembers([ 'sb-0.f1', 'sb-1.k6', 'sb-2.d1', 'sb-3.d2', 'sb-4.d6', 'sb-5.e8', 'sb-6.e9', 'sb-7.e10', 'sb-8.k3', 'sb-9.m11', 'sr-01.c11', 'sr-02.f2', 'sr-04.m4', 'sr-05.m5', 'sr-06.m6', 'sr-07.m7', 'sr-08.a1', 'sr-09.a2', 'sr-10.a3', 'sr-11.a4', 'sr-12.a5', 'sr-13.a6', 'sr-14.l1', 'sr-15.l2', 'sr-16.n1', 'sr-17.n2', 'sra-03.m3', ]); - expect(gameNamesToRomNames.get('1942abl')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942abl')).toIncludeSameMembers([ '1.bin', '2.bin', '3.bin', '5.bin', '7.bin', '9.bin', '11.bin', '13.bin', '14.bin', '16.bin', 'sb-0.f1', 'sb-1.k6', 'sb-2.d1', 'sb-3.d2', 'sb-4.d6', 'sb-5.e8', 'sb-6.e9', 'sb-7.e10', 'sb-8.k3', 'sb-9.m11', ]); - expect(gameNamesToRomNames.get('1942b')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942b')).toIncludeSameMembers([ 'sb-0.f1', 'sb-1.k6', 'sb-2.d1', 'sb-3.d2', 'sb-4.d6', 'sb-5.e8', 'sb-6.e9', 'sb-7.e10', 'sb-8.k3', 'sb-9.m11', 'sr-01.c11', 'sr-02.f2', 'sr-03.m3', 'sr-04.m4', 'sr-05.m5', 'sr-06.m6', 'sr-07.m7', 'sr-08.a1', 'sr-09.a2', 'sr-10.a3', 'sr-11.a4', 'sr-12.a5', 'sr-13.a6', 'sr-14.l1', 'sr-15.l2', 'sr-16.n1', 'sr-17.n2', ]); - expect(gameNamesToRomNames.get('1942h')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942h')).toIncludeSameMembers([ 'sb-0.f1', 'sb-1.k6', 'sb-2.d1', 'sb-3.d2', 'sb-4.d6', 'sb-5.e8', 'sb-6.e9', 'sb-7.e10', 'sb-8.k3', 'sb-9.m11', 'sr-01.c11', 'sr-02.f2', 'sr-08.a1', 'sr-09.a2', 'sr-10.a3', 'sr-11.a4', 'sr-12.a5', 'sr-13.a6', 'sr-14.l1', 'sr-15.l2', 'sr-16.n1', 'sr-17.n2', 'srb-06.m6', 'srb-07.m7', 'supercharger_1942_@3.m3', 'supercharger_1942_@4.m4', 'supercharger_1942_@5.m5', ]); - expect(gameNamesToRomNames.get('1942p')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942p')).toIncludeSameMembers([ '1.bin', '2.bin', '3.bin', '04.bin', '5.bin', '6.bin', '7.bin', '8.bin', '9.bin', '10.bin', '11.bin', '12.bin', 'ic22.bin', ]); - expect(gameNamesToRomNames.get('1942w')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942w')).toIncludeSameMembers([ 'sb-0.f1', 'sb-1.k6', 'sb-2.d1', 'sb-3.d2', 'sb-4.d6', 'sb-5.e8', 'sb-6.e9', 'sb-7.e10', 'sb-8.k3', 'sb-9.m11', 'sr-01.c11', 'sr-08.a1', 'sr-09.a2', 'sr-10.a3', 'sr-11.a4', 'sr-12.a5', 'sr-13.a6', 'sr-14.l1', 'sr-15.l2', 'sr-16.n1', 'sr-17.n2', 'sw-02.f2', 'sw-03.m3', 'sw-04.m4', 'sw-05.m5', 'sw-06.m6', 'sw-07.m7', ]); - expect(gameNamesToRomNames.get('aes')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('2spicy')).toIncludeSameMembers([]); + expect(gameNamesToRomNames.get('aes')).toIncludeSameMembers([ '000-lo.lo', 'neo-epo.bin', 'neo-po.bin', 'neodebug.rom', 'uni-bios_1_3.rom', 'uni-bios_2_0.rom', 'uni-bios_2_1.rom', 'uni-bios_2_2.rom', 'uni-bios_2_3.rom', 'uni-bios_2_3o.rom', 'uni-bios_3_0.rom', 'uni-bios_3_1.rom', 'uni-bios_3_2.rom', 'uni-bios_3_3.rom', 'uni-bios_4_0.rom', ]); - expect(gameNamesToRomNames.get('bbtime')).toIncludeAllMembers(['bbtime.svg', 'hd38820a65']); - expect(gameNamesToRomNames.get('c64')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('area51mx')).toIncludeSameMembers([ + '2.0_68020_max-a51_kit_3h.3h', '2.0_68020_max-a51_kit_3k.3k', '2.0_68020_max-a51_kit_3m.3m', + '2.0_68020_max-a51_kit_3p.3p', 'jagwave.rom', + ]); + expect(gameNamesToDiskNames.get('area51mx')).toIncludeSameMembers(['area51mx']); + expect(gameNamesToRomNames.get('a51mxr3k')).toIncludeSameMembers([ + '1.0_r3k_max-a51_kit_hh.hh', '1.0_r3k_max-a51_kit_hl.hl', '1.0_r3k_max-a51_kit_lh.lh', + '1.0_r3k_max-a51_kit_ll.ll', 'jagwave.rom', + ]); + expect(gameNamesToDiskNames.get('a51mxr3k')).toIncludeSameMembers(['area51mx']); + expect(gameNamesToRomNames.get('bbtime')).toIncludeSameMembers(['bbtime.svg', 'hd38820a65']); + expect(gameNamesToRomNames.get('c64')).toIncludeSameMembers([ '901225-01.u5', '901226-01.u3', '901227-01.u4', '901227-02.u4', '901227-03.u4', '906114-01.u17', 'digidos.u4', 'dosrom12.u4', 'exos3.u4', 'exos4.u4', 'jiffydos c64.u4', 'kernal-10-mager.u4', 'kernal-20-1.u4', 'kernal-20-1_au.u4', 'kernal-20-2.u4', 'kernal-20-3.u4', 'kernal-30.u4', @@ -1824,58 +2017,58 @@ describe('MAME v0.258', () => { 'turboaccess301.u4', 'turboaccess302.u4', 'turboprocess.u4', 'turboprocessus.u4', 'turborom2.u4', 'turborom.u4', ]); - expect(gameNamesToRomNames.get('ddonpach')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('ddonpach')).toIncludeSameMembers([ 'b1.u27', 'b2.u26', 'eeprom-ddonpach.bin', 'u6.bin', 'u7.bin', 'u50.bin', 'u51.bin', 'u52.bin', 'u53.bin', 'u60.bin', 'u61.bin', 'u62.bin', ]); - expect(gameNamesToRomNames.get('ddonpacha')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('ddonpacha')).toIncludeSameMembers([ 'arrange_u26.bin', 'arrange_u27.bin', 'arrange_u51.bin', 'arrange_u62.bin', 'eeprom-ddonpach.bin', 'u6.bin', 'u7.bin', 'u50.bin', 'u52.bin', 'u53.bin', 'u60.bin', 'u61.bin', ]); - expect(gameNamesToRomNames.get('ddonpachj')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('ddonpachj')).toIncludeSameMembers([ 'eeprom-ddonpach.bin', 'u6.bin', 'u7.bin', 'u26.bin', 'u27.bin', 'u50.bin', 'u51.bin', 'u52.bin', 'u53.bin', 'u60.bin', 'u61.bin', 'u62.bin', ]); - expect(gameNamesToRomNames.get('galaga')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('galaga')).toIncludeSameMembers([ 'gg1_1b.3p', 'gg1_2b.3m', 'gg1_3.2m', 'gg1_4b.2l', 'gg1_5b.3f', 'gg1_7b.2c', 'gg1_9.4l', 'gg1_10.4f', 'gg1_11.4d', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('galagamf')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('galagamf')).toIncludeSameMembers([ '2600j.bin', '2700k.bin', '2800l.bin', '3200a.bin', '3300b.bin', '3400c.bin', '3500d.bin', '3600fast.bin', '3700g.bin', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('galagamk')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('galagamk')).toIncludeSameMembers([ '3400c.bin', 'gg1-5.3f', 'gg1-7b.2c', 'gg1-9.4l', 'gg1-10.4f', 'gg1-11.4d', 'mk2-1', 'mk2-2', 'mk2-4', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('galagamw')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('galagamw')).toIncludeSameMembers([ '2600j.bin', '2700k.bin', '2800l.bin', '3200a.bin', '3300b.bin', '3400c.bin', '3500d.bin', '3600e.bin', '3700g.bin', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('galagao')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('galagao')).toIncludeSameMembers([ 'gg1-1.3p', 'gg1-2.3m', 'gg1-3.2m', 'gg1-4.2l', 'gg1-5.3f', 'gg1-7.2c', 'gg1-9.4l', 'gg1-10.4f', 'gg1-11.4d', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('gallag')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('gallag')).toIncludeSameMembers([ 'gallag.1', 'gallag.2', 'gallag.3', 'gallag.4', 'gallag.5', 'gallag.6', 'gallag.7', 'gallag.8', 'gallag.9', 'gallag.a', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('gatsbee')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('gatsbee')).toIncludeSameMembers([ '1.4b', '2.4c', '3.4d', '4.4e', '8.5r', '9.6a', '10.7a', 'gallag.6', 'gg1-5.3f', 'gg1-7.2c', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('nebulbee')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('nebulbee')).toIncludeSameMembers([ '1c.bin', '1d.bin', '2n.bin', '5c.bin', 'gg1-5', 'gg1-7', 'gg1_3.2m', 'gg1_9.4l', 'gg1_10.4f', 'gg1_11.4d', 'nebulbee.01', 'nebulbee.02', 'nebulbee.04', 'nebulbee.07', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('liblrabl')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('liblrabl')).toIncludeSameMembers([ '2c.rom', '5b.rom', '5c.rom', '5p.rom', '8c.rom', '9t.rom', '10c.rom', 'lr1-1.1t', 'lr1-2.1s', 'lr1-3.1r', 'lr1-4.3d', 'lr1-5.5l', 'lr1-6.2p', ]); // No change to BIOS or devices - expect(result.getGames().filter((game) => game.isBios())).toHaveLength(2); + expect(result.getGames().filter((game) => game.isBios())).toHaveLength(3); expect(result.getGames().filter((game) => game.isDevice())).toHaveLength(65); expect(gameNamesToRomNames.get('aristmk6')).toHaveLength(96); expect(gameNamesToRomNames.get('neogeo')).toHaveLength(34); @@ -1900,47 +2093,67 @@ describe('MAME v0.258', () => { return map; }, new Map()); + const gameNamesToDiskNames = result.getGames() + .reduce((map, game) => { + map.set(game.getName(), game.getDisks().map((disk) => disk.getName().replace(/[\\/]/g, '\\'))); + return map; + }, new Map()); + // No change - expect(gameNamesToRomNames.get('aes')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('aes')).toIncludeSameMembers([ '000-lo.lo', 'neo-epo.bin', 'neo-po.bin', 'neodebug.rom', 'uni-bios_1_3.rom', 'uni-bios_2_0.rom', 'uni-bios_2_1.rom', 'uni-bios_2_2.rom', 'uni-bios_2_3.rom', 'uni-bios_2_3o.rom', 'uni-bios_3_0.rom', 'uni-bios_3_1.rom', 'uni-bios_3_2.rom', 'uni-bios_3_3.rom', 'uni-bios_4_0.rom', ]); - expect(gameNamesToRomNames.get('bbtime')).toIncludeAllMembers(['bbtime.svg', 'hd38820a65']); - expect(gameNamesToRomNames.get('liblrabl')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('area51mx')).toIncludeSameMembers([ + '2.0_68020_max-a51_kit_3h.3h', '2.0_68020_max-a51_kit_3k.3k', '2.0_68020_max-a51_kit_3m.3m', + '2.0_68020_max-a51_kit_3p.3p', 'jagwave.rom', + ]); + expect(gameNamesToDiskNames.get('area51mx')).toIncludeSameMembers(['area51mx']); + expect(gameNamesToRomNames.get('bbtime')).toIncludeSameMembers(['bbtime.svg', 'hd38820a65']); + expect(gameNamesToRomNames.get('liblrabl')).toIncludeSameMembers([ '2c.rom', '5b.rom', '5c.rom', '5p.rom', '8c.rom', '9t.rom', '10c.rom', 'lr1-1.1t', 'lr1-2.1s', 'lr1-3.1r', 'lr1-4.3d', 'lr1-5.5l', 'lr1-6.2p', ]); + // Clones exclude parent ROMs - expect(gameNamesToRomNames.get('100lions')).toIncludeAllMembers(['10219211.u73', '10219211.u86']); - expect(gameNamesToRomNames.get('100lionsa')).toIncludeAllMembers(['30223811.u73', '30223811.u86']); - expect(gameNamesToRomNames.get('1942')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('100lions')).toIncludeSameMembers(['10219211.u73', '10219211.u86']); + expect(gameNamesToRomNames.get('100lionsa')).toIncludeSameMembers(['30223811.u73', '30223811.u86']); + expect(gameNamesToRomNames.get('1942')).toIncludeSameMembers([ 'sb-0.f1', 'sb-1.k6', 'sb-2.d1', 'sb-3.d2', 'sb-4.d6', 'sb-5.e8', 'sb-6.e9', 'sb-7.e10', 'sb-8.k3', 'sb-9.m11', 'sr-01.c11', 'sr-02.f2', 'sr-08.a1', 'sr-09.a2', 'sr-10.a3', 'sr-11.a4', 'sr-12.a5', 'sr-13.a6', 'sr-14.l1', 'sr-15.l2', 'sr-16.n1', 'sr-17.n2', 'srb-03.m3', 'srb-04.m4', 'srb-05.m5', 'srb-06.m6', 'srb-07.m7', ]); - expect(gameNamesToRomNames.get('1942a')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942a')).toIncludeSameMembers([ 'sr-04.m4', 'sr-05.m5', 'sr-06.m6', 'sr-07.m7', 'sra-03.m3', ]); - expect(gameNamesToRomNames.get('1942abl')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942abl')).toIncludeSameMembers([ '3.bin', '5.bin', '7.bin', '9.bin', '11.bin', '13.bin', '14.bin', '16.bin', ]); - expect(gameNamesToRomNames.get('1942b')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942b')).toIncludeSameMembers([ 'sr-03.m3', 'sr-04.m4', 'sr-05.m5', 'sr-06.m6', 'sr-07.m7', ]); - expect(gameNamesToRomNames.get('1942h')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942h')).toIncludeSameMembers([ 'supercharger_1942_@3.m3', 'supercharger_1942_@4.m4', 'supercharger_1942_@5.m5', ]); - expect(gameNamesToRomNames.get('1942p')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942p')).toIncludeSameMembers([ '1.bin', '2.bin', '3.bin', '04.bin', '5.bin', '6.bin', '7.bin', '9.bin', '10.bin', '11.bin', '12.bin', ]); - expect(gameNamesToRomNames.get('1942w')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942w')).toIncludeSameMembers([ 'sw-02.f2', 'sw-03.m3', 'sw-04.m4', 'sw-05.m5', 'sw-07.m7', ]); - expect(gameNamesToRomNames.get('c64')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('2spicy')).toIncludeSameMembers([ + // It exactly matches its BIOS + ]); + expect(gameNamesToRomNames.get('a51mxr3k')).toIncludeSameMembers([ + '1.0_r3k_max-a51_kit_hh.hh', '1.0_r3k_max-a51_kit_hl.hl', '1.0_r3k_max-a51_kit_lh.lh', + '1.0_r3k_max-a51_kit_ll.ll', + ]); + expect(gameNamesToDiskNames.get('a51mxr3k')).toIncludeSameMembers([]); + expect(gameNamesToRomNames.get('c64')).toIncludeSameMembers([ '901225-01.u5', '901226-01.u3', '901227-01.u4', '901227-02.u4', '901227-03.u4', '906114-01.u17', 'digidos.u4', 'dosrom12.u4', 'exos3.u4', 'exos4.u4', 'jiffydos c64.u4', 'kernal-10-mager.u4', 'kernal-20-1.u4', 'kernal-20-1_au.u4', 'kernal-20-2.u4', 'kernal-20-3.u4', 'kernal-30.u4', @@ -1949,46 +2162,46 @@ describe('MAME v0.258', () => { 'turboaccess301.u4', 'turboaccess302.u4', 'turboprocess.u4', 'turboprocessus.u4', 'turborom2.u4', 'turborom.u4', ]); - expect(gameNamesToRomNames.get('ddonpach')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('ddonpach')).toIncludeSameMembers([ 'b1.u27', 'b2.u26', 'eeprom-ddonpach.bin', 'u6.bin', 'u7.bin', 'u50.bin', 'u51.bin', 'u52.bin', 'u53.bin', 'u60.bin', 'u61.bin', 'u62.bin', ]); - expect(gameNamesToRomNames.get('ddonpacha')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('ddonpacha')).toIncludeSameMembers([ 'arrange_u26.bin', 'arrange_u27.bin', 'arrange_u51.bin', 'arrange_u62.bin', 'eeprom-ddonpach.bin', ]); - expect(gameNamesToRomNames.get('ddonpachj')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('ddonpachj')).toIncludeSameMembers([ 'u26.bin', 'u27.bin', ]); - expect(gameNamesToRomNames.get('galaga')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('galaga')).toIncludeSameMembers([ 'gg1_1b.3p', 'gg1_2b.3m', 'gg1_3.2m', 'gg1_4b.2l', 'gg1_5b.3f', 'gg1_7b.2c', 'gg1_9.4l', 'gg1_10.4f', 'gg1_11.4d', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('galagamf')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('galagamf')).toIncludeSameMembers([ '3200a.bin', '3300b.bin', '3400c.bin', '3500d.bin', '3600fast.bin', '3700g.bin', ]); - expect(gameNamesToRomNames.get('galagamk')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('galagamk')).toIncludeSameMembers([ '3400c.bin', 'gg1-5.3f', 'mk2-1', 'mk2-2', 'mk2-4', ]); - expect(gameNamesToRomNames.get('galagamw')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('galagamw')).toIncludeSameMembers([ '3200a.bin', '3300b.bin', '3400c.bin', '3500d.bin', '3600e.bin', '3700g.bin', ]); - expect(gameNamesToRomNames.get('galagao')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('galagao')).toIncludeSameMembers([ 'gg1-1.3p', 'gg1-2.3m', 'gg1-4.2l', 'gg1-5.3f', 'gg1-7.2c', ]); - expect(gameNamesToRomNames.get('gallag')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('gallag')).toIncludeSameMembers([ 'gallag.1', 'gallag.2', 'gallag.4', 'gallag.5', 'gallag.6', 'gallag.7', 'gallag.8', ]); - expect(gameNamesToRomNames.get('gatsbee')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('gatsbee')).toIncludeSameMembers([ '1.4b', '2.4c', '3.4d', '4.4e', '8.5r', '9.6a', '10.7a', 'gallag.6', 'gg1-5.3f', 'gg1-7.2c', ]); - expect(gameNamesToRomNames.get('nebulbee')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('nebulbee')).toIncludeSameMembers([ '1c.bin', '1d.bin', '2n.bin', '5c.bin', 'gg1-5', 'gg1-7', 'nebulbee.01', 'nebulbee.02', 'nebulbee.04', 'nebulbee.07', ]); // No change to BIOS or devices - expect(result.getGames().filter((game) => game.isBios())).toHaveLength(2); + expect(result.getGames().filter((game) => game.isBios())).toHaveLength(3); expect(result.getGames().filter((game) => game.isDevice())).toHaveLength(65); expect(gameNamesToRomNames.get('aristmk6')).toHaveLength(96); expect(gameNamesToRomNames.get('neogeo')).toHaveLength(34); @@ -2013,15 +2226,21 @@ describe('MAME v0.258', () => { return map; }, new Map()); + const gameNamesToDiskNames = result.getGames() + .reduce((map, game) => { + map.set(game.getName(), game.getDisks().map((disk) => disk.getName().replace(/[\\/]/g, '\\'))); + return map; + }, new Map()); + // No change from regular non-merged (because there are no clones) - expect(gameNamesToRomNames.get('aes')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('aes')).toIncludeSameMembers([ '000-lo.lo', 'neo-epo.bin', 'neo-po.bin', 'neodebug.rom', 'uni-bios_1_3.rom', 'uni-bios_2_0.rom', 'uni-bios_2_1.rom', 'uni-bios_2_2.rom', 'uni-bios_2_3.rom', 'uni-bios_2_3o.rom', 'uni-bios_3_0.rom', 'uni-bios_3_1.rom', 'uni-bios_3_2.rom', 'uni-bios_3_3.rom', 'uni-bios_4_0.rom', ]); - expect(gameNamesToRomNames.get('bbtime')).toIncludeAllMembers(['bbtime.svg', 'hd38820a65']); - expect(gameNamesToRomNames.get('c64')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('bbtime')).toIncludeSameMembers(['bbtime.svg', 'hd38820a65']); + expect(gameNamesToRomNames.get('c64')).toIncludeSameMembers([ // NOTE(cemmer): excludes clones '901225-01.u5', '901226-01.u3', '901227-01.u4', '901227-02.u4', '901227-03.u4', '906114-01.u17', 'digidos.u4', 'dosrom12.u4', 'exos3.u4', 'exos4.u4', 'jiffydos c64.u4', 'kernal-10-mager.u4', @@ -2031,17 +2250,18 @@ describe('MAME v0.258', () => { 'turboaccess301.u4', 'turboaccess302.u4', 'turboprocess.u4', 'turboprocessus.u4', 'turborom2.u4', 'turborom.u4', ]); - expect(gameNamesToRomNames.get('liblrabl')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('liblrabl')).toIncludeSameMembers([ '2c.rom', '5b.rom', '5c.rom', '5p.rom', '8c.rom', '9t.rom', '10c.rom', 'lr1-1.1t', 'lr1-2.1s', 'lr1-3.1r', 'lr1-4.3d', 'lr1-5.5l', 'lr1-6.2p', ]); + // Clones are merged in - expect(gameNamesToRomNames.get('100lions')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('100lions')).toIncludeSameMembers([ '100lionsa\\30223811.u73', '100lionsa\\30223811.u86', '10219211.u73', '10219211.u86', ]); expect(gameNamesToRomNames.has('100lionsa')).toEqual(false); - expect(gameNamesToRomNames.get('1942')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('1942')).toIncludeSameMembers([ '1942a\\sr-04.m4', '1942a\\sr-05.m5', '1942a\\sr-06.m6', '1942a\\sr-07.m7', '1942a\\sra-03.m3', '1942abl\\3.bin', '1942abl\\7.bin', '1942abl\\9.bin', '1942abl\\11.bin', '1942abl\\13.bin', '1942abl\\14.bin', '1942abl\\16.bin', '1942b\\sr-03.m3', @@ -2059,7 +2279,19 @@ describe('MAME v0.258', () => { expect(gameNamesToRomNames.has('1942h')).toEqual(false); expect(gameNamesToRomNames.has('1942p')).toEqual(false); expect(gameNamesToRomNames.has('1942w')).toEqual(false); - expect(gameNamesToRomNames.get('galaga')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('2spicy')).toIncludeSameMembers([ + // It exactly matches its BIOS + ]); + expect(gameNamesToRomNames.get('area51mx')).toIncludeSameMembers([ + 'a51mxr3k\\1.0_r3k_max-a51_kit_hh.hh', 'a51mxr3k\\1.0_r3k_max-a51_kit_hl.hl', + 'a51mxr3k\\1.0_r3k_max-a51_kit_lh.lh', 'a51mxr3k\\1.0_r3k_max-a51_kit_ll.ll', + '2.0_68020_max-a51_kit_3h.3h', '2.0_68020_max-a51_kit_3k.3k', '2.0_68020_max-a51_kit_3m.3m', + '2.0_68020_max-a51_kit_3p.3p', 'jagwave.rom', + ]); + expect(gameNamesToDiskNames.get('area51mx')).toIncludeSameMembers(['area51mx']); + expect(gameNamesToRomNames.has('a51mxr3k')).toEqual(false); + expect(gameNamesToDiskNames.has('a51mxr3k')).toEqual(false); + expect(gameNamesToRomNames.get('galaga')).toIncludeSameMembers([ 'galagamf\\3200a.bin', 'galagamf\\3300b.bin', 'galagamf\\3400c.bin', 'galagamf\\3500d.bin', 'galagamf\\3600fast.bin', 'galagamf\\3700g.bin', 'galagamk\\gg1-5.3f', 'galagamk\\mk2-1', 'galagamk\\mk2-2', 'galagamk\\mk2-4', 'galagamw\\3600e.bin', 'galagao\\gg1-1.3p', 'galagao\\gg1-2.3m', 'galagao\\gg1-4.2l', 'galagao\\gg1-7.2c', @@ -2069,7 +2301,7 @@ describe('MAME v0.258', () => { 'gg1_1b.3p', 'gg1_2b.3m', 'gg1_3.2m', 'gg1_4b.2l', 'gg1_5b.3f', 'gg1_7b.2c', 'gg1_9.4l', 'gg1_10.4f', 'gg1_11.4d', 'prom-1.1d', 'prom-2.5c', 'prom-3.1c', 'prom-4.2n', 'prom-5.5n', ]); - expect(gameNamesToRomNames.get('ddonpach')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('ddonpach')).toIncludeSameMembers([ 'ddonpacha\\arrange_u26.bin', 'ddonpacha\\arrange_u27.bin', 'ddonpacha\\arrange_u51.bin', 'ddonpacha\\arrange_u62.bin', 'ddonpacha\\eeprom-ddonpach.bin', 'ddonpachj\\u26.bin', 'ddonpachj\\u27.bin', 'b1.u27', 'b2.u26', 'eeprom-ddonpach.bin', 'u6.bin', 'u7.bin', 'u50.bin', @@ -2086,7 +2318,7 @@ describe('MAME v0.258', () => { expect(gameNamesToRomNames.has('nebulbee')).toEqual(false); // No change to BIOS or devices - expect(result.getGames().filter((game) => game.isBios())).toHaveLength(2); + expect(result.getGames().filter((game) => game.isBios())).toHaveLength(3); expect(result.getGames().filter((game) => game.isDevice())).toHaveLength(65); expect(gameNamesToRomNames.get('aristmk6')).toHaveLength(96); expect(gameNamesToRomNames.get('neogeo')).toHaveLength(34); @@ -2272,7 +2504,7 @@ describe('FinalBurn Neo Neo Geo e544671', () => { }, new Map()); // No change - expect(gameNamesToRomNames.get('neogeo')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('neogeo')).toIncludeSameMembers([ 'sp-s3.sp1', 'sp-s2.sp1', 'sp-s.sp1', 'sp-u2.sp1', 'sp1-u2', 'sp-e.sp1', 'sp1-u4.bin', 'sp1-u3.bin', 'vs-bios.rom', 'sp-j2.sp1', 'sp1.jipan.1024', 'sp-45.sp1', 'sp-j3.sp1', 'japan-j3.bin', 'sp1-j3.bin', 'neo-po.bin', 'neo-epo.bin', 'neodebug.bin', 'sp-1v1_3db8c.bin', @@ -2283,11 +2515,11 @@ describe('FinalBurn Neo Neo Geo e544671', () => { 'sfix.sfix', '000-lo.lo', ]); // Clones exclude parent ROMs - expect(gameNamesToRomNames.get('3countb')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('3countb')).toIncludeSameMembers([ '043-p1.p1', '043-s1.s1', '043-c1.c1', '043-c2.c2', '043-c3.c3', '043-c4.c4', '043-m1.m1', '043-v1.v1', '043-v2.v2', ]); - expect(gameNamesToRomNames.get('3countba')).toIncludeAllMembers([ + expect(gameNamesToRomNames.get('3countba')).toIncludeSameMembers([ '043-epr.ep1', '043-epr.ep2', ]); }); diff --git a/test/modules/romScanner.test.ts b/test/modules/romScanner.test.ts index 98f4489f2..a207df77b 100644 --- a/test/modules/romScanner.test.ts +++ b/test/modules/romScanner.test.ts @@ -33,7 +33,7 @@ it('should not throw on bad archives', async () => { describe('multiple files', () => { it('should scan multiple files with no exclusions', async () => { - const expectedRomFiles = 87; + const expectedRomFiles = 91; await expect(createRomScanner(['test/fixtures/roms']).scan()).resolves.toHaveLength(expectedRomFiles); await expect(createRomScanner(['test/fixtures/roms/*', 'test/fixtures/roms/**/*']).scan()).resolves.toHaveLength(expectedRomFiles); await expect(createRomScanner(['test/fixtures/roms/**/*']).scan()).resolves.toHaveLength(expectedRomFiles); @@ -41,7 +41,7 @@ describe('multiple files', () => { }); test.each([ - [{ input: [path.join('test', 'fixtures', 'roms')] }, 126], + [{ input: [path.join('test', 'fixtures', 'roms')] }, 132], [{ input: [path.join('test', 'fixtures', 'roms', '7z')] }, 12], [{ input: [path.join('test', 'fixtures', 'roms', 'gz')] }, 14], [{ input: [path.join('test', 'fixtures', 'roms', 'rar')] }, 12], @@ -54,9 +54,9 @@ describe('multiple files', () => { }); it('should scan multiple files with some file exclusions', async () => { - await expect(createRomScanner(['test/fixtures/roms/**/*'], ['test/fixtures/roms/**/*.rom']).scan()).resolves.toHaveLength(70); - await expect(createRomScanner(['test/fixtures/roms/**/*'], ['test/fixtures/roms/**/*.rom', 'test/fixtures/roms/**/*.rom']).scan()).resolves.toHaveLength(70); - await expect(createRomScanner(['test/fixtures/roms/**/*'], ['test/fixtures/roms/**/*.rom', 'test/fixtures/roms/**/*.zip']).scan()).resolves.toHaveLength(59); + await expect(createRomScanner(['test/fixtures/roms/**/*'], ['test/fixtures/roms/**/*.rom']).scan()).resolves.toHaveLength(74); + await expect(createRomScanner(['test/fixtures/roms/**/*'], ['test/fixtures/roms/**/*.rom', 'test/fixtures/roms/**/*.rom']).scan()).resolves.toHaveLength(74); + await expect(createRomScanner(['test/fixtures/roms/**/*'], ['test/fixtures/roms/**/*.rom', 'test/fixtures/roms/**/*.zip']).scan()).resolves.toHaveLength(63); }); it('should scan multiple files with every file excluded', async () => { diff --git a/test/types/dats/disk.test.ts b/test/types/dats/disk.test.ts deleted file mode 100644 index cbf3959e3..000000000 --- a/test/types/dats/disk.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Disk from '../../../src/types/dats/disk.js'; - -it('should instantiate', () => { - expect(new Disk()).toBeTruthy(); -}); diff --git a/test/types/files/file.test.ts b/test/types/files/file.test.ts index 512f66278..d03e0de1c 100644 --- a/test/types/files/file.test.ts +++ b/test/types/files/file.test.ts @@ -49,7 +49,7 @@ describe('getSize', () => { [10_000], [1_000_000], ])('%s', (size) => { - it('should get the file\'s size: %s', async () => { + it('should get the file\'s size', async () => { const tempDir = await fsPoly.mkdtemp(Temp.getTempDir()); try { const tempFile = path.resolve(await fsPoly.mktemp(path.join(tempDir, 'file')));