Skip to content

Commit

Permalink
Feature: support MAME software list DATs (#1031)
Browse files Browse the repository at this point in the history
  • Loading branch information
emmercm committed May 9, 2024
1 parent aef5adb commit 7f59ca6
Show file tree
Hide file tree
Showing 22 changed files with 179,753 additions and 110 deletions.
64 changes: 32 additions & 32 deletions docs/alternatives.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/dats/processing.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ There have been a few DAT-like formats developed over the years. `igir` supports
igir [commands..] --dat "$(which "mame")" --input <input>
```
- [MAME software lists](https://docs.mamedev.org/contributing/softlist.html) (XML exported by the `mame -getsoftlist` command)
- [CMPro](http://www.logiqx.com/DatFAQs/CMPro.php)
- [Hardware Target Game Database](https://github.com/frederic-mahe/Hardware-Target-Game-Database) SMDBs
Expand Down
1 change: 1 addition & 0 deletions src/modules/datParentInferrer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export default class DATParentInferrer extends Module {

private static stripGameVariants(name: string): string {
return name
// TODO(cemmer): strip any directories from the game name (i.e. HTGD)
// ***** Retail types *****
.replace(/\(Alt( [a-z0-9. ]*)?\)/i, '')
.replace(/\([^)]*Collector's Edition\)/i, '')
Expand Down
26 changes: 23 additions & 3 deletions src/modules/datScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import Header from '../types/dats/logiqx/header.js';
import LogiqxDAT from '../types/dats/logiqx/logiqxDat.js';
import MameDAT from '../types/dats/mame/mameDat.js';
import ROM from '../types/dats/rom.js';
import SoftwareListDAT from '../types/dats/softwarelist/softwareListDat.js';
import SoftwareListsDAT from '../types/dats/softwarelist/softwareListsDat.js';
import File from '../types/files/file.js';
import { ChecksumBitmask } from '../types/files/fileChecksums.js';
import Options from '../types/options.js';
Expand Down Expand Up @@ -258,7 +260,25 @@ export default class DATScanner extends Scanner {
try {
return MameDAT.fromObject(datObject.mame);
} catch (error) {
this.progressBar.logTrace(`${datFile.toString()}: failed to parse DAT object: ${error}`);
this.progressBar.logTrace(`${datFile.toString()}: failed to parse MAME DAT object: ${error}`);
return undefined;
}
}

if (datObject.softwarelists) {
try {
return SoftwareListsDAT.fromObject(datObject.softwarelists);
} catch (error) {
this.progressBar.logTrace(`${datFile.toString()}: failed to parse software list DAT object: ${error}`);
return undefined;
}
}

if (datObject.softwarelist) {
try {
return SoftwareListDAT.fromObject(datObject.softwarelist);
} catch (error) {
this.progressBar.logTrace(`${datFile.toString()}: failed to parse software list DAT object: ${error}`);
return undefined;
}
}
Expand All @@ -271,7 +291,7 @@ export default class DATScanner extends Scanner {
/**
* Validation that this might be a CMPro file.
*/
if (fileContents.match(/^(clrmamepro|game|resource) \(\r?\n(\t.+\r?\n)+\)$/m) === null) {
if (fileContents.match(/^(clrmamepro|game|resource) \(\r?\n(\s.+\r?\n)+\)$/m) === null) {
return undefined;
}

Expand Down Expand Up @@ -376,7 +396,7 @@ export default class DATScanner extends Scanner {
sha1: row.sha1,
sha256: row.sha256,
});
const gameName = row.name.replace(/\.[^\\/]+$/, '');
const gameName = row.name.replace(/\.[^.]*$/, '');
return new Game({
name: gameName,
description: gameName,
Expand Down
2 changes: 1 addition & 1 deletion src/types/dats/dat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default abstract class DAT {
* Group all {@link Game} clones together into one {@link Parent}. If no parent/clone information
* exists, then there will be one {@link Parent} for every {@link Game}.
*/
protected generateGameNamesToParents(): DAT {
protected generateGameNamesToParents(): this {
const gameNamesToParents: Map<string, Parent> = new Map();

// Find all parents
Expand Down
4 changes: 4 additions & 0 deletions src/types/dats/datObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { XMLParser } from 'fast-xml-parser';
export interface DATObjectProps {
datafile?: object
mame?: object
softwarelists?: {
softwarelist?: object | object[]
}
softwarelist?: object
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/types/dats/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ export default class Game implements GameProps {
@Expose({ name: 'cloneof' })
readonly cloneOf?: string;

// TODO(cemmer): support cloneofid

@Expose({ name: 'romof' })
readonly romOf?: string;

Expand Down
4 changes: 2 additions & 2 deletions src/types/dats/logiqx/logiqxDat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ export default class LogiqxDAT extends DAT {
}

/**
* Construct a {@link DAT} from a generic object, such as one from reading an XML file.
* Construct a {@link LogiqxDAT} from a generic object, such as one from reading an XML file.
*/
static fromObject(obj: object): DAT {
static fromObject(obj: object): LogiqxDAT {
return plainToInstance(LogiqxDAT, obj, {
enableImplicitConversion: true,
excludeExtraneousValues: true,
Expand Down
10 changes: 5 additions & 5 deletions src/types/dats/mame/mameDat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ export default class MameDAT extends DAT {
@Expose()
private readonly build?: string;

@Expose()
private readonly debug: 'yes' | 'no' = 'no';
// @Expose()
// private readonly debug: 'yes' | 'no' = 'no';

@Expose()
private readonly mameconfig: number = 0;
// @Expose()
// private readonly mameconfig: number = 0;

@Expose()
@Type(() => Machine)
Expand All @@ -34,7 +34,7 @@ export default class MameDAT extends DAT {
/**
* Construct a {@link DAT} from a generic object, such as one from reading an XML file.
*/
static fromObject(obj: object): DAT {
static fromObject(obj: object): MameDAT {
return plainToInstance(MameDAT, obj, {
enableImplicitConversion: true,
excludeExtraneousValues: true,
Expand Down
32 changes: 32 additions & 0 deletions src/types/dats/softwarelist/dataArea.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Expose, Transform, Type } from 'class-transformer';

import ROM from '../rom.js';

/**
* Media image used by a {@link Part}.
*/
export default class DataArea {
// @Expose()
// readonly name?: string;

// @Expose()
// readonly size?: number;

@Expose()
@Type(() => ROM)
@Transform(({ value }) => value || [])
readonly rom?: ROM | ROM[];

constructor(rom: ROM | ROM[]) {
this.rom = rom;
}

getRoms(): ROM[] {
if (Array.isArray(this.rom)) {
return this.rom;
} if (this.rom) {
return [this.rom];
}
return [];
}
}
32 changes: 32 additions & 0 deletions src/types/dats/softwarelist/part.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Expose, Transform, Type } from 'class-transformer';

import DataArea from './dataArea.js';

/**
* Media used by a {@link Software}.
*/
export default class Part {
// @Expose()
// readonly name?: string;

// @Expose()
// readonly interface?: string;

@Expose()
@Type(() => DataArea)
@Transform(({ value }) => value || [])
readonly dataarea?: DataArea | DataArea[];

constructor(dataarea: DataArea | DataArea[]) {
this.dataarea = dataarea;
}

getDataAreas(): DataArea[] {
if (Array.isArray(this.dataarea)) {
return this.dataarea;
} if (this.dataarea) {
return [this.dataarea];
}
return [];
}
}
35 changes: 35 additions & 0 deletions src/types/dats/softwarelist/software.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Expose, Transform, Type } from 'class-transformer';

import Game from '../game.js';
import ROM from '../rom.js';
import Part from './part.js';

/**
* A MAME software image.
*/
export default class Software extends Game {
@Expose()
@Type(() => Part)
@Transform(({ value }) => value || [])
readonly part?: Part | Part[];

constructor(part: Part | Part[]) {
super();
this.part = part;
}

private getParts(): Part[] {
if (Array.isArray(this.part)) {
return this.part;
} if (this.part) {
return [this.part];
}
return [];
}

getRoms(): ROM[] {
return this.getParts()
.flatMap((part) => part.getDataAreas())
.flatMap((dataArea) => dataArea.getRoms());
}
}
57 changes: 57 additions & 0 deletions src/types/dats/softwarelist/softwareListDat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
Expose, plainToInstance, Transform, Type,
} from 'class-transformer';

import DAT from '../dat.js';
import Game from '../game.js';
import Header from '../logiqx/header.js';
import Software from './software.js';

/**
* MAME-schema DAT that documents {@link Software}s.
*/
export default class SoftwareListDAT extends DAT {
@Expose()
readonly name?: string;

@Expose()
readonly description?: string;

@Expose()
@Type(() => Software)
@Transform(({ value }) => value || [])
readonly software?: Software | Software[];

constructor(software: Software | Software[]) {
super();
this.software = software;
}

/**
* Construct a {@link SoftwareListDAT} from a generic object, such as one from reading an XML
* file.
*/
static fromObject(obj: object): SoftwareListDAT {
return plainToInstance(SoftwareListDAT, obj, {
enableImplicitConversion: true,
excludeExtraneousValues: true,
})
.generateGameNamesToParents();
}

getHeader(): Header {
return new Header({
name: this.name,
description: this.description,
});
}

getGames(): Game[] {
if (Array.isArray(this.software)) {
return this.software;
} if (this.software) {
return [this.software];
}
return [];
}
}
53 changes: 53 additions & 0 deletions src/types/dats/softwarelist/softwareListsDat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
Expose, plainToInstance, Transform, Type,
} from 'class-transformer';

import DAT from '../dat.js';
import Game from '../game.js';
import Header from '../logiqx/header.js';
import SoftwareListDAT from './softwareListDat.js';

/**
* MAME-schema DAT that documents {@link SoftwareListDAT}s.
*/
export default class SoftwareListsDAT extends DAT {
@Expose()
@Type(() => SoftwareListDAT)
@Transform(({ value }) => value || [])
readonly softwarelist?: SoftwareListDAT | SoftwareListDAT[];

/**
* Construct a {@link SoftwareListsDAT} from a generic object, such as one from reading an XML
* file.
*/
static fromObject(obj: object): SoftwareListsDAT {
return plainToInstance(SoftwareListsDAT, obj, {
enableImplicitConversion: true,
excludeExtraneousValues: true,
})
.generateGameNamesToParents();
}

getHeader(): Header {
return new Header({
name: this.getSoftwareLists()
.map((softwareList) => softwareList.getName()).join(', '),
description: this.getSoftwareLists()
.map((softwareList) => softwareList.getDescription()).join(', '),
});
}

private getSoftwareLists(): SoftwareListDAT[] {
if (Array.isArray(this.softwarelist)) {
return this.softwarelist;
} if (this.softwarelist) {
return [this.softwarelist];
}
return [];
}

getGames(): Game[] {
return this.getSoftwareLists()
.flatMap((softwareList) => softwareList.getGames());
}
}
Loading

0 comments on commit 7f59ca6

Please sign in to comment.