diff --git a/src/types/outputFactory.ts b/src/types/outputFactory.ts index 903cf71b4..70629af73 100644 --- a/src/types/outputFactory.ts +++ b/src/types/outputFactory.ts @@ -101,8 +101,6 @@ export default class OutputFactory { romBasename?: string, romBasenames?: string[], ): string { - const romNameSanitized = romBasename?.replace(/[\\/]/g, '_'); - let output = options.getOutput(); // Replace all {token}s in the output path @@ -112,7 +110,7 @@ export default class OutputFactory { inputFile?.getFilePath(), game, release, - romNameSanitized, + romBasename, )); if (options.getDirMirror() && inputFile?.getFilePath()) { @@ -131,18 +129,11 @@ export default class OutputFactory { output = path.join(output, dat.getDescription() as string); } - const dirLetter = this.getDirLetterParsed(options, romNameSanitized, romBasenames); + const dirLetter = this.getDirLetterParsed(options, romBasename, romBasenames); if (dirLetter) { output = path.join(output, dirLetter); } - if (game - && game.getRoms().length > 1 - && (!romNameSanitized || !FileFactory.isArchive(romNameSanitized)) - ) { - output = path.join(output, game.getName()); - } - return fsPoly.makeLegal(output); } @@ -277,24 +268,38 @@ export default class OutputFactory { // Split the letter directories, if needed if (options.getDirLetterLimit()) { - lettersToFilenames = [...lettersToFilenames.entries()].reduce((map, [letter, filenames]) => { - if (filenames.length <= options.getDirLetterLimit()) { - map.set(letter, filenames); - return map; - } - - const uniqueFilenames = filenames - .sort() - .filter((val, idx, vals) => vals.indexOf(val) === idx); - const chunkSize = options.getDirLetterLimit(); - for (let i = 0; i < uniqueFilenames.length; i += chunkSize) { - const newLetter = `${letter}${i / chunkSize + 1}`; - const chunk = uniqueFilenames.slice(i, i + chunkSize); - map.set(newLetter, chunk); - } - - return map; - }, new Map()); + lettersToFilenames = [...lettersToFilenames.entries()] + .reduce((lettersMap, [letter, filenames]) => { + if (filenames.length <= options.getDirLetterLimit()) { + lettersMap.set(letter, filenames); + return lettersMap; + } + + // ROMs may have been grouped together into a subdirectory. For example, when a game has + // multiple ROMs, they get grouped by their game name. Therefore, we have to understand + // what the "sub-path" should be within the letter directory: the dirname if the ROM has a + // subdir, or just the ROM's basename otherwise. + const subPathsToFilenames = filenames + .filter((val, idx, vals) => vals.indexOf(val) === idx) + .reduce((subPathMap, filename) => { + const subPath = filename.replace(/[\\/].+$/, ''); + subPathMap.set(subPath, [...subPathMap.get(subPath) ?? [], filename]); + return subPathMap; + }, new Map()); + + const subPaths = [...subPathsToFilenames.keys()].sort(); + const chunkSize = options.getDirLetterLimit(); + for (let i = 0; i < subPaths.length; i += chunkSize) { + const chunk = subPaths + .slice(i, i + chunkSize) + .flatMap((subPath) => subPathsToFilenames.get(subPath) ?? []); + + const newLetter = `${letter}${i / chunkSize + 1}`; + lettersMap.set(newLetter, chunk); + } + + return lettersMap; + }, new Map()); } const foundEntry = [...lettersToFilenames.entries()] @@ -315,17 +320,27 @@ export default class OutputFactory { rom: ROM, inputFile: File, ): string { - const { dir, name } = path.parse(this.getOutputFileBasename( + const { dir, name, ext } = path.parse(this.getOutputFileBasename( options, dat, game, rom, inputFile, )); - if (dir.trim() === '') { - return name; + + let output = name; + if (dir.trim() !== '') { + output = path.join(dir, output); + } + + if (game + && game.getRoms().length > 1 + && !FileFactory.isArchive(ext) + ) { + output = path.join(game.getName(), output); } - return path.join(dir, name); + + return output; } private static getExt(options: Options, dat: DAT, game: Game, rom: ROM, inputFile: File): string { @@ -363,7 +378,12 @@ export default class OutputFactory { } private static getRomBasename(options: Options, dat: DAT, rom: ROM, inputFile: File): string { - const { base, ...parsedRomPath } = path.parse(rom.getName()); + let romNameSanitized = rom.getName(); + if (!dat.getRomNamesContainDirectories()) { + romNameSanitized = romNameSanitized?.replace(/[\\/]/g, '_'); + } + + const { base, ...parsedRomPath } = path.parse(romNameSanitized); // Alter the output extension of the file const fileHeader = inputFile.getFileHeader(); diff --git a/test/modules/candidatePostProcessor.test.ts b/test/modules/candidatePostProcessor.test.ts index 585539794..02d7a2363 100644 --- a/test/modules/candidatePostProcessor.test.ts +++ b/test/modules/candidatePostProcessor.test.ts @@ -11,7 +11,7 @@ import ReleaseCandidate from '../../src/types/releaseCandidate.js'; import ROMWithFiles from '../../src/types/romWithFiles.js'; import ProgressBarFake from '../console/progressBarFake.js'; -const games = [ +const singleRomGames = [ 'Admirable', 'Adorable', 'Adventurous', @@ -19,13 +19,31 @@ const games = [ 'Awesome', 'Best', 'Brilliant', +].map((name) => new Game({ + name, + rom: new ROM(`${name}.rom`, 0, '00000000'), +})); +const subDirRomGames = [ 'Cheerful', 'Confident', 'Cool', ].map((name) => new Game({ name, - rom: new ROM(`${name}.rom`, 0, '00000000'), + rom: new ROM(`disk1\\${name}.rom`, 0, '00000000'), +})); +const multiRomGames = [ + 'Dainty', + 'Daring', + 'Dazzling', + 'Dedicated', +].map((name) => new Game({ + name, + rom: [ + new ROM(`${name}.cue`, 0, '00000000'), + new ROM(`${name} (Track 01).bin`, 0, '00000000'), + ], })); +const games = [...singleRomGames, ...subDirRomGames, ...multiRomGames]; const dat = new DAT(new Header(), games); async function runCandidatePostProcessor( @@ -69,9 +87,17 @@ it('should do nothing with no options', async () => { path.join('Output', 'Awesome.rom'), path.join('Output', 'Best.rom'), path.join('Output', 'Brilliant.rom'), - path.join('Output', 'Cheerful.rom'), - path.join('Output', 'Confident.rom'), - path.join('Output', 'Cool.rom'), + path.join('Output', 'Dainty', 'Dainty (Track 01).bin'), + path.join('Output', 'Dainty', 'Dainty.cue'), + path.join('Output', 'Daring', 'Daring (Track 01).bin'), + path.join('Output', 'Daring', 'Daring.cue'), + path.join('Output', 'Dazzling', 'Dazzling (Track 01).bin'), + path.join('Output', 'Dazzling', 'Dazzling.cue'), + path.join('Output', 'Dedicated', 'Dedicated (Track 01).bin'), + path.join('Output', 'Dedicated', 'Dedicated.cue'), + path.join('Output', 'disk1_Cheerful.rom'), + path.join('Output', 'disk1_Confident.rom'), + path.join('Output', 'disk1_Cool.rom'), ]); }); @@ -85,9 +111,17 @@ describe('dirLetterLimit', () => { path.join('Output', 'A', 'Awesome.rom'), path.join('Output', 'B', 'Best.rom'), path.join('Output', 'B', 'Brilliant.rom'), - path.join('Output', 'C', 'Cheerful.rom'), - path.join('Output', 'C', 'Confident.rom'), - path.join('Output', 'C', 'Cool.rom'), + path.join('Output', 'D', 'Dainty', 'Dainty (Track 01).bin'), + path.join('Output', 'D', 'Dainty', 'Dainty.cue'), + path.join('Output', 'D', 'Daring', 'Daring (Track 01).bin'), + path.join('Output', 'D', 'Daring', 'Daring.cue'), + path.join('Output', 'D', 'Dazzling', 'Dazzling (Track 01).bin'), + path.join('Output', 'D', 'Dazzling', 'Dazzling.cue'), + path.join('Output', 'D', 'Dedicated', 'Dedicated (Track 01).bin'), + path.join('Output', 'D', 'Dedicated', 'Dedicated.cue'), + path.join('Output', 'D', 'disk1_Cheerful.rom'), + path.join('Output', 'D', 'disk1_Confident.rom'), + path.join('Output', 'D', 'disk1_Cool.rom'), ]], [2, [ path.join('Output', 'A1', 'Admirable.rom'), @@ -97,9 +131,17 @@ describe('dirLetterLimit', () => { path.join('Output', 'A3', 'Awesome.rom'), path.join('Output', 'B', 'Best.rom'), path.join('Output', 'B', 'Brilliant.rom'), - path.join('Output', 'C1', 'Cheerful.rom'), - path.join('Output', 'C1', 'Confident.rom'), - path.join('Output', 'C2', 'Cool.rom'), + path.join('Output', 'D1', 'Dainty', 'Dainty (Track 01).bin'), + path.join('Output', 'D1', 'Dainty', 'Dainty.cue'), + path.join('Output', 'D1', 'Daring', 'Daring (Track 01).bin'), + path.join('Output', 'D1', 'Daring', 'Daring.cue'), + path.join('Output', 'D2', 'Dazzling', 'Dazzling (Track 01).bin'), + path.join('Output', 'D2', 'Dazzling', 'Dazzling.cue'), + path.join('Output', 'D2', 'Dedicated', 'Dedicated (Track 01).bin'), + path.join('Output', 'D2', 'Dedicated', 'Dedicated.cue'), + path.join('Output', 'D3', 'disk1_Cheerful.rom'), + path.join('Output', 'D3', 'disk1_Confident.rom'), + path.join('Output', 'D4', 'disk1_Cool.rom'), ]], [3, [ path.join('Output', 'A1', 'Admirable.rom'), @@ -109,9 +151,17 @@ describe('dirLetterLimit', () => { path.join('Output', 'A2', 'Awesome.rom'), path.join('Output', 'B', 'Best.rom'), path.join('Output', 'B', 'Brilliant.rom'), - path.join('Output', 'C', 'Cheerful.rom'), - path.join('Output', 'C', 'Confident.rom'), - path.join('Output', 'C', 'Cool.rom'), + path.join('Output', 'D1', 'Dainty', 'Dainty (Track 01).bin'), + path.join('Output', 'D1', 'Dainty', 'Dainty.cue'), + path.join('Output', 'D1', 'Daring', 'Daring (Track 01).bin'), + path.join('Output', 'D1', 'Daring', 'Daring.cue'), + path.join('Output', 'D1', 'Dazzling', 'Dazzling (Track 01).bin'), + path.join('Output', 'D1', 'Dazzling', 'Dazzling.cue'), + path.join('Output', 'D2', 'Dedicated', 'Dedicated (Track 01).bin'), + path.join('Output', 'D2', 'Dedicated', 'Dedicated.cue'), + path.join('Output', 'D2', 'disk1_Cheerful.rom'), + path.join('Output', 'D2', 'disk1_Confident.rom'), + path.join('Output', 'D3', 'disk1_Cool.rom'), ]], ])('it should split the letter dirs: %s', async (limit, expectedFilePaths) => { const options = new Options({