UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

323 lines (322 loc) • 15.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ModUpgraded = void 0; const isaac_typescript_definitions_1 = require("isaac-typescript-definitions"); const callbacks_1 = require("../callbacks"); const decorators_1 = require("../decorators"); const ISCFeature_1 = require("../enums/ISCFeature"); const ModCallbackCustom_1 = require("../enums/ModCallbackCustom"); const features_1 = require("../features"); const debugFunctions_1 = require("../functions/debugFunctions"); const enums_1 = require("../functions/enums"); const log_1 = require("../functions/log"); const tstlClass_1 = require("../functions/tstlClass"); const types_1 = require("../functions/types"); const utils_1 = require("../functions/utils"); /** * `isaacscript-common` has many custom callbacks that you can use in your mods. Instead of * hijacking the vanilla `Mod` object, we provide a `ModUpgraded` object for you to use, which * extends the base class and adds a new method of `AddCallbackCustom`. * * To upgrade your mod, use the `upgradeMod` helper function. * * By specifying one or more optional features when upgrading your mod, you will get a version of * `ModUpgraded` that has extra methods corresponding to the features that were specified. (This * corresponds to the internal-type `ModUpgradedWithFeatures` type, which extends `ModUpgraded`.) */ class ModUpgraded { // ----------------- // Vanilla variables // ----------------- Name; // ---------------- // Custom variables // ---------------- /** We store a copy of the original mod object so that we can re-implement its functions. */ mod; debug; timeThreshold; callbacks; features; // ----------- // Constructor // ----------- constructor(mod, debug, timeThreshold) { this.Name = mod.Name; this.mod = mod; this.debug = debug; this.timeThreshold = timeThreshold; this.callbacks = (0, callbacks_1.getCallbacks)(); this.features = (0, features_1.getFeatures)(this, this.callbacks); } // --------------- // Vanilla methods // --------------- AddCallback(modCallback, ...args) { this.AddPriorityCallback(modCallback, isaac_typescript_definitions_1.CallbackPriority.DEFAULT, ...args); } AddPriorityCallback(modCallback, priority, ...args) { if (this.debug) { const callback = args[0]; const optionalArg = args[1]; const parentFunctionDescription = (0, log_1.getParentFunctionDescription)(); const customCallback = type(modCallback) === "string"; const callbackName = customCallback ? `${modCallback} (custom callback)` : `ModCallback.${isaac_typescript_definitions_1.ModCallback[modCallback]}`; const signature = parentFunctionDescription === undefined ? callbackName : `${parentFunctionDescription} - ${callbackName}`; /** * We don't use the "log" helper function here since it will always show the same "unknown" * prefix. */ const callbackWithLogger = ( // @ts-expect-error The compiler is not smart enough to know that the callback args should // match the callback. ...callbackArgs) => { const startTime = (0, debugFunctions_1.getTime)(); Isaac.DebugString(`${signature} - START`); // @ts-expect-error The compiler is not smart enough to know that the callback args should // match the callback. // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const returnValue = callback(...callbackArgs); const elapsedTime = (0, debugFunctions_1.getElapsedTimeSince)(startTime); if (this.timeThreshold === undefined || this.timeThreshold <= elapsedTime) { Isaac.DebugString(`${signature} - END - time: ${elapsedTime}`); } else { Isaac.DebugString(`${signature} - END`); } // eslint-disable-next-line @typescript-eslint/no-unsafe-return return returnValue; }; const newArgs = [callbackWithLogger, optionalArg]; // @ts-expect-error The compiler is not smart enough to know that the callback args should // match the callback. this.mod.AddPriorityCallback(modCallback, priority, ...newArgs); } else { this.mod.AddPriorityCallback(modCallback, priority, ...args); } } HasData() { return this.mod.HasData(); } LoadData() { return this.mod.LoadData(); } RemoveCallback(modCallback, callback) { this.mod.RemoveCallback(modCallback, callback); } RemoveData() { this.mod.RemoveData(); } SaveData(data) { this.mod.SaveData(data); } // --------------------- // Custom public methods // --------------------- /** * Registers a function to be executed when an in-game event happens. * * This method is specifically for events that are provided by the IsaacScript standard library. * For example, the `ModCallbackCustom.POST_BOMB_EXPLODE` event corresponds to when a bomb * explodes. */ AddCallbackCustom(modCallbackCustom, ...args) { this.AddPriorityCallbackCustom(modCallbackCustom, isaac_typescript_definitions_1.CallbackPriority.DEFAULT, ...args); } /** * The same as the `ModUpgraded.AddCallbackCustom` method, but allows setting a custom priority. * By default, callbacks are added with a priority of 0, so this allows you to add early or late * callbacks as necessary. See the `CallbackPriority` enum. */ AddPriorityCallbackCustom(modCallbackCustom, priority, ...args) { const callbackClass = this.callbacks[modCallbackCustom]; // @ts-expect-error The compiler is not smart enough to figure out that the parameters match. // eslint-disable-next-line complete/require-variadic-function-argument callbackClass.addSubscriber(priority, ...args); this.initFeature(callbackClass); } /** * Unregisters a function that was previously registered with the `AddCallbackCustom` method. * * This method is specifically for events that are provided by the IsaacScript standard library. * For example, the `ModCallbackCustom.POST_BOMB_EXPLODE` event corresponds to when a bomb * explodes. * * This method does not care about the tertiary argument. In other words, regardless of the * conditions of how you registered the callback, it will be removed. */ RemoveCallbackCustom(modCallbackCustom, callback) { const callbackClass = this.callbacks[modCallbackCustom]; // @ts-expect-error The compiler is not smart enough to figure out that the parameters match. callbackClass.removeSubscriber(callback); this.uninitFeature(callbackClass); } /** * Logs every custom callback or extra feature that is currently enabled. Useful for debugging or * profiling. */ logUsedFeatures() { // Custom callbacks for (const [modCallbackCustomString, callbackClass] of Object.entries(this.callbacks)) { if (callbackClass.numConsumers === 0) { continue; } const modCallbackCustom = (0, types_1.parseIntSafe)(modCallbackCustomString); (0, utils_1.assertDefined)(modCallbackCustom, `Failed to convert the string "${modCallbackCustomString}" representing a "ModCallbackCustom" value to a number.`); if (!(0, enums_1.isEnumValue)(modCallbackCustom, ModCallbackCustom_1.ModCallbackCustom)) { error(`Failed to convert the number ${modCallbackCustom} to a "ModCallbackCustom" value.`); } (0, log_1.log)(`- ModCallbackCustom.${ModCallbackCustom_1.ModCallbackCustom[modCallbackCustom]} (${modCallbackCustom})`); } // Extra features for (const [iscFeatureString, featureClass] of Object.entries(this.features)) { if (featureClass.numConsumers === 0) { continue; } const iscFeature = (0, types_1.parseIntSafe)(iscFeatureString); (0, utils_1.assertDefined)(iscFeature, `Failed to convert the string "${iscFeatureString}" representing a "ISCFeature" value to a number.`); if (!(0, enums_1.isEnumValue)(iscFeature, ISCFeature_1.ISCFeature)) { error(`Failed to convert the number ${iscFeature} to a "ISCFeature" value.`); } (0, log_1.log)(`- ISCFeature.${ISCFeature_1.ISCFeature[iscFeature]} (${iscFeature})`); } } // ---------------------- // Custom private methods // ---------------------- /** * This is used to initialize both custom callbacks and "extra features". * * This mirrors the `uninitFeature` method. */ initFeature(feature) { feature.numConsumers++; if (feature.initialized) { return; } feature.initialized = true; if (feature.v !== undefined) { feature.featuresUsed ??= []; if (!feature.featuresUsed.includes(ISCFeature_1.ISCFeature.SAVE_DATA_MANAGER)) { feature.featuresUsed.unshift(ISCFeature_1.ISCFeature.SAVE_DATA_MANAGER); } } if (feature.featuresUsed !== undefined) { for (const featureUsed of feature.featuresUsed) { const featureClass = this.features[featureUsed]; this.initFeature(featureClass); } } if (feature.callbacksUsed !== undefined) { for (const callbackTuple of feature.callbacksUsed) { const [modCallback, callbackFunc, optionalArgs] = callbackTuple; // TypeScript is not smart enough to know that the arguments match the function. this.AddPriorityCallback(modCallback, isaac_typescript_definitions_1.CallbackPriority.IMPORTANT, callbackFunc, ...(optionalArgs ?? [])); } } if (feature.customCallbacksUsed !== undefined) { for (const callbackTuple of feature.customCallbacksUsed) { const [modCallback, callbackFunc, optionalArgs] = callbackTuple; // TypeScript is not smart enough to know that the arguments match the function. this.AddPriorityCallbackCustom(modCallback, isaac_typescript_definitions_1.CallbackPriority.IMPORTANT, callbackFunc, ...(optionalArgs ?? [])); } } if (feature.v !== undefined) { const className = (0, tstlClass_1.getTSTLClassName)(feature); (0, utils_1.assertDefined)(className, "Failed to get the name of a feature."); const saveDataManagerClass = this.features[ISCFeature_1.ISCFeature.SAVE_DATA_MANAGER]; saveDataManagerClass.saveDataManager(className, feature.v, feature.vConditionalFunc); } } /** * This is used to uninitialize both custom callbacks and "extra features". * * This mirrors the `initFeature` method. */ uninitFeature(feature) { if (feature.numConsumers <= 0) { const className = (0, tstlClass_1.getTSTLClassName)(feature) ?? "unknown"; error(`Failed to uninit feature "${className}" since it has ${feature.numConsumers} consumers, which should never happen.`); } if (!feature.initialized) { const className = (0, tstlClass_1.getTSTLClassName)(feature) ?? "unknown"; error(`Failed to uninit feature "${className}" since it was not initialized, which should never happen.`); } feature.numConsumers--; if (feature.numConsumers > 0) { return; } feature.initialized = false; if (feature.featuresUsed !== undefined) { for (const featureUsed of feature.featuresUsed) { const featureClass = this.features[featureUsed]; this.uninitFeature(featureClass); } } if (feature.callbacksUsed !== undefined) { for (const callbackTuple of feature.callbacksUsed) { const [modCallback, callbackFunc] = callbackTuple; this.RemoveCallback(modCallback, callbackFunc); } } if (feature.customCallbacksUsed !== undefined) { for (const callbackTuple of feature.customCallbacksUsed) { const [modCallback, callbackFunc] = callbackTuple; this.RemoveCallbackCustom(modCallback, callbackFunc); } } if (feature.v !== undefined) { const className = (0, tstlClass_1.getTSTLClassName)(feature); (0, utils_1.assertDefined)(className, "Failed to get the name of a feature."); const saveDataManagerClass = this.features[ISCFeature_1.ISCFeature.SAVE_DATA_MANAGER]; saveDataManagerClass.saveDataManagerRemove(className); } } /** * Returns the names of the exported class methods from the features that were added. This is * called from the "upgradeMod" function, but we want to mark it as private so that end-users * don't have access to it. */ initOptionalFeature(feature) { const featureClass = this.features[feature]; this.initFeature(featureClass); return getExportedMethodsFromFeature(featureClass); } } exports.ModUpgraded = ModUpgraded; /** * In this context, "exported" methods are methods that are annotated with the "@Exported" * decorator, which signify that the method should be attached to the `ModUpgraded` class. * * Exported methods are stored in an internal static array on the class that is created by the * decorator. */ function getExportedMethodsFromFeature(featureClass) { const constructor = (0, tstlClass_1.getTSTLClassConstructor)(featureClass); const exportedMethodNames = constructor[decorators_1.EXPORTED_METHOD_NAMES_KEY]; if (exportedMethodNames === undefined) { return []; } return exportedMethodNames.map((name) => { const featureClassRecord = featureClass; // We cannot split out the method to a separate variable or else the "self" parameter will not // be properly passed to the method. if (featureClassRecord[name] === undefined) { error(`Failed to find a decorated exported method: ${name}`); } // In order for "this" to work properly in the method, we have to wrap the method invocation in // an arrow function. const wrappedMethod = (...args) => // We use a non-null assertion since we have already validated that the function exists. (See // the above comment.) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion featureClassRecord[name](...args); return [name, wrappedMethod]; }); }