isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
245 lines (244 loc) • 12 kB
JavaScript
;
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);
}
}