UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

245 lines (244 loc) • 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ModFeature = exports.MOD_FEATURE_CUSTOM_CALLBACKS_KEY = exports.MOD_FEATURE_CALLBACKS_KEY = void 0; const array_1 = require("../functions/array"); const tstlClass_1 = require("../functions/tstlClass"); const types_1 = require("../functions/types"); const utils_1 = require("../functions/utils"); exports.MOD_FEATURE_CALLBACKS_KEY = "__callbacks"; exports.MOD_FEATURE_CUSTOM_CALLBACKS_KEY = "__customCallbacks"; const WRAPPED_CALLBACK_METHODS_KEY = "__wrappedCallbackMethods"; const WRAPPED_CUSTOM_CALLBACK_METHODS_KEY = "__wrappedCustomCallbacksMethods"; /** * Helper class for mods that want to represent their individual features as classes. Extend your * mod features from this class in order to enable the `@Callback` and `@CustomCallback` decorators * that automatically subscribe to callbacks. * * It is recommended that you use the `initModFeatures` helper function to instantiate all of your * mod classes (instead of instantiating them yourself). This is so that any attached `v` objects * are properly registered with the save data manager; see below. * * If you are manually instantiating a mod feature yourself, then: * * - You must pass your upgraded mod as the first argument to the constructor. * - In almost all cases, you will want the callback functions to be immediately subscribed after * instantiating the class. However, if this is not the case, you can pass `false` as the optional * second argument to the constructor. * * If your mod feature has a property called `v`, it will be assumed that these are variables that * should be managed by the save data manager. Unfortunately, due to technical limitations with * classes, this registration will only occur if you initialize the class with the `initModFeatures` * helper function. (This is because the parent class does not have access to the child's properties * upon first construction.) */ class ModFeature { mod; /** * An optional method that allows for conditional callback execution. If specified, any class * method that is annotated with a `@Callback` or `@CallbackCustom` decorator will only be fired * if the executed conditional function returns true. * * This property is used to easily turn entire mod features on and off (rather than repeating * conditional logic and early returning at the beginning of every callback function). * * Since the specific information for the firing callback is passed as arguments into the * conditional method, you can also write logic that would only apply to a specific type of * callback. * * By default, this is set to null, which means that all callback methods will fire * unconditionally. Override this property in your class if you need to use it. * * The function has the following signature: * * ```ts * <T extends boolean>( * vanilla: T, // Whether this is a vanilla or custom callback. * modCallback: T extends true ? ModCallback : ModCallbackCustom, * ...callbackArgs: unknown[] // This would be e.g. `pickup: EntityPickup` for the `POST_PICKUP_INIT` callback. * ) => boolean; * ``` */ shouldCallbackMethodsFire = null; /** * Whether the feature has registered its callbacks yet. * * This will almost always be equal to true unless you explicitly passed `false` to the second * argument of the constructor. */ initialized = false; constructor(mod, init = true) { this.mod = mod; if (init) { this.init(); } } /** * Runs the `Mod.AddCallback` and `ModUpgraded.AddCallbackCustom` methods for all of the decorated * callbacks. Also registers/unregisters the "v" variable on the save data manager. * * @param init Optional. Whether to initialize or uninitialize. Default is true. */ init(init = true) { if (this.initialized === init) { return; } this.initialized = init; const constructor = (0, tstlClass_1.getTSTLClassConstructor)(this); (0, utils_1.assertDefined)(constructor, "Failed to get the TSTL class constructor for a mod feature."); const tstlClassName = (0, tstlClass_1.getTSTLClassName)(this); (0, utils_1.assertDefined)(tstlClassName, "Failed to get the TSTL class name for a mod feature."); initDecoratedCallbacks(this, constructor, tstlClassName, true, init); initDecoratedCallbacks(this, constructor, tstlClassName, false, init); initSaveDataManager(this, tstlClassName, init); } /** * Runs the `Mod.RemoveCallback` and `ModUpgraded.RemoveCallbackCustom` methods for all of the * decorated callbacks. * * This is just an alias for `ModFeature.init(false)`. */ uninit() { this.init(false); } } exports.ModFeature = ModFeature; function initDecoratedCallbacks(modFeature, constructor, tstlClassName, vanilla, init) { const modFeatureConstructor = constructor; const callbackTuplesKey = vanilla ? exports.MOD_FEATURE_CALLBACKS_KEY : exports.MOD_FEATURE_CUSTOM_CALLBACKS_KEY; const callbackTuples = modFeatureConstructor[callbackTuplesKey]; if (callbackTuples === undefined) { return; } if (!(0, array_1.isArray)(callbackTuples)) { error(`Failed to initialize/uninitialize the decorated callbacks on a mod feature since the callback arguments on the key of "${callbackTuplesKey}" was not an array and was instead of type: ${type(callbackTuples)}`); } for (const callbackTuple of callbackTuples) { if (!(0, array_1.isArray)(callbackTuple)) { error(`Failed to initialize/uninitialize the decorated callbacks on a mod feature since one of the callback arguments on the key of "${callbackTuplesKey}" was not an array and was instead of type: ${type(callbackTuple)}`); } const modCallback = callbackTuple[0]; if (!(0, types_1.isInteger)(modCallback)) { error(`Failed to get the callback number from the callback tuple for class: ${tstlClassName}`); } const priority = callbackTuple[1]; if (!(0, types_1.isInteger)(priority)) { error(`Failed to get the callback priority from the callback tuple for class: ${tstlClassName}`); } const callback = callbackTuple[2]; if (!(0, types_1.isFunction)(callback)) { error(`Failed to get the callback function from the callback tuple for class: ${tstlClassName}`); } const parameters = callbackTuple[3]; // We must pass false as the second argument to `isArray` since the callback parameters may not // necessarily be contiguous. (They might be separated by `undefined` values.) if (!(0, array_1.isArray)(parameters, false)) { error(`Failed to get the callback parameters from the callback tuple for class: ${tstlClassName}`); } // eslint-disable-next-line @typescript-eslint/dot-notation, @typescript-eslint/prefer-destructuring const mod = modFeature["mod"]; if (init) { addCallback(modFeature, modFeatureConstructor, mod, modCallback, // eslint-disable-line complete/strict-enums priority, callback, parameters, vanilla); } else { removeCallback(modFeatureConstructor, mod, modCallback, // eslint-disable-line complete/strict-enums vanilla); } } } function addCallback(modFeature, modFeatureConstructor, mod, modCallback, priority, callback, // eslint-disable-line @typescript-eslint/no-unsafe-function-type parameters, vanilla) { // We need to wrap the callback in a new function so that we can explicitly pass the class as the // first argument. (Otherwise, the method will not be able to properly access `this`. const wrappedCallback = (...callbackArgs) => { // eslint-disable-next-line @typescript-eslint/dot-notation const conditionalFunc = modFeature["shouldCallbackMethodsFire"]; if (conditionalFunc !== null) { const shouldRun = conditionalFunc(vanilla, modCallback, ...callbackArgs); if (!shouldRun) { return undefined; } } const castedCallback = callback; return castedCallback(modFeature, ...callbackArgs); }; // We need to save the wrapped function for later (so we can unregister them). if (vanilla) { const modCallbackVanilla = modCallback; let wrappedMethodsMap = modFeatureConstructor[WRAPPED_CALLBACK_METHODS_KEY]; if (wrappedMethodsMap === undefined) { wrappedMethodsMap = new Map(); modFeatureConstructor[WRAPPED_CALLBACK_METHODS_KEY] = wrappedMethodsMap; } wrappedMethodsMap.set(modCallbackVanilla, wrappedCallback); } else { const modCallbackCustom = modCallback; let wrappedMethodsMap = modFeatureConstructor[WRAPPED_CUSTOM_CALLBACK_METHODS_KEY]; if (wrappedMethodsMap === undefined) { wrappedMethodsMap = new Map(); modFeatureConstructor[WRAPPED_CUSTOM_CALLBACK_METHODS_KEY] = wrappedMethodsMap; } wrappedMethodsMap.set(modCallbackCustom, wrappedCallback); } if (vanilla) { mod.AddPriorityCallback(modCallback, priority, wrappedCallback, ...parameters); } else { mod.AddPriorityCallbackCustom(modCallback, priority, wrappedCallback, ...parameters); } } function removeCallback(modFeatureConstructor, mod, modCallback, vanilla) { if (vanilla) { const modCallbackVanilla = modCallback; const wrappedMethodsMap = modFeatureConstructor[WRAPPED_CALLBACK_METHODS_KEY]; if (wrappedMethodsMap === undefined) { return; } const wrappedCallback = wrappedMethodsMap.get(modCallbackVanilla); mod.RemoveCallback(modCallback, wrappedCallback); } else { const modCallbackCustom = modCallback; const wrappedMethodsMap = modFeatureConstructor[WRAPPED_CUSTOM_CALLBACK_METHODS_KEY]; if (wrappedMethodsMap === undefined) { return; } const wrappedCallback = wrappedMethodsMap.get(modCallbackCustom); mod.RemoveCallbackCustom(modCallback, wrappedCallback); } } /** * This will only work for end-users who are calling the `ModFeature.init` method explicitly. (See * the discussion in the `ModFeature` comment.) */ function initSaveDataManager(modFeature, tstlClassName, init) { // Do nothing if this class does not have any variables. const { v } = modFeature; if (v === undefined) { return; } if (!(0, types_1.isTable)(v)) { error('Failed to initialize a mod feature class due to having a "v" property that is not an object. (The "v" property is supposed to be an object that holds the variables for the class, managed by the save data manager.)'); } // Do nothing if we have not enabled the save data manager. // eslint-disable-next-line @typescript-eslint/dot-notation const mod = modFeature["mod"]; const saveDataManagerMethodName = init ? "saveDataManager" : "saveDataManagerRemove"; const saveDataManagerMethod = mod[saveDataManagerMethodName]; (0, utils_1.assertDefined)(saveDataManagerMethod, 'Failed to initialize a mod feature class due to having a "v" object and not having the save data manager initialized. You must pass "ISCFeature.SAVE_DATA_MANAGER" to the "upgradeMod" function.'); if (typeof saveDataManagerMethod !== "function") { error(`The "${saveDataManagerMethodName}" property of the "ModUpgraded" object was not a function.`); } if (init) { saveDataManagerMethod(tstlClassName, v); } else { saveDataManagerMethod(tstlClassName); } }