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.
672 lines (671 loc) • 19.7 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
import "reflect-metadata";
import { Expose, Transform, Type } from "class-transformer";
import ArrayUtil from "../../utils/arrayUtil.js";
import Disk from "./disk.js";
import Internationalization from "./internationalization.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"
};
const _Game = class _Game {
static BIOS_REGEX = /\[BIOS\]/i;
static REVISION_NUMBER_REGEX = /\((Rev|Version)\s*([0-9.]+)\)/i;
static REVISION_LETTER_REGEX = /\(Rev\s*([A-Z])\)/i;
static TOSEC_VERSION_REGEX = /\Wv([0-9]+\.[0-9]+)\W/i;
static RING_CODE_REGEX = /\(RE?-?([0-9]*)\)/i;
static AFTERMARKET_REGEX = /\(Aftermarket[a-z0-9. ]*\)/i;
static ALPHA_REGEX = /\(Alpha[a-z0-9. ]*\)/i;
static ALTERNATE_REGEX = /\(Alt( [a-z0-9. ]*)?\)|\[a[0-9]*\]/i;
static BAD_REGEX = /\[b[0-9]*\]/;
static BETA_REGEX = /\(Beta[a-z0-9. ]*\)/i;
static CRACKED_REGEX = /\[cr([0-9]+| [^\]]+)?\]/;
static DEBUG_REGEX = /\(Debug[a-z0-9. ]*\)/i;
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"
);
static ENHANCEMENT_CHIP_REGEX = /\(Enhancement Chip\)/i;
static FIXED_REGEX = /\[f[0-9]*\]/;
static HOMEBREW_REGEX = /\(Homebrew[a-z0-9. ]*\)/i;
static MIA_REGEX = /\[MIA\]/i;
static OVERDUMP_REGEX = /\[o[0-9]*\]/;
static PIRATED_PAREN_REGEX = /\(Pirate[a-z0-9. ]*\)/i;
static PIRATED_BRACKET_REGEX = /\[p[0-9]*\]/;
static PROGRAM_REGEX = /\([a-z0-9. ]*Program\)|(Check|Sample) Program/i;
static PROTOTYPE_REGEX = /\([^)]*Proto[a-z0-9. ]*\)/i;
static SAMPLE_REGEX = /\([^)]*Sample[a-z0-9. ]*\)/i;
static TRANSLATED_REGEX = /\[T[+-][^\]]+\]/;
static UNLICENSED_REGEX = /\(Unl[a-z0-9. ]*\)/i;
static BUNG_FIX_REGEX = /\(Bung\)|\[bf\]/i;
static HACK_PAREN_REGEX = /\(Hack\)/i;
static HACK_BRACKET_REGEX = /\[h[a-zA-Z90-9+]*\]/;
static TRAINER_REGEX = /\[t[0-9]*\]/;
static TWO_LETTER_LANGUAGE_REGEX = /\(([a-zA-Z]{2}([,+-][a-zA-Z]{2})*)\)/;
static THREE_LETTER_LANGUAGE_REGEX = /\(([a-zA-Z]{3}(-[a-zA-Z]{3})*)\)/;
static LONG_REGIONS_REGEX = new RegExp(
`\\(((${Internationalization.REGION_OPTIONS.map((regionOption) => regionOption.long).join("|")})(, (${Internationalization.REGION_OPTIONS.map((regionOption) => regionOption.long).join("|")}))*)\\)`,
"i"
);
name;
isBios = "no";
cloneOf;
romOf;
release;
roms;
disks;
categories;
description;
id;
cloneOfId;
isDevice = "no";
manufacturer;
deviceRef;
genre;
dir2datSource;
region;
language;
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;
this.region = props?.region;
this.language = props?.language;
}
/**
* Create an XML object, to be used by the owning {@link DAT}.
*/
toXmlDatObj(parentNames) {
return {
$: {
name: this.name,
isbios: this.getIsBios() ? "yes" : void 0,
cloneof: this.cloneOf !== void 0 && parentNames.has(this.cloneOf) ? this.cloneOf : void 0,
romof: this.romOf && parentNames.has(this.romOf) ? this.romOf : void 0,
id: this.id,
cloneofid: this.cloneOfId,
isdevice: this.getIsDevice() ? "yes" : void 0
},
...this.dir2datSource !== void 0 && {
xml_comment: this.dir2datSource
},
...this.description !== void 0 && {
description: {
_: this.description
}
},
category: this.getCategories().map((category) => ({ _: category })),
...this.manufacturer !== void 0 && {
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" || _Game.BIOS_REGEX.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() {
const revNumberMatches = _Game.REVISION_NUMBER_REGEX.exec(this.getName());
if (revNumberMatches && revNumberMatches.length >= 3 && !Number.isNaN(revNumberMatches[1])) {
return Number(revNumberMatches[2]);
}
const revLetterMatches = _Game.REVISION_LETTER_REGEX.exec(this.getName());
if (revLetterMatches && revLetterMatches.length >= 2) {
return revLetterMatches[1].toUpperCase().codePointAt(0) - "A".codePointAt(0) + 1;
}
const versionMatches = _Game.TOSEC_VERSION_REGEX.exec(this.getName());
if (versionMatches && versionMatches.length >= 2 && !Number.isNaN(versionMatches[1])) {
return Number(versionMatches[1]);
}
const ringCodeMatches = _Game.RING_CODE_REGEX.exec(this.getName());
if (ringCodeMatches && ringCodeMatches.length >= 2) {
if (ringCodeMatches[1] === "") {
return 1;
}
if (!Number.isNaN(ringCodeMatches[1])) {
return Number(ringCodeMatches[1]);
}
}
return 0;
}
/**
* Is this game aftermarket (released after the last known console release)?
*/
isAftermarket() {
return _Game.AFTERMARKET_REGEX.test(this.name);
}
/**
* Is this game an alpha pre-release?
*/
isAlpha() {
return _Game.ALPHA_REGEX.test(this.name);
}
/**
* Is this game an alternate release?
*/
isAlternate() {
return _Game.ALTERNATE_REGEX.test(this.name);
}
/**
* Is this game a "bad" dump?
*/
isBad() {
if (_Game.BAD_REGEX.test(this.name)) {
return true;
}
if (this.isVerified()) {
return false;
}
return this.name.includes("[c]") || // "known bad checksum but good dump"
this.name.includes("[x]");
}
/**
* Is this game a beta pre-release?
*/
isBeta() {
return _Game.BETA_REGEX.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 _Game.CRACKED_REGEX.test(this.name);
}
/**
* Does this game contain debug symbols?
*/
isDebug() {
return _Game.DEBUG_REGEX.test(this.name);
}
/**
* 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 _Game.ENHANCEMENT_CHIP_REGEX.test(this.name);
}
/**
* Is this game "fixed" (altered to run better in emulation)?
*/
isFixed() {
return _Game.FIXED_REGEX.test(this.name);
}
/**
* Is this game community homebrew?
*/
isHomebrew() {
return _Game.HOMEBREW_REGEX.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 _Game.MIA_REGEX.test(this.name);
}
/**
* Is this game an overdump (contains excess data)?
*/
isOverdump() {
return _Game.OVERDUMP_REGEX.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 _Game.PIRATED_PAREN_REGEX.test(this.name) || _Game.PIRATED_BRACKET_REGEX.test(this.name);
}
/**
* Is this game a "program" app?
*/
isProgram() {
return _Game.PROGRAM_REGEX.test(this.name) || this.getCategories().some((category) => category.toLowerCase() === "applications");
}
/**
* Is this game a prototype?
*/
isPrototype() {
return _Game.PROTOTYPE_REGEX.test(this.name) || this.getCategories().some((category) => category.toLowerCase() === "preproduction");
}
/**
* Is this game a sample?
*/
isSample() {
return _Game.SAMPLE_REGEX.test(this.name);
}
/**
* Is this game translated by the community?
*/
isTranslated() {
return _Game.TRANSLATED_REGEX.test(this.name);
}
/**
* Is this game unlicensed (but was still physically produced and sold)?
*/
isUnlicensed() {
return _Game.UNLICENSED_REGEX.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 _Game.BUNG_FIX_REGEX.test(this.name);
}
/**
* Does this game have a hack?
*/
hasHack() {
return _Game.HACK_PAREN_REGEX.test(this.name) || _Game.HACK_BRACKET_REGEX.test(this.name) || (this.manufacturer?.toLowerCase().includes("hack") ?? false);
}
/**
* Does this game have a trainer?
*/
hasTrainer() {
return _Game.TRAINER_REGEX.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() {
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() !== void 0 || this.getCloneOfId() !== void 0;
}
// Internationalization
getRegions() {
if (this.region !== void 0) {
return [this.region];
}
const longRegionsMatch = this.getName().match(_Game.LONG_REGIONS_REGEX);
if (longRegionsMatch !== null) {
return longRegionsMatch[1].toLowerCase().split(/, ?/).map(
(region) => Internationalization.REGION_OPTIONS.find(
(regionOption) => regionOption.long.toLowerCase() === region
)?.region
).filter((region) => region !== void 0);
}
for (const regionOption of Internationalization.REGION_OPTIONS) {
if (regionOption.regex?.test(this.getName())) {
return [regionOption.region.toUpperCase()];
}
}
const releaseRegions = this.getReleases().map((release) => release.getRegion().toUpperCase());
if (releaseRegions.length > 0) {
return releaseRegions;
}
return [];
}
getLanguages() {
if (this.language !== void 0) {
return [this.language];
}
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 !== void 0);
if (releaseLanguages.length > 0) {
return releaseLanguages;
}
const regionLanguages = this.getLanguagesFromRegions();
if (regionLanguages.length > 0) {
return regionLanguages;
}
return [];
}
getTwoLetterLanguagesFromName() {
const twoMatches = _Game.TWO_LETTER_LANGUAGE_REGEX.exec(this.getName());
if (twoMatches && twoMatches.length >= 2) {
const twoMatchesParsed = twoMatches[1].replace(/-[a-zA-Z]+$/, "").split(/[,+]/).map((lang) => lang.toUpperCase()).filter((lang) => Internationalization.LANGUAGES.includes(lang)).reduce(ArrayUtil.reduceUnique(), []);
if (twoMatchesParsed.length > 0) {
return twoMatchesParsed;
}
}
return [];
}
getThreeLetterLanguagesFromName() {
const threeMatches = _Game.THREE_LETTER_LANGUAGE_REGEX.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 !== void 0 && // Is known
Internationalization.LANGUAGES.includes(lang)
).reduce(ArrayUtil.reduceUnique(), []);
if (threeMatchesParsed.length > 0) {
return threeMatchesParsed;
}
}
return [];
}
getLanguagesFromRegions() {
return this.getRegions().map((region) => {
for (const regionOption of Internationalization.REGION_OPTIONS) {
if (regionOption.region === region) {
return regionOption.language.toUpperCase();
}
}
return void 0;
}).filter((language) => language !== void 0);
}
// 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()).toSorted((a, b) => a.localeCompare(b)).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;
}
};
__decorateClass([
Expose()
], _Game.prototype, "name", 2);
__decorateClass([
Expose({ name: "isbios" })
], _Game.prototype, "isBios", 2);
__decorateClass([
Expose({ name: "cloneof" })
], _Game.prototype, "cloneOf", 2);
__decorateClass([
Expose({ name: "romof" })
], _Game.prototype, "romOf", 2);
__decorateClass([
Expose(),
Type(() => Release),
Transform(({ value }) => value ?? [])
], _Game.prototype, "release", 2);
__decorateClass([
Expose({ name: "rom" }),
Type(() => ROM),
Transform(({ value }) => value ?? [])
], _Game.prototype, "roms", 2);
__decorateClass([
Expose({ name: "disk" }),
Type(() => Disk),
Transform(({ value }) => value ?? [])
], _Game.prototype, "disks", 2);
__decorateClass([
Expose({ name: "category" }),
Transform(({ value }) => value ?? [])
], _Game.prototype, "categories", 2);
__decorateClass([
Expose()
], _Game.prototype, "description", 2);
__decorateClass([
Expose()
], _Game.prototype, "id", 2);
__decorateClass([
Expose({ name: "cloneofid" })
], _Game.prototype, "cloneOfId", 2);
__decorateClass([
Expose({ name: "isdevice" })
], _Game.prototype, "isDevice", 2);
__decorateClass([
Expose()
], _Game.prototype, "manufacturer", 2);
__decorateClass([
Expose({ name: "device_ref" }),
Type(() => DeviceRef),
Transform(({ value }) => value ?? [])
], _Game.prototype, "deviceRef", 2);
__decorateClass([
Expose({ name: "genre" })
], _Game.prototype, "genre", 2);
let Game = _Game;
export {
Game as default
};
//# sourceMappingURL=game.js.map