UNPKG

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
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);