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.

672 lines (671 loc) • 19.7 kB
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