@@ -9,6 +9,7 @@ import type FileFactory from '../../factories/fileFactory.js';
99import type DAT from '../../models/dats/dat.js' ;
1010import Disk from '../../models/dats/disk.js' ;
1111import type Game from '../../models/dats/game.js' ;
12+ import MergedDiscGame from '../../models/dats/mergedDiscGame.js' ;
1213import type ROM from '../../models/dats/rom.js' ;
1314import type Archive from '../../models/files/archives/archive.js' ;
1415import ArchiveEntry from '../../models/files/archives/archiveEntry.js' ;
@@ -289,6 +290,17 @@ export default class CandidateGenerator extends Module {
289290 return new Map ( ) ;
290291 }
291292
293+ if ( game instanceof MergedDiscGame ) {
294+ // Resolve each disc independently so every disc is the single-archive case the code already
295+ // handles correctly (including pulling an unknown .cue from the same CHD that holds its .bins).
296+ return this . findOptimalInputFilesForMergedDiscGame (
297+ dat ,
298+ game ,
299+ romsAndInputFiles ,
300+ indexedFiles ,
301+ ) ;
302+ }
303+
292304 const archiveWithEveryRom = this . findArchiveFileWithEveryRomForGame (
293305 dat ,
294306 game ,
@@ -310,6 +322,71 @@ export default class CandidateGenerator extends Module {
310322 ) ;
311323 }
312324
325+ /**
326+ * Resolve input files for a {@link MergedDiscGame} one sub-game at a time. Any ROM not resolved
327+ * to a single containing archive falls back to its first matched input file.
328+ */
329+ private findOptimalInputFilesForMergedDiscGame (
330+ dat : DAT ,
331+ game : MergedDiscGame ,
332+ romsAndInputFiles : [ ROM , File [ ] ] [ ] ,
333+ indexedFiles : IndexedFiles ,
334+ ) : Map < ROM , File > {
335+ // All intermediate matching is keyed by a stable ROM name + hash code string rather than by ROM
336+ // instance, so it survives any earlier reinstantiation
337+ const entryKey = ( rom : ROM ) : string => `${ rom . getName ( ) } |${ rom . hashCode ( ) } ` ;
338+ const entriesByKey = new Map < string , [ ROM , File [ ] ] > (
339+ romsAndInputFiles . map ( ( romAndInputFiles ) => [
340+ entryKey ( romAndInputFiles [ 0 ] ) ,
341+ romAndInputFiles ,
342+ ] ) ,
343+ ) ;
344+
345+ // Resolve each sub-game's single containing archive, keyed by entryKey
346+ const filesByKey = new Map < string , File > ( ) ;
347+ for ( const subGame of game . getSubGames ( ) ) {
348+ const subGameRoms = subGame . getRoms ( ) ;
349+ const subGameRomsAndInputFiles = subGameRoms
350+ . map ( ( subGameRom ) => entriesByKey . get ( entryKey ( subGameRom ) ) )
351+ . filter ( ( entry ) => entry !== undefined ) ;
352+ if ( subGameRomsAndInputFiles . length === 0 ) {
353+ continue ;
354+ }
355+
356+ const archiveWithEveryRom = this . findArchiveFileWithEveryRomForGame (
357+ dat ,
358+ subGame ,
359+ subGameRoms ,
360+ subGameRomsAndInputFiles ,
361+ indexedFiles ,
362+ ) ;
363+ if ( archiveWithEveryRom === undefined ) {
364+ continue ;
365+ }
366+ for ( const [ rom , inputFile ] of archiveWithEveryRom ) {
367+ filesByKey . set ( entryKey ( rom ) , inputFile ) ;
368+ }
369+ }
370+
371+ const resolved = new Map < ROM , File > ( ) ;
372+ for ( const [ rom , inputFiles ] of romsAndInputFiles ) {
373+ const inputFile = filesByKey . get ( entryKey ( rom ) ) ?? inputFiles . at ( 0 ) ;
374+ if ( inputFile !== undefined ) {
375+ resolved . set ( rom , inputFile ) ;
376+ }
377+ }
378+ return resolved ;
379+ }
380+
381+ /**
382+ * Find a single input {@link Archive} that contains every one of a {@link Game}'s {@link ROM}s, and
383+ * return a map from each ROM to its matching entry within that archive. Preferring one archive for
384+ * the whole game avoids output-path conflicts when raw-copying and avoids leaving archives partially
385+ * used when zipping. Returns `undefined` when extracting (the source archive doesn't matter) or when
386+ * no single archive holds every ROM, leaving the caller to fall back to per-ROM matching. ROMs with
387+ * no matching entry are omitted from the returned map, so it is always a `Map<ROM, File>` of only
388+ * resolved ROMs.
389+ */
313390 private findArchiveFileWithEveryRomForGame (
314391 dat : DAT ,
315392 game : Game ,
@@ -455,7 +532,7 @@ export default class CandidateGenerator extends Module {
455532 // An Archive was found, use that as the only possible input file
456533 // For each of this Game's ROMs, find the matching ArchiveEntry from this Archive
457534 return new Map (
458- romsAndInputFiles . map ( ( [ rom , inputFiles ] ) => {
535+ romsAndInputFiles . flatMap ( ( [ rom , inputFiles ] ) => {
459536 const archiveEntries = inputFiles . filter (
460537 ( inputFile ) =>
461538 inputFile . getFilePath ( ) === archiveWithEveryRom . getFilePath ( ) &&
@@ -484,7 +561,7 @@ export default class CandidateGenerator extends Module {
484561 ?. find ( ( file ) => file . getExtractedFilePath ( ) . toLowerCase ( ) . endsWith ( '.cue' ) ) ;
485562 }
486563
487- return [ rom , archiveEntry as ArchiveEntry < Archive > ] ;
564+ return archiveEntry === undefined ? [ ] : [ [ rom , archiveEntry ] ] ;
488565 } ) ,
489566 ) ;
490567 }
0 commit comments