igir
Version:
🕹 A zero-setup ROM collection manager that sorts, filters, extracts or archives, patches, and reports on collections of any size on any OS.
205 lines (204 loc) • 7.13 kB
JavaScript
import xml2js from 'xml2js';
import FsPoly from '../../polyfill/fsPoly.js';
import { ChecksumBitmask } from '../files/fileChecksums.js';
import Parent from './parent.js';
/**
* The base class for other DAT classes.
*/
export default class DAT {
filePath;
parents = [];
constructor(props) {
this.filePath = props?.filePath;
}
/**
* 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}.
*/
generateGameNamesToParents() {
if (this.getGames().length === 0) {
// Nothing to do
return this;
}
const gameNamesToParents = new Map();
const gameIdsToParents = new Map();
// Find all parents
this.getGames().forEach((game) => {
if (game.getCloneOfId() !== undefined) {
// Is a clone
return;
}
const id = game.getId();
if (id !== undefined) {
const parent = gameIdsToParents.get(id);
if (parent === undefined) {
gameIdsToParents.set(id, new Parent(game));
}
else {
// Two games have the same name, assume this one is a clone
parent.addChild(game);
}
return;
}
if (game.getCloneOf() !== undefined) {
// Is a clone
return;
}
const parent = gameNamesToParents.get(game.getName());
if (parent === undefined) {
gameNamesToParents.set(game.getName(), new Parent(game));
}
else {
// Two games have the same name, assume this one is a clone
parent.addChild(game);
}
});
// Find all clones
this.getGames().forEach((game) => {
const cloneOfId = game.getCloneOfId();
if (cloneOfId !== undefined) {
const id = game.getId();
const parent = gameIdsToParents.get(cloneOfId);
if (parent) {
parent.addChild(game);
}
else if (id !== undefined) {
// The DAT is bad, the game is referencing a parent that doesn't exist
gameIdsToParents.set(cloneOfId, new Parent(game));
}
return;
}
const cloneOf = game.getCloneOf();
if (cloneOf !== undefined) {
const parent = gameNamesToParents.get(cloneOf);
if (parent) {
parent.addChild(game);
}
else {
// The DAT is bad, the game is referencing a parent that doesn't exist
gameNamesToParents.set(cloneOf, new Parent(game));
}
return;
}
});
this.parents = [...gameIdsToParents.values(), ...gameNamesToParents.values()];
return this;
}
getFilePath() {
return this.filePath;
}
getParents() {
return this.parents;
}
/**
* Does any {@link Game} in this {@link DAT} have clone information.
*/
hasParentCloneInfo() {
return this.getParents().length > 0 && this.getParents().length !== this.getGames().length;
}
getName() {
return this.getHeader().getName().trim();
}
getDisplayName() {
return (this.getName()
// Cleanup
.replaceAll(/-( +-)+/g, '- ')
.replace(/^[ -]+/, '')
.replace(/[ -]+$/, '')
.replaceAll(/ +/g, ' ')
.trim());
}
getDescription() {
return this.getHeader().getDescription();
}
/**
* Get a No-Intro style filename.
*/
getFilename() {
let filename = this.getName();
if (this.getHeader().getVersion()) {
filename += ` (${this.getHeader().getVersion()})`;
}
filename += '.dat';
return FsPoly.makeLegal(filename.trim());
}
getRequiredRomChecksumBitmask() {
let checksumBitmask = 0;
this.getGames().forEach((game) => {
game.getRoms().forEach((rom) => {
if (rom.getCrc32() && rom.getSize()) {
checksumBitmask |= ChecksumBitmask.CRC32;
}
else if (rom.getMd5()) {
checksumBitmask |= ChecksumBitmask.MD5;
}
else if (rom.getSha1()) {
checksumBitmask |= ChecksumBitmask.SHA1;
}
else if (rom.getSha256()) {
checksumBitmask |= ChecksumBitmask.SHA256;
}
});
});
return checksumBitmask;
}
getRequiredDiskChecksumBitmask() {
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.
*/
toXmlDat() {
// TODO(cemmer): replace with fast-xml-parser https://github.com/NaturalIntelligence/fast-xml-parser/issues/639
return new xml2js.Builder({
renderOpts: { pretty: true, indent: '\t', newline: '\n' },
xmldec: { version: '1.0' },
doctype: {
pubID: '-//Logiqx//DTD ROM Management Datafile//EN',
sysID: 'http://www.logiqx.com/Dats/datafile.dtd',
},
cdata: true,
})
.buildObject(this.toXmlDatObj())
.replaceAll('<xml_comment>', '<!-- ')
.replaceAll('</xml_comment>', ' -->');
}
toXmlDatObj() {
const parentNames = new Set(this.getParents().map((parent) => parent.getName()));
return {
datafile: {
header: this.getHeader().toXmlDatObj(),
game: this.getGames().map((game) => game.toXmlDatObj(parentNames)),
},
};
}
/**
* Return a short string representation of this {@link DAT}.
*/
toString() {
return JSON.stringify({
header: this.getHeader(),
games: this.getGames().length,
gamesSize: FsPoly.sizeReadable(this.getGames()
.flatMap((game) => game.getRoms())
.reduce((sum, rom) => sum + rom.getSize(), 0), 2),
}, undefined, 2);
}
}