Skip to content

Commit

Permalink
Refactor: dir2dat use Game and ROM information from candidates (#1213)
Browse files Browse the repository at this point in the history
  • Loading branch information
emmercm authored Jul 12, 2024
1 parent 895ce43 commit 865955a
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 7 deletions.
2 changes: 1 addition & 1 deletion src/igir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export default class Igir {

// Write a dir2dat
const dir2DatPath = await new Dir2DatCreator(this.options, progressBar)
.create(filteredDat);
.create(filteredDat, parentsToCandidates);
if (dir2DatPath) {
datsToWrittenFiles.set(filteredDat, [
...(datsToWrittenFiles.get(filteredDat) ?? []),
Expand Down
33 changes: 29 additions & 4 deletions src/modules/dir2DatCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import path from 'node:path';
import ProgressBar, { ProgressBarSymbol } from '../console/progressBar.js';
import fsPoly from '../polyfill/fsPoly.js';
import DAT from '../types/dats/dat.js';
import Game from '../types/dats/game.js';
import LogiqxDAT from '../types/dats/logiqx/logiqxDat.js';
import Parent from '../types/dats/parent.js';
import Options from '../types/options.js';
import OutputFactory from '../types/outputFactory.js';
import ReleaseCandidate from '../types/releaseCandidate.js';
import Module from './module.js';

/**
Expand All @@ -21,7 +25,10 @@ export default class Dir2DatCreator extends Module {
/**
* Write the DAT.
*/
async create(dat: DAT): Promise<string | undefined> {
async create(
dat: DAT,
parentsToCandidates: Map<Parent, ReleaseCandidate[]>,
): Promise<string | undefined> {
if (!this.options.shouldDir2Dat()) {
return undefined;
}
Expand All @@ -38,11 +45,29 @@ export default class Dir2DatCreator extends Module {
}
const datPath = path.join(datDir, dat.getFilename());

this.progressBar.logInfo(`${dat.getNameShort()}: creating dir2dat '${datPath}'`);
const datContents = dat.toXmlDat();
// It is possible that the {@link ROM} embedded within {@link ReleaseCandidate}s has been
// manipulated, such as from {@link CandidateExtensionCorrector}. Use the {@link Game}s and
// {@link ROM}s from the {@link ReleaseCandidate}s instead of the original {@link DAT}.
const gamesToCandidates = [...parentsToCandidates.values()]
.flat()
.reduce((map, releaseCandidate) => {
const key = releaseCandidate.getGame();
map.set(key, [...(map.get(key) ?? []), releaseCandidate]);
return map;
}, new Map<Game, ReleaseCandidate[]>());
const gamesFromCandidates = [...gamesToCandidates.entries()]
.map(([game, releaseCandidates]) => {
const roms = releaseCandidates.at(0)?.getRomsWithFiles()
.map((romWithFiles) => romWithFiles.getRom());
return game.withProps({ rom: roms });
});
const datFromCandidates = new LogiqxDAT(dat.getHeader(), gamesFromCandidates);

this.progressBar.logInfo(`${datFromCandidates.getNameShort()}: creating dir2dat '${datPath}'`);
const datContents = datFromCandidates.toXmlDat();
await fsPoly.writeFile(datPath, datContents);

this.progressBar.logTrace(`${dat.getNameShort()}: done writing dir2dat`);
this.progressBar.logTrace(`${datFromCandidates.getNameShort()}: done writing dir2dat`);
return datPath;
}
}
10 changes: 10 additions & 0 deletions src/types/dats/rom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ export default class ROM implements ROMProps {
return this.bios;
}

/**
* Return a new copy of this {@link ROM} with a different name.
*/
withName(name: string): ROM {
if (name === this.name) {
return this;
}
return new ROM({ ...this, name });
}

/**
* Turn this {@link ROM} into a non-existent {@link File}.
*/
Expand Down
90 changes: 88 additions & 2 deletions test/modules/dir2DatCreator.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import 'jest-extended';

import CandidateGenerator from '../../src/modules/candidateGenerator.js';
import DATGameInferrer from '../../src/modules/datGameInferrer.js';
import DATScanner from '../../src/modules/datScanner.js';
import Dir2DatCreator from '../../src/modules/dir2DatCreator.js';
import ROMIndexer from '../../src/modules/romIndexer.js';
import ROMScanner from '../../src/modules/romScanner.js';
import FsPoly from '../../src/polyfill/fsPoly.js';
import DAT from '../../src/types/dats/dat.js';
import Options from '../../src/types/options.js';
import ReleaseCandidate from '../../src/types/releaseCandidate.js';
import ROMWithFiles from '../../src/types/romWithFiles.js';
import ProgressBarFake from '../console/progressBarFake.js';

it('should do nothing if dir2dat command not provided', async () => {
Expand All @@ -22,8 +26,15 @@ it('should do nothing if dir2dat command not provided', async () => {
expect(inferredDats).toHaveLength(1);
const [inferredDat] = inferredDats;

// And candidates
const candidates = await new CandidateGenerator(options, new ProgressBarFake()).generate(
inferredDat,
await new ROMIndexer(options, new ProgressBarFake()).index(files),
);

// When writing the DAT to disk
const dir2dat = await new Dir2DatCreator(options, new ProgressBarFake()).create(inferredDat);
const dir2dat = await new Dir2DatCreator(options, new ProgressBarFake())
.create(inferredDat, candidates);

// Then the DAT wasn't written
expect(dir2dat).toBeUndefined();
Expand All @@ -42,8 +53,15 @@ it('should write a valid DAT', async () => {
expect(inferredDats).toHaveLength(1);
const [inferredDat] = inferredDats;

// And candidates
const candidates = await new CandidateGenerator(options, new ProgressBarFake()).generate(
inferredDat,
await new ROMIndexer(options, new ProgressBarFake()).index(files),
);

// When writing the DAT to disk
const dir2dat = await new Dir2DatCreator(options, new ProgressBarFake()).create(inferredDat);
const dir2dat = await new Dir2DatCreator(options, new ProgressBarFake())
.create(inferredDat, candidates);

// Then the written DAT exists
if (dir2dat === undefined) {
Expand All @@ -67,8 +85,76 @@ it('should write a valid DAT', async () => {
// And the written DAT matches the inferred DAT
expect(writtenDat.getHeader().toString())
.toEqual(inferredDat.getHeader().toString());
expect(writtenDat.getParents()).toHaveLength(inferredDat.getParents().length);
expect(writtenDat.getParents().map((parent) => parent.getName()))
.toIncludeAllMembers(inferredDat.getParents().map((parent) => parent.getName()));
expect(writtenDat.getGames()).toHaveLength(inferredDat.getGames().length);
expect(writtenDat.getGames().map((game) => game.hashCode()))
.toIncludeAllMembers(inferredDat.getGames().map((game) => game.hashCode()));
});

it('should use the candidates for games and ROMs', async () => {
// Given some input ROMs
const options = new Options({
commands: ['dir2dat'],
input: ['test/fixtures/roms'],
});
const files = await new ROMScanner(options, new ProgressBarFake()).scan();

// And a DAT
const inferredDats = new DATGameInferrer(options, new ProgressBarFake()).infer(files);
expect(inferredDats).toHaveLength(1);
const [inferredDat] = inferredDats;

// And candidates
const candidates = await new CandidateGenerator(options, new ProgressBarFake()).generate(
inferredDat,
await new ROMIndexer(options, new ProgressBarFake()).index(files),
);

// When manipulating the candidates
const updatedCandidates = new Map([...candidates.entries()].map(([parent, releaseCandidates]) => [
parent,
releaseCandidates.map((candidate) => new ReleaseCandidate(
candidate.getGame().withProps({ name: `${candidate.getGame().getName()} (updated)` }),
candidate.getRelease(),
candidate.getRomsWithFiles().map((romWithFiles) => new ROMWithFiles(
romWithFiles.getRom().withName(`${romWithFiles.getRom().getName()} (updated)`),
romWithFiles.getInputFile(),
romWithFiles.getOutputFile(),
)),
))]));

// When writing the DAT to disk
const dir2dat = await new Dir2DatCreator(options, new ProgressBarFake())
.create(inferredDat, updatedCandidates);

// Then the written DAT exists
if (dir2dat === undefined) {
throw new Error('failed to create dir2dat');
}

// And the written DAT can be parsed
let writtenDat: DAT;
try {
await expect(FsPoly.exists(dir2dat)).resolves.toEqual(true);
const writtenDats = await new DATScanner(new Options({
...options,
dat: [dir2dat],
}), new ProgressBarFake()).scan();
expect(writtenDats).toHaveLength(1);
[writtenDat] = writtenDats;
} finally {
await FsPoly.rm(dir2dat, { force: true });
}

// And the written DAT matches the inferred DAT
expect(writtenDat.getHeader().toString())
.toEqual(inferredDat.getHeader().toString());
expect(writtenDat.getParents()).toHaveLength(inferredDat.getParents().length);
expect(writtenDat.getParents().map((parent) => parent.getName()))
.not.toIncludeAnyMembers(inferredDat.getParents().map((parent) => parent.getName()));
expect(writtenDat.getGames()).toHaveLength(inferredDat.getGames().length);
expect(writtenDat.getGames().map((game) => game.hashCode()))
.not.toIncludeAnyMembers(inferredDat.getGames().map((game) => game.hashCode()));
});

0 comments on commit 865955a

Please sign in to comment.