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.
697 lines (696 loc) • 22 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import 'reflect-metadata';
import { Expose, Transform, Type } from 'class-transformer';
import ArrayPoly from '../../polyfill/arrayPoly.js';
import Internationalization from '../internationalization.js';
import Disk from './disk.js';
import DeviceRef from './mame/deviceRef.js';
import Release from './release.js';
import ROM from './rom.js';
const GameType = {
AFTERMARKET: 'Aftermarket',
ALPHA: 'Alpha',
BAD: 'Bad',
BETA: 'Beta',
BIOS: 'BIOS',
CRACKED: 'Cracked',
DEBUG: 'Debug',
DEMO: 'Demo',
DEVICE: 'Device',
FIXED: 'Fixed',
HACKED: 'Hacked',
HOMEBREW: 'Homebrew',
OVERDUMP: 'Overdump',
PENDING_DUMP: 'Pending Dump',
PIRATED: 'Pirated',
PROGRAM: 'Program',
PROTOTYPE: 'Prototype',
RETAIL: 'Retail',
SAMPLE: 'Sample',
TRAINED: 'Trained',
TRANSLATED: 'Translated',
UNLICENSED: 'Unlicensed',
};
/**
* A logical "game" that contains zero or more {@link ROM}s, and has zero or more region
* {@link Release}s.
*/
export default class Game {
name;
isBios = 'no';
cloneOf;
romOf;
release;
roms;
disks;
categories;
description;
id;
cloneOfId;
isDevice = 'no';
manufacturer;
deviceRef;
genre;
dir2datSource;
constructor(props) {
this.name = props?.name ?? '';
this.isBios = props?.isBios ?? this.isBios;
this.cloneOf = props?.cloneOf;
this.romOf = props?.romOf;
this.release = props?.release ?? [];
this.roms = props?.roms ?? [];
this.disks = props?.disks ?? [];
this.categories = props?.categories ?? [];
this.description = props?.description;
this.id = props?.id;
this.cloneOfId = props?.cloneOfId;
this.isDevice = props?.isDevice ?? this.isDevice;
this.manufacturer = props?.manufacturer;
this.deviceRef = props?.deviceRef ?? [];
this.genre = props?.genre;
this.dir2datSource = props?.dir2datSource;
}
/**
* Create an XML object, to be used by the owning {@link DAT}.
*/
toXmlDatObj(parentNames) {
return {
$: {
name: this.name,
isbios: this.getIsBios() ? 'yes' : undefined,
cloneof: this.cloneOf !== undefined && parentNames.has(this.cloneOf) ? this.cloneOf : undefined,
romof: this.romOf && parentNames.has(this.romOf) ? this.romOf : undefined,
id: this.id,
cloneofid: this.cloneOfId,
isdevice: this.getIsDevice() ? 'yes' : undefined,
},
...(this.dir2datSource === undefined
? {}
: {
xml_comment: this.dir2datSource,
}),
...(this.description === undefined
? {}
: {
description: {
_: this.description,
},
}),
category: this.getCategories().map((category) => ({ _: category })),
...(this.manufacturer === undefined
? {}
: {
manufacturer: {
_: this.manufacturer,
},
}),
release: this.getReleases().map((release) => release.toXmlDatObj()),
rom: this.getRoms().map((rom) => rom.toXmlDatObj()),
disk: this.getDisks().map((disk) => disk.toXmlDatObj()),
};
}
// Property getters
getName() {
return this.name;
}
getCategories() {
if (Array.isArray(this.categories)) {
return this.categories;
}
return [this.categories];
}
/**
* Is this game a collection of BIOS file(s).
*/
getIsBios() {
return this.isBios === 'yes' || /\[BIOS\]/i.test(this.name);
}
/**
* Is this game a MAME "device"?
*/
getIsDevice() {
return this.isDevice === 'yes';
}
getDeviceRefs() {
if (Array.isArray(this.deviceRef)) {
return this.deviceRef;
}
return [this.deviceRef];
}
getGenre() {
return this.genre;
}
getReleases() {
if (Array.isArray(this.release)) {
return this.release;
}
return [this.release];
}
getRoms() {
if (Array.isArray(this.roms)) {
return this.roms;
}
return [this.roms];
}
getDisks() {
if (Array.isArray(this.disks)) {
return this.disks;
}
return [this.disks];
}
getCloneOf() {
return this.cloneOf;
}
getRomOf() {
return this.romOf;
}
getId() {
return this.id;
}
getCloneOfId() {
return this.cloneOfId;
}
// Computed getters
getRevision() {
// Numeric revision
const revNumberMatches = /\((Rev|Version)\s*([0-9.]+)\)/i.exec(this.getName());
if (revNumberMatches && revNumberMatches.length >= 3 && !Number.isNaN(revNumberMatches[1])) {
return Number(revNumberMatches[2]);
}
// Letter revision
const revLetterMatches = /\(Rev\s*([A-Z])\)/i.exec(this.getName());
if (revLetterMatches && revLetterMatches.length >= 2) {
return (revLetterMatches[1].toUpperCase().codePointAt(0) -
'A'.codePointAt(0) +
1);
}
// TOSEC versions
const versionMatches = /\Wv([0-9]+\.[0-9]+)\W/i.exec(this.getName());
if (versionMatches && versionMatches.length >= 2 && !Number.isNaN(versionMatches[1])) {
return Number(versionMatches[1]);
}
// Ring code revision
const ringCodeMatches = /\(RE?-?([0-9]*)\)/i.exec(this.getName());
if (ringCodeMatches && ringCodeMatches.length >= 2) {
if (ringCodeMatches[1] === '') {
// Redump doesn't always include a number
return 1;
}
else if (!Number.isNaN(ringCodeMatches[1])) {
return Number(ringCodeMatches[1]);
}
}
return 0;
}
/**
* Is this game aftermarket (released after the last known console release)?
*/
isAftermarket() {
return /\(Aftermarket[a-z0-9. ]*\)/i.test(this.name);
}
/**
* Is this game an alpha pre-release?
*/
isAlpha() {
return /\(Alpha[a-z0-9. ]*\)/i.test(this.name);
}
/**
* Is this game an alternate release?
*/
isAlternate() {
return /\(Alt( [a-z0-9. ]*)?\)|\[a[0-9]*\]/i.test(this.name);
}
/**
* Is this game a "bad" dump?
*/
isBad() {
if (/\[b[0-9]*\]/.test(this.name)) {
return true;
}
if (this.isVerified()) {
// Sometimes [!] can get mixed with [c], consider it not bad
return false;
}
return (this.name.includes('[c]') || // "known bad checksum but good dump"
this.name.includes('[x]')); // "thought to have a bad checksum"
}
/**
* Is this game a beta pre-release?
*/
isBeta() {
return /\(Beta[a-z0-9. ]*\)/i.test(this.name);
}
/**
* Is this game an unlicensed bootleg?
*/
isBootleg() {
return this.manufacturer?.toLowerCase().includes('bootleg') ?? false;
}
/**
* Is this game a "cracked" release (has copy protection removed)?
*/
isCracked() {
return /\[cr([0-9]+| [^\]]+)?\]/.test(this.name);
}
/**
* Does this game contain debug symbols?
*/
isDebug() {
return /\(Debug[a-z0-9. ]*\)/i.test(this.name);
}
static DEMO_REGEX = new RegExp([
'\\(Demo[a-z0-9. -]*\\)',
'@barai',
'\\(Kiosk[a-z0-9. -]*\\)',
'\\(Preview\\)',
'GameCube Preview',
'Kiosk Demo Disc',
'PS2 Kiosk',
'PSP System Kiosk',
'Taikenban', // "trial"
'Trial Edition',
].join('|'), 'i');
/**
* Is this game a demo?
*/
isDemo() {
return (this.name.match(Game.DEMO_REGEX) !== null ||
this.getCategories().some((category) => category.toLowerCase() === 'demos'));
}
/**
* Is this game an enhancement chip? Primarily for SNES
*/
isEnhancementChip() {
return /\(Enhancement Chip\)/i.test(this.name);
}
/**
* Is this game "fixed" (altered to run better in emulation)?
*/
isFixed() {
return /\[f[0-9]*\]/.test(this.name);
}
/**
* Is this game community homebrew?
*/
isHomebrew() {
return /\(Homebrew[a-z0-9. ]*\)/i.test(this.name);
}
/**
* Is this game MIA (has not been dumped yet)?
*
* NOTE(cemmer): RomVault indicates that some DATs include <rom mia="yes"/>, but I did not find
* any evidence of this in No-Intro, Redump, TOSEC, and FinalBurn Neo.
* https://wiki.romvault.com/doku.php?id=mia_rom_tracking#can_i_manually_flag_roms_as_mia
*/
isMIA() {
return /\[MIA\]/i.test(this.name);
}
/**
* Is this game an overdump (contains excess data)?
*/
isOverdump() {
return /\[o[0-9]*\]/.test(this.name);
}
/**
* Is this game a pending dump (works, but isn't a proper dump)?
*/
isPendingDump() {
return this.name.includes('[!p]');
}
/**
* Is this game pirated (probably has copyright information removed)?
*/
isPirated() {
return /\(Pirate[a-z0-9. ]*\)/i.test(this.name) || /\[p[0-9]*\]/.test(this.name);
}
/**
* Is this game a "program" application?
*/
isProgram() {
return (/\([a-z0-9. ]*Program\)|(Check|Sample) Program/i.test(this.name) ||
this.getCategories().some((category) => category.toLowerCase() === 'applications'));
}
/**
* Is this game a prototype?
*/
isPrototype() {
return (/\([^)]*Proto[a-z0-9. ]*\)/i.test(this.name) ||
this.getCategories().some((category) => category.toLowerCase() === 'preproduction'));
}
/**
* Is this game a sample?
*/
isSample() {
return /\([^)]*Sample[a-z0-9. ]*\)/i.test(this.name);
}
/**
* Is this game translated by the community?
*/
isTranslated() {
return /\[T[+-][^\]]+\]/.test(this.name);
}
/**
* Is this game unlicensed (but was still physically produced and sold)?
*/
isUnlicensed() {
return /\(Unl[a-z0-9. ]*\)/i.test(this.name);
}
/**
* Is this game an explicitly verified dump?
*/
isVerified() {
return this.name.includes('[!]');
}
/**
* Was this game altered to work on a Bung cartridge?
* @see https://en.wikipedia.org/wiki/Bung_Enterprises
*/
hasBungFix() {
return /\(Bung\)|\[bf\]/i.test(this.name);
}
/**
* Does this game have a hack?
*/
hasHack() {
return (/\(Hack\)/i.test(this.name) ||
/\[h[a-zA-Z90-9+]*\]/.test(this.name) ||
(this.manufacturer?.toLowerCase().includes('hack') ?? false));
}
/**
* Does this game have a trainer?
*/
hasTrainer() {
return /\[t[0-9]*\]/.test(this.name);
}
/**
* Is this game "retail"?
*/
isRetail() {
return (
// Has their own dedicated filters
!this.isDebug() &&
!this.isDemo() &&
!this.isBeta() &&
!this.isSample() &&
!this.isPrototype() &&
!this.isProgram() &&
!this.isAftermarket() &&
!this.isHomebrew() &&
!this.isBad() &&
// Doesn't have their own dedicated filter
!this.isAlpha() &&
!this.isBootleg() &&
!this.isCracked() &&
!this.isEnhancementChip() &&
!this.isFixed() &&
!this.isMIA() &&
!this.isOverdump() &&
!this.isPendingDump() &&
!this.isPirated() &&
!this.isTranslated() &&
!this.hasBungFix() &&
!this.hasHack() &&
!this.hasTrainer());
}
getGameType() {
// NOTE(cemmer): priority here matters!
if (this.getIsBios()) {
return GameType.BIOS;
}
if (this.isVerified()) {
return GameType.RETAIL;
}
if (this.isAftermarket()) {
return GameType.AFTERMARKET;
}
if (this.isAlpha()) {
return GameType.ALPHA;
}
if (this.isBad()) {
return GameType.BAD;
}
if (this.isBeta()) {
return GameType.BETA;
}
if (this.isCracked()) {
return GameType.CRACKED;
}
if (this.isDebug()) {
return GameType.DEBUG;
}
if (this.isDemo()) {
return GameType.DEMO;
}
if (this.getIsDevice()) {
return GameType.DEVICE;
}
if (this.isFixed()) {
return GameType.FIXED;
}
if (this.hasHack()) {
return GameType.HACKED;
}
if (this.isHomebrew()) {
return GameType.HOMEBREW;
}
if (this.isOverdump()) {
return GameType.OVERDUMP;
}
if (this.isPendingDump()) {
return GameType.PENDING_DUMP;
}
if (this.isPirated()) {
return GameType.PIRATED;
}
if (this.isProgram()) {
return GameType.PROGRAM;
}
if (this.isPrototype()) {
return GameType.PROTOTYPE;
}
if (this.isSample()) {
return GameType.SAMPLE;
}
if (this.hasTrainer()) {
return GameType.TRAINED;
}
if (this.isTranslated()) {
return GameType.TRANSLATED;
}
if (this.isUnlicensed()) {
return GameType.UNLICENSED;
}
return GameType.RETAIL;
}
/**
* Is this game a parent (is not a clone)?
*/
isParent() {
return !this.isClone();
}
/**
* Is this game a clone?
*/
isClone() {
return this.getCloneOf() !== undefined || this.getCloneOfId() !== undefined;
}
// Internationalization
getRegions() {
const longRegions = Internationalization.REGION_OPTIONS.map((regionOption) => regionOption.long).join('|');
const longRegionsRegex = new RegExp(`\\(((${longRegions})(, (${longRegions}))*)\\)`, 'i');
const longRegionsMatch = this.getName().match(longRegionsRegex);
if (longRegionsMatch !== null) {
return longRegionsMatch[1]
.toLowerCase()
.split(/, ?/)
.map((region) => Internationalization.REGION_OPTIONS.find((regionOption) => regionOption.long.toLowerCase() === region)?.region)
.filter((region) => region !== undefined);
}
for (const regionOption of Internationalization.REGION_OPTIONS) {
if (regionOption.regex?.test(this.getName())) {
return [regionOption.region.toUpperCase()];
}
}
// Note: <release>s tend to be less reliable than game names
const releaseRegions = this.getReleases().map((release) => release.getRegion().toUpperCase());
if (releaseRegions.length > 0) {
return releaseRegions;
}
return [];
}
getLanguages() {
const shortLanguages = this.getTwoLetterLanguagesFromName();
if (shortLanguages.length > 0) {
return shortLanguages;
}
const longLanguages = this.getThreeLetterLanguagesFromName();
if (longLanguages.length > 0) {
return longLanguages;
}
const releaseLanguages = this.getReleases()
.map((release) => release.getLanguage())
.filter((language) => language !== undefined);
if (releaseLanguages.length > 0) {
return releaseLanguages;
}
// Note: <release>s tend to be less reliable than game names
const regionLanguages = this.getLanguagesFromRegions();
if (regionLanguages.length > 0) {
return regionLanguages;
}
return [];
}
getTwoLetterLanguagesFromName() {
const twoMatches = /\(([a-zA-Z]{2}([,+-][a-zA-Z]{2})*)\)/.exec(this.getName());
if (twoMatches && twoMatches.length >= 2) {
const twoMatchesParsed = twoMatches[1]
.replace(/-[a-zA-Z]+$/, '') // chop off country
.split(/[,+]/)
.map((lang) => lang.toUpperCase())
.filter((lang) => Internationalization.LANGUAGES.includes(lang)) // is known
.reduce(ArrayPoly.reduceUnique(), []);
if (twoMatchesParsed.length > 0) {
return twoMatchesParsed;
}
}
return [];
}
getThreeLetterLanguagesFromName() {
// Get language from long languages in the game name
const threeMatches = /\(([a-zA-Z]{3}(-[a-zA-Z]{3})*)\)/.exec(this.getName());
if (threeMatches && threeMatches.length >= 2) {
const threeMatchesParsed = threeMatches[1]
.split('-')
.map((lang) => lang.toUpperCase())
.map((lang) => Internationalization.LANGUAGE_OPTIONS.find((langOpt) => langOpt.long?.toUpperCase() === lang.toUpperCase())?.short)
.filter((lang) => lang !== undefined &&
// Is known
Internationalization.LANGUAGES.includes(lang))
.reduce(ArrayPoly.reduceUnique(), []);
if (threeMatchesParsed.length > 0) {
return threeMatchesParsed;
}
}
return [];
}
getLanguagesFromRegions() {
// Get languages from regions
return this.getRegions()
.map((region) => {
for (const regionOption of Internationalization.REGION_OPTIONS) {
if (regionOption.region === region) {
return regionOption.language.toUpperCase();
}
}
return undefined;
})
.filter((language) => language !== undefined);
}
// Immutable setters
/**
* Return a new copy of this {@link Game} with some different properties.
*/
withProps(props) {
return new Game({ ...this, ...props });
}
// Pseudo Built-Ins
/**
* A string hash code to uniquely identify this {@link Game}.
*/
hashCode() {
let hashCode = this.getName();
hashCode += `|${this.getRoms()
.map((rom) => rom.hashCode())
.sort()
.join(',')}`;
return hashCode;
}
/**
* Is this {@link Game} equal to another {@link Game}?
*/
equals(other) {
if (this === other) {
return true;
}
return (this.getName() === other.getName() &&
this.getReleases().length === other.getReleases().length &&
this.getRoms().length === other.getRoms().length);
}
}
__decorate([
Expose(),
__metadata("design:type", String)
], Game.prototype, "name", void 0);
__decorate([
Expose({ name: 'isbios' }),
__metadata("design:type", String)
], Game.prototype, "isBios", void 0);
__decorate([
Expose({ name: 'cloneof' }),
__metadata("design:type", String)
], Game.prototype, "cloneOf", void 0);
__decorate([
Expose({ name: 'romof' }),
__metadata("design:type", String)
], Game.prototype, "romOf", void 0);
__decorate([
Expose(),
Type(() => Release),
Transform(({ value }) => value ?? []),
__metadata("design:type", Object)
], Game.prototype, "release", void 0);
__decorate([
Expose({ name: 'rom' }),
Type(() => ROM),
Transform(({ value }) => value ?? []),
__metadata("design:type", Object)
], Game.prototype, "roms", void 0);
__decorate([
Expose({ name: 'disk' }),
Type(() => Disk),
Transform(({ value }) => value ?? []),
__metadata("design:type", Object)
], Game.prototype, "disks", void 0);
__decorate([
Expose({ name: 'category' }),
Transform(({ value }) => value ?? []),
__metadata("design:type", Object)
], Game.prototype, "categories", void 0);
__decorate([
Expose(),
__metadata("design:type", String)
], Game.prototype, "description", void 0);
__decorate([
Expose(),
__metadata("design:type", String)
], Game.prototype, "id", void 0);
__decorate([
Expose({ name: 'cloneofid' }),
__metadata("design:type", String)
], Game.prototype, "cloneOfId", void 0);
__decorate([
Expose({ name: 'isdevice' }),
__metadata("design:type", String)
], Game.prototype, "isDevice", void 0);
__decorate([
Expose(),
__metadata("design:type", String)
], Game.prototype, "manufacturer", void 0);
__decorate([
Expose({ name: 'device_ref' }),
Type(() => DeviceRef),
Transform(({ value }) => value ?? []),
__metadata("design:type", Object)
], Game.prototype, "deviceRef", void 0);
__decorate([
Expose({ name: 'genre' }),
__metadata("design:type", String)
], Game.prototype, "genre", void 0);